Skip to content

5.2 · Bootstrap + TypeORM 2 connections

บทนี้อธิบาย boot sequence ตั้งแต่ node dist/src/main.js จนถึงพร้อมรับ request พร้อมเจาะลึกการตั้งค่า TypeORM 2 connections (center + nbc) ที่เชื่อมไป HRMI MSSQL ทั้งสอง database


Boot sequence

12:43:centralize-api/src/main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: new I3GatewayLogger('System'),
  });

  const logger = new I3GatewayLogger('Main');
  const config = app.get<ConfigService>(ConfigService);
  const port = config.get('port') || 3000;
  const reflector = app.get(Reflector);
  const prefix = 'api';

  app.setGlobalPrefix(prefix);
  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(`docs`, app, document);
  await app.listen(port, '0.0.0.0', () => {
    logger.log(`Listening at http://localhost:${port}/docs`);
    logger.log(`Environment at `);
  });
}

ลำดับการทำงานเมื่อ boot

  1. สร้าง NestApplication — ใช้ I3GatewayLogger (custom logger) แทน default ส่งผลให้ทุก log เขียนลงไฟล์ด้วย (บท 5.7)
  2. ดึง ConfigService — อ่าน env var ผ่าน ConfigModule.forRoot({ load: [configuration] }) ที่ register ใน AppModule
  3. อ่าน port — default 3000
  4. ตั้ง global prefix /api — ทุก route จะอยู่ใต้ /api/...
  5. เพิ่ม body parser — limit 50mb ผิดปกติ (default 100kb) เดาว่าเพื่อรองรับ empCode array ใน HrPaySlipController ที่อาจยาว
  6. register globals 4 ตัว — Filter, Guard, Pipe, CORS
  7. ตั้งค่า Swagger/docs endpoint
  8. listen ที่ 0.0.0.0:3000 — bind ทุก interface (ไม่ใช่แค่ localhost)

Globals 4 ตัวที่ register

GlobalClassไฟล์หน้าที่
FilterHttpExceptionFilterlibs/exception/http-exception.filter.tsจับ HttpException → format response
GuardJwtAuthGuardlibs/jwt/jwt-auth.guard.tsauth guard — แต่ถูก bypass ทุก route (บท 5.5)
PipeValidationPipeNestJS built-invalidate DTO ด้วย class-validator
CORS(built-in)origin: '*' ทุก origin

Bug ที่น่าสนใจ

  • logger.log('Environment at ') ที่บรรทัด 41 — empty string ลืมใส่ env name ทำให้ log ขาดบริบทว่ารัน environment อะไร
  • app.enableShutdownHooks() ไม่ได้เรียก → ไม่มี graceful shutdown เมื่อได้รับ SIGTERM (Kubernetes rolling deploy จะตัดการเชื่อมต่อกลางคัน)
  • helmet, compression, cookie-parser ไม่ได้ใช้ → ขาดการเตรียมพร้อมสำหรับ production

AppModule — root module + TypeORM

14:65:centralize-api/src/app.module.ts
@Module({
  imports: [
    HrPaySlipModule,
    HrTimeTempModule,
    EmployeeModule,
    ConfigModule.forRoot({
      load: [configuration],
      isGlobal: true,
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      name: 'center',
      useFactory: (configService: ConfigService) => ({
        type: 'mssql',
        host: configService.get('db.host'),
        port: +configService.get<number>('db.port'),
        username: configService.get('db.user'),
        password: configService.get('db.password'),
        database: configService.get('db.databaseName'),
        synchronize: false,
        logging: [`error`, `query`],
        entities: [__dirname + '/**/*.entity{.ts,.js}'],
        extra: {
          trustServerCertificate: true,
        },
      }),
      inject: [ConfigService],
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      name: 'nbc',
      useFactory: (configService: ConfigService) => ({
        type: 'mssql',
        host: configService.get('db.host'),
        port: +configService.get<number>('db.port'),
        username: configService.get('db.user'),
        password: configService.get('db.password'),
        database: configService.get('db.databaseNameNBC'),
        synchronize: false,
        logging: [`error`, `query`],
        entities: [__dirname + '/**/*.nbc.entity{.ts,.js}'],
        extra: {
          trustServerCertificate: true,
        },
      }),
      inject: [ConfigService],
    }),
  ],
  controllers: [HrPaySlipController, AppController],
  providers: [HrPaySlipService, JwtStrategy, AppService],
})
export class AppModule {}

