Skip to content

6.2 · Bootstrap + TypeORM 3 connections

บทนี้อธิบายจุดเริ่มต้นของ service ตั้งแต่ main.ts ไปจนถึงการ setup TypeORM connections ทั้ง 3 ตัวที่เชื่อมไปยัง MSSQL host เดียวกัน


src/main.ts:14-47 — bootstrap

14:47:benefit-api/src/main.ts
async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule, {
    logger: new I3GatewayLogger('System'),
  });
  // ...
  const port = config.get('port') || 3000;
  const prefix = `${process.env.BASE_PATH}api/v1`;
  app.setGlobalPrefix(prefix);
  app.useStaticAssets(join(process.cwd(), '/uploads'), {
    prefix: '/benefit-api/',
  });
  app.use(bodyParser.json({ limit: '50mb' }));
  app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
  app.useGlobalFilters(new HttpExceptionFilter());
  app.useGlobalGuards(new JwtAuthGuard(reflector));
  app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));
  app.enableCors({
    origin: '*',
    allowedHeaders: '*',
    methods: 'GET,PUT,POST,DELETE,PATCH,OPTIONS',
  });
  const document = SwaggerModule.createDocument(app, documentConfig);
  SwaggerModule.setup(`${process.env.BASE_PATH}docs`, app, document);
  await app.listen(port, '0.0.0.0', () => { /* ... */ });
}

จุดที่ต้องสังเกต

รายการค่าหมายเหตุ
Port9320 (จาก .env:5)fallback 3000 ใน code ไม่เคยถูกใช้จริง
Global prefix${BASE_PATH}api/v1benefit-api/api/v1BASE_PATH จาก .env:3
Swagger${BASE_PATH}docsbenefit-api/docsเปิดให้ใครก็ได้โดยไม่ auth
Static assets/uploads ที่ prefix /benefit-api/แสดงไฟล์ที่ upload สาธารณะ — ดู 6.8 S9
Body limit50mbใหญ่เพราะรองรับ base64 file upload
Global guardJwtAuthGuard (JWT)แต่ bypass ด้วย @Public() เยอะมาก — ดู 6.5
Global pipeValidationPipe({ whitelist: true, forbidNonWhitelisted: true })ดี — แต่ถูก undermined โดย body: any ในบาง controller (เช่น studyLeave)
CORSorigin: '*', allowedHeaders: '*'เปิดหมด — ดู 6.8 S7
Helmetไม่ได้ติดตั้งgrep helmet ไม่พบ
CSRFไม่ได้ติดตั้งไม่มี csurf

src/app.module.ts:40-57 — 3 TypeORM connections ผ่าน forRootAsync

40:57:benefit-api/src/app.module.ts
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      name: 'main',
      useFactory: (configService: ConfigService) => configService.get('db'),
      inject: [ConfigService],
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      name: 'vtrc',
      useFactory: (configService: ConfigService) => configService.get('vtrcDB'),
      inject: [ConfigService],
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      name: 'hrmi',
      useFactory: (configService: ConfigService) => configService.get('dbHRMI'),
      inject: [ConfigService],
    }),

แต่ละ connection โหลด config จาก key ต่างกันใน ConfigService ซึ่ง map กลับไปยัง config/configuration.ts


config/configuration.ts:8-61 — connection options

ทั้งสาม connections ใช้ pattern เดียวกัน ขอยกตัวอย่าง main connection:

8:25:benefit-api/config/configuration.ts
  db: {
    type: process.env.DB_TYPE,
    host: process.env.DB_HOST,
    port: +process.env.DB_PORT,
    username: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    autoLoadEntities: process.env.NODE_ENV === 'dev',
    synchronize: process.env.NODE_ENV === 'dev',
    logging: [`error`, `query`],
    entities: [process.cwd() + '/dist/**/*.entity{.ts,.js}'],
    extra: {
      trustServerCertificate: true,
      encrypt: false,
      connectionTimeout: 15000,
      requestTimeout: 15000,
    },
  } as TypeOrmModuleOptions,

ตารางเปรียบเทียบ 3 connections

