5.2 · Bootstrap + TypeORM 2 connections
บทนี้อธิบาย boot sequence ตั้งแต่ node dist/src/main.js จนถึงพร้อมรับ request พร้อมเจาะลึกการตั้งค่า TypeORM 2 connections (center + nbc) ที่เชื่อมไป HRMI MSSQL ทั้งสอง database
Boot sequence
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
- สร้าง NestApplication — ใช้
I3GatewayLogger(custom logger) แทน default ส่งผลให้ทุก log เขียนลงไฟล์ด้วย (บท 5.7) - ดึง ConfigService — อ่าน env var ผ่าน
ConfigModule.forRoot({ load: [configuration] })ที่ register ในAppModule - อ่าน port — default 3000
- ตั้ง global prefix
/api— ทุก route จะอยู่ใต้/api/... - เพิ่ม body parser — limit 50mb ผิดปกติ (default 100kb) เดาว่าเพื่อรองรับ
empCodearray ในHrPaySlipControllerที่อาจยาว - register globals 4 ตัว — Filter, Guard, Pipe, CORS
- ตั้งค่า Swagger —
/docsendpoint - listen ที่
0.0.0.0:3000— bind ทุก interface (ไม่ใช่แค่ localhost)
Globals 4 ตัวที่ register
| Global | Class | ไฟล์ | หน้าที่ |
|---|---|---|---|
| Filter | HttpExceptionFilter | libs/exception/http-exception.filter.ts | จับ HttpException → format response |
| Guard | JwtAuthGuard | libs/jwt/jwt-auth.guard.ts | auth guard — แต่ถูก bypass ทุก route (บท 5.5) |
| Pipe | ValidationPipe | NestJS built-in | validate 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
@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
| Property | center | nbc |
|---|---|---|
name | 'center' | 'nbc' |
database | dbHRMI_Center | dbHRMI_Center_NBC |
host | เดียวกัน (DB_HOST) | เดียวกัน |
port | เดียวกัน (DB_PORT) | เดียวกัน |
username | เดียวกัน (DB_USER) | เดียวกัน |
password | เดียวกัน (DB_PASSWORD) | เดียวกัน |
entities glob | **/*.entity | **/*.nbc.entity |
synchronize | false | false |
trustServerCertificate | true | true |
จุดสำคัญ — ทั้งสอง 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
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
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 ได้ทุกที่ไม่ต้อง importConfigModuleซ้ำ- ไม่มี Joi schema → env var ผิด/ขาดจะ crash ตอน runtime (เช่น
port: undefinedจะทำให้app.listen(undefined)fail) ไม่ใช่ตอน boot ที่มี validation ล่วงหน้า - ไม่มี type casting →
portเป็น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
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)
.envcommitted ใน repo —.gitignore:3บอกว่า ignore.env*แต่ไฟล์ถูก force-add เข้าไปในประวัติ git อยู่แล้ว ทำให้ credential อยู่ใน git history ตลอดไปDB_PASSWORD=Xfwo_gawPp.!— plaintext UAT DB password รั่วDB_HOST=10.188.128.58— internal IP รั่วNODE_ENV=prodแต่ใช้ UAT user —DB_USER=CENTRALIZE_APP_UATบ่งบอกว่าไฟล์นี้อาจไม่ใช่ production จริง แต่ก็ยังมี password ที่ใช้กับ UAT database จริง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_CentervsdbHRMI_Center_NBC) synchronize: false— TypeORM ไม่ alter schema ของ HRMIlogging: ['query']— รั่ว PII ลง log ทุก query ต้องปิดใน productiontrustServerCertificate: true— จำเป็นเพราะ HRMI ใช้ self-signed cert แต่เสี่ยง MITM- ไม่มี Joi validation สำหรับ env var → boot ผ่านแม้ config ผิด จะ crash ตอน runtime
.envcommitted — credential รั่วที่ต้องเปลี่ยนทันที (บท 5.8)- ไม่มี cron, ไม่มี health check, ไม่มี graceful shutdown — ขาดการเตรียมพร้อมสำหรับ production หลายจุด