TypeORM 2 connections

Propertycenternbc
name'center''nbc'
databasedbHRMI_CenterdbHRMI_Center_NBC
hostเดียวกัน (DB_HOST)เดียวกัน
portเดียวกัน (DB_PORT)เดียวกัน
usernameเดียวกัน (DB_USER)เดียวกัน
passwordเดียวกัน (DB_PASSWORD)เดียวกัน
entities glob**/*.entity**/*.nbc.entity
synchronizefalsefalse
trustServerCertificatetruetrue

จุดสำคัญ — ทั้งสอง connection ใช้ MSSQL instance เดียวกัน แค่ต่าง database name ในปกติจะใช้ cross-database query ใน SQL เดียวกันได้เลย แต่ที่นี่แยก connection เพื่อให้ TypeORM รู้จัก entity แยกตาม database (เห็นได้จาก @Entity({ database: '...' }) ในไฟล์ entity)

synchronize: false — สำคัญ

ทั้งสอง connection ตั้ง synchronize: false หมายความว่า TypeORM จะไม่ auto-create/alter table ตาม entity definition — ปลอดภัยเพราะ HRMI tables เป็นของระบบ CU/RC ถ้า TypeORM ไป sync schema จะพัง HRMI ทันที

เปรียบเทียบกับ vtrc-api ที่ใช้ syncModelToTable() รันทุก boot (บท 3.5) — pattern ที่ตรงกันข้าม

logging: ['error', 'query'] — รั่ว PII

ทั้งสอง connection เปิด logging ระดับ queryทุก SQL query และผลลัพธ์จะถูก log ไป stdout ผ่าน I3GatewayLogger รวมถึงข้อมูลพนักงาน (empCode, fullName, identityCard, bankAccount) ที่ query กลับมา

ผลกระทบ — log file ที่เขียนลง ${cwd}/logs/DD-MM-YYYY.log จะมี PII จำนวนมาก ถ้าไฟล์รั่วออกนอกระบบจะเป็น data breach

การแก้ — เปลี่ยนเป็น logging: ['error'] อย่างเดียวใน production หรือใช้ custom logger ที่ redact PII fields (บท 5.8)

trustServerCertificate: true

MSSQL driver จะไม่ตรวจ TLS certificate ของ MSSQL server → ยอมรับ self-signed cert โดยไม่ error ใน production ที่ใช้ real CA cert ควรตั้งเป็น false แต่ HRMI server ภายในองค์กรใช้ self-signed → จำเป็นต้องเปิด

ความเสี่ยง — ถ้ามี man-in-the-middle ระหว่าง centralize-api กับ MSSQL จะ intercept query ได้โดยไม่มีการตรวจ detected

Entity glob bug

center: __dirname + '/**/*.entity{.ts,.js}'
nbc:    __dirname + '/**/*.nbc.entity{.ts,.js}'