ConnectionnameDB hostDB namesynchronizelogging
main (db)'main'DB_HOST=10.188.128.61benefitNODE_ENV === 'dev'['error','query']
vtrcDB'vtrc'VTRC_DB_HOST=10.188.128.61vtrcfalse (hardcoded)['error','query']
dbHRMI'hrmi'DB_HRMI_HOST=10.188.128.61dbHRMI_Centerfalse (hardcoded)['error','query']

ข้อสังเกตสำคัญ

  1. ทั้งสาม connections ชี้ไปที่ MSSQL host เดียวกัน 10.188.128.61 — เป็นคนละ database ใน instance เดียวกัน ไม่ใช่ separate server
  2. ทั้งหมดล็อกอินด้วย sa (DB_USER=sa, DB_PASSWORD เดียวกัน) — ดู 6.8 S11
  3. trustServerCertificate: true + encrypt: false สำหรับทุก connection — ปิด TLS และข้าม cert validation
  4. logging: ['error', 'query'] เปิดไว้ทุก connection รวมถึง production — ทุก SQL statement ถูก log (รวมถึง query ที่มี user input ที่ถูก interpolate เข้าไป) — ดู 6.8 S8
  5. synchronize เปิดเฉพาะ main ใน NODE_ENV === 'dev' เท่านั้น — สำหรับ vtrc และ hrmi ถูก hardcoded เป็น false เสมอ เพราะเป็น schema ของระบบอื่น

Dockerfile:1-30 — production image

1:30:benefit-api/Dockerfile
FROM node:20-alpine
WORKDIR /usr/src/app
ENV TZ="Asia/Bangkok"
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk-repositories && \
    apk update && apk add --no-cache nmap harfbuzz freetype ttf-freefont nss chromium
RUN apk add chromium
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
COPY package.json ./
ADD . .
RUN yarn
RUN yarn build
EXPOSE 9320
CMD ["yarn","start:prod"]

ปัญหาของ Dockerfile

ปัญหารายละเอียด
ติดตั้ง nmap (บรรทัด 9)nmap เป็น network scanning tool — ไม่มีเหตุผลที่ต้องอยู่ใน production runtime image — ขยาย attack surface ถ้ามี RCE
ใช้ Aliyun mirror (บรรทัด 8)mirrors.aliyun.com เป็น mirror ของจีน — บ่งชี้ว่า build environment อยู่ในจีน
ติดตั้ง chromium สองรอบ (บรรทัด 8 และ 16)redundant — ทิ้งขยะใน image
ADD . . (บรรทัด 22)copy ทุกอย่างรวมทั้ง .env เข้า image — ทำให้ .env ถูก bake เข้าไปแม้จะมี .dockerignore ก็ตาม (เพราะ .dockerignore มีแค่ yarn.lock)
Node version skewDocker ใช้ node:20-alpine แต่ CI pipeline ใช้ node:16 (bitbucket-pipelines.yml:6) — build กับ runtime คนละ major version

ลำดับการ startup

npm run start:prod
  └─ node dist/main.js
      └─ NestFactory.create(AppModule)
          ├─ ConfigModule.forRoot({ load: [configuration] })
          ├─ TypeOrmModule.forRootAsync('main')  ← ต่อ MSSQL 'benefit'
          ├─ TypeOrmModule.forRootAsync('vtrc')  ← ต่อ MSSQL 'vtrc'
          ├─ TypeOrmModule.forRootAsync('hrmi')  ← ต่อ MSSQL 'dbHRMI_Center'
          └─ 12 feature modules (forRoot + controllers + services)
      └─ app.setGlobalPrefix('benefit-api/api/v1')
      └─ app.useGlobalGuards(JwtAuthGuard)
      └─ app.useGlobalPipes(ValidationPipe)
      └─ SwaggerModule.setup('benefit-api/docs')
      └─ app.listen(9320, '0.0.0.0')

ทั้งสาม DB connections เปิดตอน NestJS bootstrap — ถ้า host 10.188.128.61 ไม่ตอบ, ทั้ง service ไม่ start


ไปต่อ