251129:1700 update to 1.4.5

This commit is contained in:
admin
2025-11-29 16:50:34 +07:00
parent f7a43600a3
commit a78c9941be
55 changed files with 14641 additions and 2090 deletions

View File

@@ -15,14 +15,10 @@ import { VirtualColumnService } from './services/virtual-column.service';
import { CryptoService } from '../../common/services/crypto.service';
// Import Module อื่นๆ ที่จำเป็นสำหรับ Guard (ถ้า Guards อยู่ใน Common อาจจะไม่ต้อง import ที่นี่โดยตรง)
// import { UserModule } from '../user/user.module';
import { UserModule } from '../user/user.module';
@Module({
imports: [
TypeOrmModule.forFeature([JsonSchema]),
ConfigModule,
// UserModule,
],
imports: [TypeOrmModule.forFeature([JsonSchema]), ConfigModule, UserModule],
controllers: [JsonSchemaController],
providers: [
JsonSchemaService,

View File

@@ -1,172 +1,406 @@
// File: src/modules/json-schema/json-schema.controller.ts
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
ParseIntPipe,
Patch,
Post,
Query,
UseGuards,
} from '@nestjs/common';
import {
ApiBearerAuth,
ApiOperation,
ApiParam,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
// File: src/modules/json-schema/json-schema.service.ts
// บันทึกการแก้ไข: Fix TS2345 (undefined check)
import { JsonSchemaService } from './json-schema.service';
import { SchemaMigrationService } from './services/schema-migration.service';
import {
BadRequestException,
Injectable,
Logger,
NotFoundException,
OnModuleInit,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import Ajv, { ValidateFunction } from 'ajv';
import addFormats from 'ajv-formats';
import { Repository } from 'typeorm';
import { CreateJsonSchemaDto } from './dto/create-json-schema.dto';
import { MigrateDataDto } from './dto/migrate-data.dto';
import { SearchJsonSchemaDto } from './dto/search-json-schema.dto';
import { UpdateJsonSchemaDto } from './dto/update-json-schema.dto';
import { JsonSchema } from './entities/json-schema.entity';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { RbacGuard } from '../../common/guards/rbac.guard';
import { User } from '../user/entities/user.entity';
// Services ย่อยที่แยกตามหน้าที่ (Single Responsibility)
import {
JsonSecurityService,
SecurityContext,
} from './services/json-security.service';
import { UiSchemaService } from './services/ui-schema.service';
import { VirtualColumnService } from './services/virtual-column.service';
import {
ValidationErrorDetail,
ValidationOptions,
ValidationResult,
} from './interfaces/validation-result.interface';
@Injectable()
export class JsonSchemaService implements OnModuleInit {
private ajv: Ajv;
private validators = new Map<string, ValidateFunction>(); // Cache สำหรับเก็บ Validator ที่ Compile แล้ว
private readonly logger = new Logger(JsonSchemaService.name);
// ค่า Default สำหรับการตรวจสอบข้อมูล
private readonly defaultOptions: ValidationOptions = {
removeAdditional: true, // ลบฟิลด์เกิน
coerceTypes: true, // แปลงชนิดข้อมูลอัตโนมัติ (เช่น "123" -> 123)
useDefaults: true, // ใส่ค่า Default ถ้าไม่มีข้อมูล
};
@ApiTags('JSON Schemas Management')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RbacGuard)
@Controller('json-schemas')
export class JsonSchemaController {
constructor(
private readonly jsonSchemaService: JsonSchemaService,
private readonly migrationService: SchemaMigrationService,
) {}
// ----------------------------------------------------------------------
// Schema Management (CRUD)
// ----------------------------------------------------------------------
@Post()
@ApiOperation({
summary: 'Create a new schema or new version of existing schema',
})
@ApiResponse({
status: 201,
description: 'The schema has been successfully created.',
})
@RequirePermission('system.manage_all') // Admin Only
create(@Body() createDto: CreateJsonSchemaDto) {
return this.jsonSchemaService.create(createDto);
}
@Get()
@ApiOperation({ summary: 'List all schemas with pagination and filtering' })
@RequirePermission('document.view') // Viewer+ can see schemas
findAll(@Query() searchDto: SearchJsonSchemaDto) {
return this.jsonSchemaService.findAll(searchDto);
}
@Get(':id')
@ApiOperation({ summary: 'Get a specific schema version by ID' })
@RequirePermission('document.view')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.jsonSchemaService.findOne(id);
}
@Get('latest/:code')
@ApiOperation({
summary: 'Get the latest active version of a schema by code',
})
@ApiParam({ name: 'code', description: 'Schema Code (e.g., RFA_DWG)' })
@RequirePermission('document.view')
findLatest(@Param('code') code: string) {
return this.jsonSchemaService.findLatestByCode(code);
}
@Patch(':id')
@ApiOperation({
summary: 'Update a specific schema (Not recommended for active schemas)',
})
@RequirePermission('system.manage_all')
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateDto: UpdateJsonSchemaDto,
@InjectRepository(JsonSchema)
private readonly jsonSchemaRepository: Repository<JsonSchema>,
private readonly virtualColumnService: VirtualColumnService,
private readonly uiSchemaService: UiSchemaService,
private readonly jsonSecurityService: JsonSecurityService,
) {
return this.jsonSchemaService.update(id, updateDto);
// กำหนดค่าเริ่มต้นให้กับ AJV Validation Engine
this.ajv = new Ajv({
allErrors: true, // แสดง Error ทั้งหมด ไม่หยุดแค่จุดแรก
strict: false, // ไม่เคร่งครัดเกินไป (ยอมรับ Keyword แปลกๆ เช่น ui:widget)
coerceTypes: true,
useDefaults: true,
removeAdditional: true,
});
addFormats(this.ajv); // เพิ่ม Format มาตรฐาน (email, date, uri ฯลฯ)
this.registerCustomValidators(); // ลงทะเบียน Validator เฉพาะของโปรเจกต์
}
@Delete(':id')
@ApiOperation({ summary: 'Delete a schema version (Hard Delete)' })
@RequirePermission('system.manage_all')
remove(@Param('id', ParseIntPipe) id: number) {
return this.jsonSchemaService.remove(id);
async onModuleInit() {
// สามารถโหลด Schema ที่ Active ทั้งหมดมา Cache ไว้ล่วงหน้าได้ที่นี่ เพื่อความเร็วในการตอบสนองครั้งแรก
}
// ----------------------------------------------------------------------
// Validation & Security
// ----------------------------------------------------------------------
/**
* ลงทะเบียน Custom Validators เฉพาะสำหรับ LCBP3
*/
private registerCustomValidators() {
// 1. ตรวจสอบรูปแบบเลขที่เอกสาร (เช่น TEAM-RFA-STR-0001)
this.ajv.addFormat('document-number', {
type: 'string',
validate: (value: string) => {
// Regex อย่างง่าย: กลุ่มตัวอักษรขีดคั่นด้วย -
return /^[A-Z0-9]{2,10}-[A-Z]{2,5}(-[A-Z0-9]{2,5})?-\d{4}-\d{3,5}$/.test(
value,
);
},
});
@Post('validate/:code')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Validate data against the latest schema version' })
@ApiResponse({
status: 200,
description: 'Validation result including errors and sanitized data',
})
@RequirePermission('document.view')
async validate(@Param('code') code: string, @Body() data: any) {
// Note: Validation API นี้ใช้สำหรับ Test หรือ Pre-check เท่านั้น
// การ Save จริงจะเรียกผ่าน Service ภายใน
return this.jsonSchemaService.validateData(code, data);
// 2. Keyword สำหรับระบุ Role ที่จำเป็น (ใช้ร่วมกับ Security Service)
this.ajv.addKeyword({
keyword: 'requiredRole',
type: 'string',
metaSchema: { type: 'string' },
validate: (schema: string, data: any) => true, // ผ่านเสมอในขั้น AJV (Security Service จะจัดการเอง)
});
}
@Post('read/:code')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Process read data (Decrypt & Filter) based on user roles',
})
@RequirePermission('document.view')
/**
* สร้าง Schema ใหม่ พร้อมจัดการ Version, UI Schema และ Virtual Columns
*/
async create(createDto: CreateJsonSchemaDto): Promise<JsonSchema> {
// 1. ตรวจสอบความถูกต้องของ JSON Schema Definition (AJV Syntax)
try {
this.ajv.compile(createDto.schemaDefinition);
} catch (error: any) {
throw new BadRequestException(
`Invalid JSON Schema format: ${error.message}`,
);
}
// 2. จัดการ UI Schema
if (createDto.uiSchema) {
// ถ้าส่งมา ให้ตรวจสอบความถูกต้องเทียบกับ Data Schema
this.uiSchemaService.validateUiSchema(
createDto.uiSchema as any,
createDto.schemaDefinition,
);
} else {
// ถ้าไม่ส่งมา ให้สร้าง UI Schema พื้นฐานให้อัตโนมัติ
createDto.uiSchema = this.uiSchemaService.generateDefaultUiSchema(
createDto.schemaDefinition,
);
}
// 3. จัดการ Versioning อัตโนมัติ (Auto-increment)
const latestSchema = await this.jsonSchemaRepository.findOne({
where: { schemaCode: createDto.schemaCode },
order: { version: 'DESC' },
});
let newVersion = 1;
if (latestSchema) {
// ถ้าผู้ใช้ไม่ระบุ Version หรือระบุมาน้อยกว่าล่าสุด ให้ +1
if (!createDto.version || createDto.version <= latestSchema.version) {
newVersion = latestSchema.version + 1;
} else {
newVersion = createDto.version;
}
} else if (createDto.version) {
newVersion = createDto.version;
}
// 4. บันทึกลงฐานข้อมูล
const newSchema = this.jsonSchemaRepository.create({
...createDto,
version: newVersion,
});
const savedSchema = await this.jsonSchemaRepository.save(newSchema);
// ล้าง Cache เพื่อให้โหลดตัวใหม่ในครั้งถัดไป
this.validators.delete(savedSchema.schemaCode);
this.logger.log(
`Schema '${savedSchema.schemaCode}' created (v${savedSchema.version})`,
);
// 5. สร้าง/อัปเดต Virtual Columns บน Database จริง (Performance Optimization)
// Fix TS2345: Add empty array fallback
if (savedSchema.virtualColumns && savedSchema.virtualColumns.length > 0) {
await this.virtualColumnService.setupVirtualColumns(
savedSchema.tableName,
savedSchema.virtualColumns || [],
);
}
return savedSchema;
}
/**
* ค้นหา Schema ทั้งหมด (Pagination & Filter)
*/
async findAll(searchDto: SearchJsonSchemaDto) {
const { search, isActive, page = 1, limit = 20 } = searchDto;
const skip = (page - 1) * limit;
const query = this.jsonSchemaRepository.createQueryBuilder('schema');
if (search) {
query.andWhere('schema.schemaCode LIKE :search', {
search: `%${search}%`,
});
}
if (isActive !== undefined) {
query.andWhere('schema.isActive = :isActive', { isActive });
}
// เรียงตาม Code ก่อน แล้วตามด้วย Version ล่าสุด
query.orderBy('schema.schemaCode', 'ASC');
query.addOrderBy('schema.version', 'DESC');
const [items, total] = await query.skip(skip).take(limit).getManyAndCount();
return {
data: items,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
/**
* ดึงข้อมูล Schema ตาม ID
*/
async findOne(id: number): Promise<JsonSchema> {
const schema = await this.jsonSchemaRepository.findOne({ where: { id } });
if (!schema) {
throw new NotFoundException(`JsonSchema with ID ${id} not found`);
}
return schema;
}
/**
* ดึงข้อมูล Schema ตาม Code และ Version (สำหรับ Migration)
*/
async findOneByCodeAndVersion(
code: string,
version: number,
): Promise<JsonSchema> {
const schema = await this.jsonSchemaRepository.findOne({
where: { schemaCode: code, version },
});
if (!schema) {
throw new NotFoundException(
`JsonSchema '${code}' version ${version} not found`,
);
}
return schema;
}
/**
* ดึง Schema เวอร์ชันล่าสุดที่ Active (สำหรับใช้งานทั่วไป)
*/
async findLatestByCode(code: string): Promise<JsonSchema> {
const schema = await this.jsonSchemaRepository.findOne({
where: { schemaCode: code, isActive: true },
order: { version: 'DESC' },
});
if (!schema) {
throw new NotFoundException(
`Active JsonSchema with code '${code}' not found`,
);
}
return schema;
}
/**
* [CORE FUNCTION] ตรวจสอบข้อมูล (Validate), ทำความสะอาด (Sanitize) และเข้ารหัส (Encrypt)
* ใช้สำหรับ "ขาเข้า" (Write) ก่อนบันทึกลง Database
*/
async validateData(
schemaCode: string,
data: any,
options: ValidationOptions = {},
): Promise<ValidationResult> {
// 1. ดึงและ Compile Validator
const validate = await this.getValidator(schemaCode);
const schema = await this.findLatestByCode(schemaCode); // ดึง Full Schema เพื่อใช้ Config อื่นๆ
// 2. สำเนาข้อมูลเพื่อป้องกัน Side Effect และเตรียมสำหรับ AJV Mutation (Sanitization)
const dataToValidate = JSON.parse(JSON.stringify(data));
// 3. เริ่มการตรวจสอบ (AJV จะทำการ Coerce Type และ Remove Additional Properties ให้ด้วย)
const valid = validate(dataToValidate);
// 4. จัดการกรณีข้อมูลไม่ถูกต้อง
if (!valid) {
const errors: ValidationErrorDetail[] = (validate.errors || []).map(
(err) => ({
field: err.instancePath || 'root',
message: err.message || 'Validation error',
value: err.params,
}),
);
return {
isValid: false,
errors,
sanitizedData: null,
};
}
// 5. เข้ารหัสข้อมูล (Encryption) สำหรับ Field ที่มีความลับ (x-encrypt: true)
const secureData = this.jsonSecurityService.encryptFields(
dataToValidate,
schema.schemaDefinition,
);
return {
isValid: true,
errors: [],
sanitizedData: secureData, // ข้อมูลนี้สะอาดและปลอดภัย พร้อมบันทึก
};
}
/**
* [CORE FUNCTION] อ่านข้อมูล, ถอดรหัส (Decrypt) และกรองตามสิทธิ์ (Filter)
* ใช้สำหรับ "ขาออก" (Read) ก่อนส่งให้ Frontend
*/
async processReadData(
@Param('code') code: string,
@Body() data: any,
@CurrentUser() user: User,
) {
// แปลง User Entity เป็น Security Context
// แก้ไข TS2339 & TS7006: Type Casting เพื่อให้เข้าถึง roles ได้โดยไม่ error
// เนื่องจาก User Entity ปกติไม่มี property roles (แต่อาจถูก Inject มาตอน Runtime หรือผ่าน Assignments)
const userWithRoles = user as any;
const userRoles = userWithRoles.roles
? userWithRoles.roles.map((r: any) => r.roleName)
: [];
schemaCode: string,
data: any,
userContext: SecurityContext,
): Promise<any> {
if (!data) return data;
return this.jsonSchemaService.processReadData(code, data, { userRoles });
}
// ดึง Schema เพื่อดู Config การถอดรหัสและการมองเห็น
const schema = await this.findLatestByCode(schemaCode);
// ----------------------------------------------------------------------
// Data Migration
// ----------------------------------------------------------------------
@Post('migrate/:table/:id')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Migrate specific entity data to target schema version',
})
@ApiParam({ name: 'table', description: 'Table Name (e.g. rfa_revisions)' })
@ApiParam({ name: 'id', description: 'Entity ID' })
@RequirePermission('system.manage_all') // Dangerous Op -> Admin Only
async migrateData(
@Param('table') tableName: string,
@Param('id', ParseIntPipe) id: number,
@Body() dto: MigrateDataDto,
) {
return this.migrationService.migrateData(
tableName,
id,
dto.targetSchemaCode,
dto.targetVersion,
return this.jsonSecurityService.decryptAndFilterFields(
data,
schema.schemaDefinition,
userContext,
);
}
/**
* Helper: ดึงและ Cache AJV Validator Function เพื่อประสิทธิภาพ
*/
private async getValidator(schemaCode: string): Promise<ValidateFunction> {
let validate = this.validators.get(schemaCode);
if (!validate) {
const schema = await this.findLatestByCode(schemaCode);
try {
validate = this.ajv.compile(schema.schemaDefinition);
this.validators.set(schemaCode, validate);
} catch (error: any) {
throw new BadRequestException(
`Invalid Schema Definition for '${schemaCode}': ${error.message}`,
);
}
}
return validate;
}
/**
* Wrapper เก่าสำหรับ Backward Compatibility (ถ้ามีโค้ดเก่าเรียกใช้)
*/
async validate(schemaCode: string, data: any): Promise<boolean> {
const result = await this.validateData(schemaCode, data);
if (!result.isValid) {
const errorMsg = result.errors
.map((e) => `${e.field}: ${e.message}`)
.join(', ');
throw new BadRequestException(`JSON Validation Failed: ${errorMsg}`);
}
return true;
}
/**
* อัปเดตข้อมูล Schema และจัดการผลกระทบ (Virtual Columns / UI Schema)
*/
async update(
id: number,
updateDto: UpdateJsonSchemaDto,
): Promise<JsonSchema> {
const schema = await this.findOne(id);
// ตรวจสอบ JSON Schema
if (updateDto.schemaDefinition) {
try {
this.ajv.compile(updateDto.schemaDefinition);
} catch (error: any) {
throw new BadRequestException(
`Invalid JSON Schema format: ${error.message}`,
);
}
this.validators.delete(schema.schemaCode); // เคลียร์ Cache เก่า
}
// ตรวจสอบ UI Schema
if (updateDto.uiSchema) {
this.uiSchemaService.validateUiSchema(
updateDto.uiSchema as any,
updateDto.schemaDefinition || schema.schemaDefinition,
);
}
const updatedSchema = this.jsonSchemaRepository.merge(schema, updateDto);
const savedSchema = await this.jsonSchemaRepository.save(updatedSchema);
// อัปเดต Virtual Columns ใน Database ถ้ามีการเปลี่ยนแปลง Config
// Fix TS2345: Add empty array fallback
if (updateDto.virtualColumns && updatedSchema.virtualColumns) {
await this.virtualColumnService.setupVirtualColumns(
savedSchema.tableName,
savedSchema.virtualColumns || [],
);
}
return savedSchema;
}
/**
* ลบ Schema (Hard Delete)
*/
async remove(id: number): Promise<void> {
const schema = await this.findOne(id);
this.validators.delete(schema.schemaCode);
await this.jsonSchemaRepository.remove(schema);
}
}