glob **/*.entity จะ match ทั้ง *.entity.ts และ *.nbc.entity.ts (เพราะไฟล์ NBC ก็ลงท้ายด้วย .entity.ts อยู่แล้ว) ส่งผลให้ entity NBC ถูก load เข้า connection center ด้วย แต่เนื่องจากแต่ละ entity declare @Entity({ database: '...' }) ไว้ TypeORM จึง route query ไป database ที่ถูกต้องอยู่ดี → code smell แต่ไม่พังจริง

Module duplication

61:65:centralize-api/src/app.module.ts
  controllers: [HrPaySlipController, AppController],
  providers: [HrPaySlipService, JwtStrategy, AppService],
})
export class AppModule {}

HrPaySlipController + HrPaySlipService ถูก register ทั้งใน HrPaySlipModule (ดู hrpayslip.module.ts) และใน AppModule อีกครั้ง → double registration แต่ NestJS ยอมรับ (จะ override ตัวเดิม) เป็น code smell ไม่ใช่ bug


ConfigModule — env var loader ไม่มี Joi

1:14:centralize-api/config/configuration.ts
export const configuration = () => ({
  port: process.env.PORT,
  jwtSecretKey: process.env.SECRET_KEY,
  db: {
    type: process.env.DB_TYPE,
    host: process.env.DB_HOST,
    port: process.env.DB_PORT,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    databaseName: process.env.DB_NAME,
    databaseNameNBC: process.env.DB_NAME_NBC,
  },
});

จุดสำคัญ

  • isGlobal: true → ConfigService inject ได้ทุกที่ไม่ต้อง import ConfigModule ซ้ำ
  • ไม่มี Joi schema → env var ผิด/ขาดจะ crash ตอน runtime (เช่น port: undefined จะทำให้ app.listen(undefined) fail) ไม่ใช่ตอน boot ที่มี validation ล่วงหน้า
  • ไม่มี type castingport เป็น string ทำให้ต้อง cast ด้วย +configService.get<number>('db.port') ใน app.module.ts:29, 48 (ทำให้โค้ดอ่านยาก)
  • env var ที่ใช้PORT, SECRET_KEY, DB_TYPE, DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME, DB_NAME_NBC, PROJECT_NAME, NODE_ENV, SERVER_URL

การแก้ — เพิ่ม Joi validation schema ใน ConfigModule.forRoot({ validationSchema: ... }) เพื่อให้ boot ล้มเหลวตั้งแต่เริ่มต้นถ้า env var ไม่ครบ


.env — committed มี prod credentials

1:15:centralize-api/.env
NODE_ENV=prod
PROJECT_NAME=Centralize Api
SERVER_URL=http://10.188.128.160
PORT=3000

DB_TYPE=mssql
DB_HOST=10.188.128.58
DB_PORT=1433
DB_USER=CENTRALIZE_APP_UAT
DB_PASSWORD=Xfwo_gawPp.!
DB_NAME=dbHRMI_Center

DB_NAME_NBC=dbHRMI_Center_NBC

SECRET_KEY=AAAAB3NzaC1yc2EAAAADAQABAAABAQCMH5xOB6PH3INC7ScxkR2b3IChoJJDpCcQqgwwtzKxqkXBTUNqb+qE20Z2WJcnca9rPTjiuahlOChjRo8mELybKZt65NrWHYbj2hv4Yliybz8xPZJC9KIT3hlcwDv5jqqUmeDRNk1MUEn1SNXJDquaerv1vQU5EZYw0/hXYNc2Y+hqltWcdPCJ12cpBO9VXnOvtGWLzufMLOHTsupaPqcjRpid5xI7tx/cQZvSKw8gUA/9UNlX/XhSToU72NEQLpxBFyGhRqPYJQprvFpqdGOmhIzjsSTut5cKdxqYmsWUbGZgVVgEzLBdA/JXrCLWXoiLaohNciqvG/xdnzyKldJb

ปัญหาหลายชั้น (รายละเอียดเต็มในบท 5.8)

  1. .env committed ใน repo.gitignore:3 บอกว่า ignore .env* แต่ไฟล์ถูก force-add เข้าไปในประวัติ git อยู่แล้ว ทำให้ credential อยู่ใน git history ตลอดไป
  2. DB_PASSWORD=Xfwo_gawPp.! — plaintext UAT DB password รั่ว
  3. DB_HOST=10.188.128.58 — internal IP รั่ว
  4. NODE_ENV=prod แต่ใช้ UAT userDB_USER=CENTRALIZE_APP_UAT บ่งบอกว่าไฟล์นี้อาจไม่ใช่ production จริง แต่ก็ยังมี password ที่ใช้กับ UAT database จริง
  5. SECRET_KEY ผิดประเภท — string ที่ขึ้นต้นด้วย AAAAB3NzaC1yc2E เป็น base64 ของ ssh-rsa คือ SSH public key fragment ไม่ใช่ JWT secret แต่นำมาใช้เป็น HMAC secret สำหรับ sign JWT → broken crypto (บท 5.5)

Cron — ไม่มี

ค้นหา @nestjs/schedule, @Cron, ScheduleModule.forRoot ทั้ง repo ไม่พบ → centralize-api ไม่มี background job ใด ๆ ทุกการทำงานเกิดจาก HTTP request ที่เข้ามาเท่านั้น ตรงข้ามกับ vtrc-api ที่มี node-schedule cron 5+ jobs (บท 3.8) และ cu-central-api ที่มี conicleTransferLongTerm


Health check — ไม่มี

ค้นหา /health, /live, /ready, @nestjs/terminus, HealthCheck ทั้ง repo ไม่พบ → ไม่มี endpoint สำหรับ load balancer หรือ Kubernetes probe

ปัญหา — root /api ที่ดูเหมือนจะเป็น health check เพราะชื่อ method getHello แต่จริง ๆ รัน SQL query 130 บรรทัดที่ join หลาย tables ทำให้

  • ใช้ resource สูงทุกครั้งที่ probe
  • ถ้า MSSQL ล่ม getHello จะ timeout หรือ 500 → probe fail (ซึ่งก็ดีในบางมุม) แต่ไม่ใช่การออกแบบที่ตั้งใจ
  • ถ้าใช้ /api เป็น liveness probe จะ restart pod ทุกครั้งที่ MSSQL ช้า → cascade failure

การแก้ — เพิ่ม HealthController ที่ return { status: 'ok' } แบบง่ายสำหรับ liveness และ /health/ready ที่ check DB connection สำหรับ readiness probe


Flow การรับ request เมื่อ boot เสร็จ

HTTP request


┌──────────────────────────────────────────────┐
│ Express adapter (NestJS)                     │
├──────────────────────────────────────────────┤
│ 1. CORS check (origin: '*')                  │
│ 2. Body parser (limit 50mb)                  │
│ 3. Route match (/api/employee/...)           │
├──────────────────────────────────────────────┤
│ Globals (เรียงตามลำดับ NestJS)               │
│                                              │
│  • JwtAuthGuard (bypass — @Public)           │
│  • ValidationPipe (DTO check)                │
│  • HttpExceptionFilter (catch HttpException) │
├──────────────────────────────────────────────┤
│ Controller                                                   │
│   ↓ inject                                                   │
│ Service                                                      │
│   ↓ inject                                                   │
│ @InjectEntityManager('center' or 'nbc')                      │
│   ↓ entityManager.query(sql, params)                         │
│ MSSQL                                                        │
└──────────────────────────────────────────────┘


                  JSON response

สังเกตว่า JwtAuthGuard ทำงานก่อน ValidationPipe — ปกติแล้วใน NestJS guard ทำงานก่อน pipe แต่เพราะทุก route @Public() guard จะ return true ทันทีทำให้ auth check ไม่เกิดขึ้นจริง (บท 5.5)


สรุปประเด็นสำคัญ

  • Boot sequence ตรงไปตรงมา — ไม่มี complex startup logic
  • TypeORM 2 connections ใช้ MSSQL instance เดียวกัน ต่างแค่ database name (dbHRMI_Center vs dbHRMI_Center_NBC)
  • synchronize: false — TypeORM ไม่ alter schema ของ HRMI
  • logging: ['query']รั่ว PII ลง log ทุก query ต้องปิดใน production
  • trustServerCertificate: true — จำเป็นเพราะ HRMI ใช้ self-signed cert แต่เสี่ยง MITM
  • ไม่มี Joi validation สำหรับ env var → boot ผ่านแม้ config ผิด จะ crash ตอน runtime
  • .env committed — credential รั่วที่ต้องเปลี่ยนทันที (บท 5.8)
  • ไม่มี cron, ไม่มี health check, ไม่มี graceful shutdown — ขาดการเตรียมพร้อมสำหรับ production หลายจุด