11.6 · centralize-api (NestJS) REST + Swagger
บทนี้รวบรวม REST endpoints ทั้ง 17 ตัวของ centralize-api (NestJS) พร้อม DTOs, auth bypass ผ่าน @Public() และ Swagger doc
ภาพรวม
| ตัวเลข | ค่า |
|---|---|
| Controllers | 4 |
| Endpoints | 17 (15 GET + 2 POST, 0 PUT/DELETE/PATCH) |
| DTO classes | 19 |
| Services | 7 |
| Custom repositories | 3 |
| TypeORM connections | 2 (center + nbc) |
| Global prefix | api → ทุก path เริ่มด้วย /api/ |
| Swagger docs | /docs |
| Interceptors | 0 (ไม่มี response wrapper) |
Bootstrap + global config
app.setGlobalPrefix('api') // line 21-23
app.useGlobalGuards(new JwtAuthGuard(reflector)) // line 27
app.useGlobalFilters(new HttpExceptionFilter()) // line 26
app.useGlobalPipes(new ValidationPipe({ // line 28-30
whitelist: true,
forbidNonWhitelisted: true,
}))
app.enableCors({ origin: '*', allowedHeaders: '*', methods: 'GET,PUT,POST,DELETE,PATCH,OPTIONS' }) // line 31-35
const document = SwaggerModule.createDocument(app, documentConfig) // line 37
SwaggerModule.setup('docs', app, document) // line 38Swagger config
export const documentConfig = new DocumentBuilder()
.setTitle('Centralize API')
.setDescription('Centralize API')
.setVersion('1.0')
.addServer(`http://10.188.128.160:${process.env.PORT || 3000}/`)
.addServer(`http://localhost:${process.env.PORT || 3000}/`)
.addBearerAuth()
.build()- Title:
Centralize API - Version:
1.0 - Servers: LAN (prod) + localhost
- Auth scheme: Bearer JWT (แต่ถูก bypass จริง)
Controllers — 4 ตัว
| Controller | ไฟล์ | @ApiTags | @Controller(...) | Endpoints |
|---|---|---|---|---|
| AppController | src/app.controller.ts | Root | (root) | 1 |
| EmployeeController | src/modules/employee/employee.controller.ts | Employee | employee | 14 |
| HrPaySlipController | src/modules/hrPaySlip/hrpayslip.controller.ts | HR Pay Slip | hrPaySlip | 1 |
| HrTimeTempController | src/modules/hrTimeTemp/hrtimetemp.controller.ts | HR Time Temp | hrTimeTemp | 1 |
Endpoints ทั้งหมด — 17 ตัว
AppController — 1 endpoint
| # | Method | Path | Handler | บรรทัด | Return |
|---|---|---|---|---|---|
| 1 | GET | /api/ | getHello | app.controller.ts:26-30 | RootObject[] — org tree |
EmployeeController — 14 endpoints (ทั้งหมด @Public())
| # | Method | Path | Handler | บรรทัด | Return |
|---|---|---|---|---|---|
| 2 | GET | /api/employee | getEmployees | 38-42 | vtrcMainProfile[] |
| 3 | GET | /api/employee/cu | getEmployeesCU | 44-48 | vtrcAllProfile[] |
| 4 | GET | /api/employee/manager | getManagers | 50-54 | manager list |
| 5 | GET | /api/employee/posManage | getPositionManagers | 56-60 | position manager list |
| 6 | GET | /api/employee/position | getPositions | 62-66 | position list |
| 7 | GET | /api/employee/division | getDivisions | 68-72 | diviCode, diviName, sourceDB |
| 8 | GET | /api/employee/department | getDepartments | 74-78 | deptCode, deptName, sourceDB |
| 9 | GET | /api/employee/group | getEmployeeGroups | 80-84 | employee group list |
| 10 | GET | /api/employee/level | getEmployeeLevels | 86-90 | employee level list |
| 11 | GET | /api/employee/posTechnical | getTechnicalPositions | 92-96 | technical position list |
| 12 | GET | /api/employee/organize | getOrganizations | 98-102 | org list |
| 13 | GET | /api/employee/getByEmpCode | getEmployeeByEmployeeCode | 104-108 | single row |
| 14 | GET | /api/employee/getEmployeeDisaster | getEmployeeDisaster | 110-114 | disaster filter |
| 15 | GET | /api/employee/bankAccount | getBankAccount | 116-120 | bank + branch |
HrPaySlipController — 1 endpoint
| # | Method | Path | Handler | บรรทัด | Return |
|---|---|---|---|---|---|
| 16 | POST | /api/hrPaySlip | getPaySlipDT | hrpayslip.controller.ts:17-21 | payslip detail rows |
HrTimeTempController — 1 endpoint
| # | Method | Path | Handler | บรรทัด | Return |
|---|---|---|---|---|---|
| 17 | POST | /api/hrTimeTemp/import | getShiftTimeWithWork | hrtimetemp.controller.ts:17-21 | { date, timeIn, timeOut }[] |
ตัวอย่าง endpoint signatures
GET /api/employee — ค้นหาพนักงาน
// employee.controller.ts:38-42
@Public()
@Get()
getEmployees(@Query() query: GetEmployeeQueryDto) {
return this.employeeService.getEmployees(query)
}GET /api/employee/getByEmpCode — รายบุคคล
// employee.controller.ts:104-108
@Public()
@Get('getByEmpCode')
getEmployeeByEmployeeCode(@Query() query: GetEmployeeByEmpCodeDto) {
return this.employeeService.getEmployeeByEmployeeCode(query)
}POST /api/hrPaySlip — payslip batch
// hrpayslip.controller.ts:17-21
@Public()
@Post()
getPaySlipDT(@Body() body: EmpCodeListDto) {
return this.hrPaySlipService.getPaySlipDT(body)
}POST /api/hrTimeTemp/import — shift time
// hrtimetemp.controller.ts:17-21
@Public()
@Post('import')
getShiftTimeWithWork(@Body() body: GethrTimeTempQueryDto) {
return this.hrTimeTempService.getShiftTimeWithWork(body)
}DTOs — 19 classes
ไฟล์: src/dto/employee.dto.ts (16 classes), hrpayslip.dto.ts (1), hrTimeTemp.dto.ts (1 DTO + 2 interfaces), test.dto.ts (1 — boilerplate leftover)
Validation decorators ที่ใช้
@IsString, @IsOptional, @IsNotEmpty, @IsDate, @IsInt, @IsArray, @IsDateString, @ArrayMinSize, @Type(() => Date) (จาก class-transformer)
ตัวอย่าง DTO
GetEmployeeQueryDto — 9 optional string fields:
export class GetEmployeeQueryDto {
@ApiProperty({ required: false })
@IsString()
@IsOptional()
department?: string;
@ApiProperty({ required: false })
@IsString()
@IsOptional()
sourceDB?: string;
// ... diviCode, orgCode, posManageID, positionCode, empCode, groupCode, levelCode
}EmpCodeListDto:
import { IsArray, IsString } from 'class-validator';
export class EmpCodeListDto {
@IsArray()
@IsString({ each: true })
empCode: string[];
}GethrTimeTempQueryDto:
export class GethrTimeTempQueryDto {
@IsString()
@IsNotEmpty()
empCode: string;
@IsArray()
@IsDateString(null, { each: true })
@ArrayMinSize(1)
workingDate: Date[];
}Global ValidationPipe
// src/main.ts:28-30
new ValidationPipe({
whitelist: true, // ลบ field ที่ไม่มีใน DTO
forbidNonWhitelisted: true, // throw ถ้ามี field ที่ไม่มีใน DTO
})ผล: request ที่ส่ง field แปลกปลอม → 400 Bad Request
Auth — JWT guard ที่ bypassed ทุก route
Guard setup
app.useGlobalGuards(new JwtAuthGuard(reflector))Guard logic
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
context.getHandler(),
context.getClass(),
])
if (isPublic) {
return true // ข้าม JWT ทั้งหมด
}
return super.canActivate(context)
}@Public() decorator
export const isPublicKey = 'isPublic';
export const Public = () => SetMetadata(isPublicKey, true);การใช้ — ทุก route
| ไฟล์ | บรรทัด |
|---|---|
src/app.controller.ts | 26 |
src/modules/employee/employee.controller.ts | 38, 44, 50, 56, 62, 68, 74, 80, 86, 92, 98, 104, 110, 116 (ทุก method) |
src/modules/hrPaySlip/hrpayslip.controller.ts | 17 |
src/modules/hrTimeTemp/hrtimetemp.controller.ts | 17 |
ผล: ไม่มี endpoint ใดต้อง JWT — ทุกคนที่เข้าถึง centralize-api สามารถเรียกได้
JWT strategy (เผื่อเปิดใช้ในอนาคต)
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.get('jwtSecretKey'), // process.env.SECRET_KEY
})
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username }
}
}Services — 7 ตัว + 2 query patterns
| Service | ไฟล์ | Injection |
|---|---|---|
| AppService | src/app.service.ts | EntityManager('center') |
| EmployeeService | src/modules/employee/employee.service.ts | EntityManager('center'), TechnicalPositionRepository |
| HrPaySlipService | src/modules/hrPaySlip/hrpayslip.service.ts | EntityManager('center') |
| HrTimeTempService | src/modules/hrTimeTemp/hrtimetemp.service.ts | 2 EntityManagers: 'center' + 'nbc' |
| TechnicalPositionService | src/modules/technicalPosition/technicalPosition.service.ts | TechnicalPositionRepository |
| EmployeeLevelService | src/modules/employeeLevel/employeeLevel.service.ts | EmployeeLevelRepository |
| EmployeeGroupService | src/modules/employeeGroup/employeeGroup.service.ts | EmployeeGroupRepository |
Pattern A — Raw SQL (EntityManager.query)
ใช้ใน AppService, EmployeeService, HrPaySlipService, HrTimeTempService:
// employee.service.ts:181-184
return this.entityManager.query(fuckQuery, [
query.sourceDB,
query.orgCode,
])ส่วนใหญ่เป็น SQL ที่เขียนเอง (hand-written) มี cross-database join (dbHRMI_Center.dbo.X UNION ALL dbHRMI_Center_NBC.dbo.X) + WITH (NOLOCK)
Pattern B — TypeORM ActiveRecord
ใช้ใน 3 repositories:
// technicalPosition.repository.ts:20-21
const center = await TechnicalPosition.findBy(...)
const nbc = await TechnicalPositionNBC.findBy(...)
return center.concat(nbc)SourceDB tenancy — SQL-level UNION
centralize-api มี 2 TypeORM connections:
| Connection | DB | Env |
|---|---|---|
'center' | dbHRMI_Center | DB_NAME |
'nbc' | dbHRMI_Center_NBC | DB_NAME_NBC |
Sharding patterns
1. SQL-level UNION ALL (ส่วนใหญ่)
-- employee.service.ts:512-518 (ตัวอย่าง concept)
SELECT PosManageID, PosManageCode, PosManageName, SourceDB
FROM dbHRMI_Center.dbo.emPositionManage_reStructure
UNION ALL
SELECT PosManageID, PosManageCode, PosManageName, SourceDB
FROM dbHRMI_Center_NBC.dbo.emPositionManage_reStructure2. Application-level Promise.all
// hrtimetemp.service.ts:35-71
const [center, nbc] = await Promise.all([
this.centerManager.query(sql, params),
this.nbcManager.query(sql, params),
])
return center.concat(nbc)3. Repository-level parallel find
// technicalPosition.repository.ts:19-23
const [center, nbc] = await Promise.all([
TechnicalPosition.find({ where }),
TechnicalPositionNBC.find({ where }),
])
return center.concat(nbc)SourceDB value mapping
// app.service.ts:15-25 + employee.service.ts:37-42
| SourceDB literal | OrgCode |
|------------------------|---------|
| dbHRMI_CU_SC_rep | 02CS |
| dbHRMI_CU_H_rep | 02 |
| dbHRMI_RC_C_rep | 03 |
| dbHRMI_RC_B_rep | 00 |
| dbHRMI_RC_NBC_rep | 07 |Error model
Global exception filter
// src/main.ts:26
app.useGlobalFilters(new HttpExceptionFilter())// libs/exception/http-exception.filter.ts:21-25
response.status(status).json({
statusCode: status,
message: message['message'] || message['error'],
})Response shape
{
"statusCode": 400,
"message": "<string หรือ string[]>"
}ไม่มี error, data, timestamp, path — มี 2 field เท่านั้น
Status codes ที่ใช้
| Status | เมื่อไร |
|---|---|
| 400 | ทุก error ใน service (รวม DB error) — แปลงเป็น BadRequestException |
| 401 | JWT auth fail (แต่ไม่เกิดเพราะ @Public() ทุก route) |
| (validation) | ValidationPipe auto — 400 |
ความผิดปกติ: DB error ถูกแปลงเป็น 400 แทน 500 — อาจทำให้ debug สับสน
Response format — ไม่มี wrapper
- ไม่มี interceptor
- ไม่มี
{ data, status, message }envelope - Response คือสิ่งที่ service return — serialize เป็น JSON โดยตรง
ตัวอย่าง response:
[
{ "empCode": "12345", "fullName": "สมชาย", "sourceDB": "dbHRMI_RC_B_rep", ... },
...
]CORS
app.enableCors({
origin: '*',
allowedHeaders: '*',
methods: 'GET,PUT,POST,DELETE,PATCH,OPTIONS',
})origin: '*' — เปิดรับทุก origin (เสี่ยงถ้าเปิดสู่ internet)
Swagger — ข้อจำกัด
สิ่งที่ Swagger แสดง
- Auto-generated DTO schema (จาก class-validator ผ่าน nest CLI plugin)
@ApiTags(grouping)@ApiBearerAuthbadge (แต่ bypass จริง)
สิ่งที่ Swagger ไม่แสดง
- ไม่มี
@ApiOperation— ไม่มี description ของ endpoint - ไม่มี
@ApiResponse— ไม่มี response example - ไม่มี
@ApiBody,@ApiQuery,@ApiParam— ไม่มี param description
ผล: Swagger doc มีแค่ skeleton — ไม่ครบข้อมูล
ข้อระวังทั้งหมด
1. JWT guard ไม่ทำงานจริง
- ทุก route
@Public()→ ไม่ต้อง auth - ถ้าจะเปิด auth ต้องลบ
@Public()ออกจาก method นั้น
2. Query SQL มี cross-DB join
dbHRMI_Center.dbo.X UNION ALL dbHRMI_Center_NBC.dbo.X- ถ้าเปลี่ยนชื่อ DB — ต้องแก้ SQL ทุกที่
- มี
WITH (NOLOCK)ทั่วไป — ระวัง dirty read
3. CORS เปิดหมด
origin: '*'+allowedHeaders: '*'- ถ้าเปิดสู่ internet — อันตราย
4. DB error → 400
- ทุก service catch block แปลงเป็น
BadRequestException - ทำให้ client คิดว่าเป็น validation error แต่จริง ๆ อาจเป็น DB down
5. ไม่มี 404
- ถ้า query ไม่เจอ → return
[](empty array) - client ต้องเช็คเองว่า empty แปลว่าไม่มีข้อมูล
6. Dead DTO
src/dto/test.dto.tsเป็น NestJS boilerplate leftover — ไม่ได้ใช้
7. Custom validator ไม่ได้ใช้
libs/common/validator-date.ts:7-22(IsOnlyDate) — ไม่ได้ wire เข้า DTO ใด
เปรียบเทียบกับ cu-central-api
| ด้าน | centralize-api | cu-central-api |
|---|---|---|
| ภาษา | TypeScript | JavaScript |
| Framework | NestJS 9 | Apollo + Express |
| API type | REST | GraphQL + REST |
| Endpoints | 17 | 88 (83 GraphQL + 5 REST) |
| Auth | JWT guard (bypassed) | JWT + apiKey + LDAP |
| ORM | TypeORM | Sequelize |
| DB | MSSQL (2 conns) | MSSQL (5 conns) |
| Sharding | SQL UNION (2 DBs) | SourceDB column |
| Docs | Swagger /docs | ไม่มี |
| Validation | class-validator | GraphQL schema |
| Caller | ไม่ยืนยัน | vtrc-api |