This commit is contained in:
@@ -32,16 +32,16 @@ export class JsonSchema {
|
||||
tableName!: string;
|
||||
|
||||
@Column({ name: 'schema_definition', type: 'json' })
|
||||
schemaDefinition!: any;
|
||||
schemaDefinition!: Record<string, unknown>;
|
||||
|
||||
@Column({ name: 'ui_schema', type: 'json', nullable: true })
|
||||
uiSchema?: any;
|
||||
uiSchema?: Record<string, unknown>;
|
||||
|
||||
@Column({ name: 'virtual_columns', type: 'json', nullable: true })
|
||||
virtualColumns?: VirtualColumnConfig[];
|
||||
|
||||
@Column({ name: 'migration_script', type: 'json', nullable: true })
|
||||
migrationScript?: any;
|
||||
migrationScript?: Record<string, unknown>;
|
||||
|
||||
@Column({ name: 'is_active', default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@@ -23,7 +23,7 @@ export type Operator =
|
||||
export interface FieldCondition {
|
||||
field: string;
|
||||
operator: Operator;
|
||||
value: any;
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
export interface FieldDependency {
|
||||
@@ -32,7 +32,7 @@ export interface FieldDependency {
|
||||
visibility?: boolean; // true = show, false = hide
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
filterOptions?: Record<string, any>; // เช่น กรอง Dropdown ตามค่าที่เลือก
|
||||
filterOptions?: Record<string, unknown>; // เช่น กรอง Dropdown ตามค่าที่เลือก
|
||||
};
|
||||
}
|
||||
|
||||
@@ -42,10 +42,10 @@ export interface UiSchemaField {
|
||||
title: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
enum?: any[]; // กรณีเป็น static options
|
||||
enum?: unknown[]; // กรณีเป็น static options
|
||||
enumNames?: string[]; // label สำหรับ options
|
||||
dataSource?: string; // กรณีดึง options จาก API (เช่น 'master-data/disciplines')
|
||||
defaultValue?: any;
|
||||
defaultValue?: unknown;
|
||||
readOnly?: boolean;
|
||||
hidden?: boolean;
|
||||
|
||||
@@ -72,7 +72,7 @@ export interface LayoutGroup {
|
||||
export interface LayoutConfig {
|
||||
type: 'stack' | 'grid' | 'tabs' | 'steps' | 'wizard';
|
||||
groups: LayoutGroup[];
|
||||
options?: Record<string, any>; // Config เพิ่มเติมเฉพาะ Layout type
|
||||
options?: Record<string, unknown>; // Config เพิ่มเติมเฉพาะ Layout type
|
||||
}
|
||||
|
||||
export interface UiSchema {
|
||||
@@ -81,4 +81,3 @@ export interface UiSchema {
|
||||
[key: string]: UiSchemaField;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -23,12 +23,11 @@ export interface ValidationOptions {
|
||||
export interface ValidationErrorDetail {
|
||||
field: string;
|
||||
message: string;
|
||||
value?: any;
|
||||
value?: unknown;
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
isValid: boolean;
|
||||
errors: ValidationErrorDetail[];
|
||||
sanitizedData: any;
|
||||
sanitizedData: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ import { User } from '../user/entities/user.entity';
|
||||
export class JsonSchemaController {
|
||||
constructor(
|
||||
private readonly jsonSchemaService: JsonSchemaService,
|
||||
private readonly migrationService: SchemaMigrationService,
|
||||
private readonly migrationService: SchemaMigrationService
|
||||
) {}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
@@ -93,7 +93,7 @@ export class JsonSchemaController {
|
||||
@RequirePermission('system.manage_all')
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() updateDto: UpdateJsonSchemaDto,
|
||||
@Body() updateDto: UpdateJsonSchemaDto
|
||||
) {
|
||||
return this.jsonSchemaService.update(id, updateDto);
|
||||
}
|
||||
@@ -117,7 +117,10 @@ export class JsonSchemaController {
|
||||
description: 'Validation result including errors and sanitized data',
|
||||
})
|
||||
@RequirePermission('document.view')
|
||||
async validate(@Param('code') code: string, @Body() data: any) {
|
||||
async validate(
|
||||
@Param('code') code: string,
|
||||
@Body() data: Record<string, unknown>
|
||||
) {
|
||||
// Note: Validation API นี้ใช้สำหรับ Test หรือ Pre-check เท่านั้น
|
||||
// การ Save จริงจะเรียกผ่าน Service ภายใน
|
||||
return this.jsonSchemaService.validateData(code, data);
|
||||
@@ -131,15 +134,16 @@ export class JsonSchemaController {
|
||||
@RequirePermission('document.view')
|
||||
async processReadData(
|
||||
@Param('code') code: string,
|
||||
@Body() data: any,
|
||||
@CurrentUser() user: User,
|
||||
@Body() data: Record<string, unknown>,
|
||||
@CurrentUser() user: User
|
||||
) {
|
||||
// แปลง User Entity เป็น Security Context
|
||||
// ใช้ as any เพื่อ bypass type checking ชั่วคราว เนื่องจาก roles มักจะถูก inject เข้ามาใน request.user
|
||||
// โดย Strategy หรือ Guard แม้จะไม่มีใน Entity หลัก
|
||||
const userWithRoles = user as any;
|
||||
// roles มักจะถูก inject เข้ามาใน request.user โดย Strategy หรือ Guard แม้จะไม่มีใน Entity หลัก
|
||||
const userWithRoles = user as User & {
|
||||
roles?: Array<{ roleName: string } | string>;
|
||||
};
|
||||
const userRoles = userWithRoles.roles
|
||||
? userWithRoles.roles.map((r: any) => r.roleName || r) // รองรับทั้ง Object Role และ String Role
|
||||
? userWithRoles.roles.map((r) => (typeof r === 'string' ? r : r.roleName)) // รองรับทั้ง Object Role และ String Role
|
||||
: [];
|
||||
|
||||
return this.jsonSchemaService.processReadData(code, data, { userRoles });
|
||||
@@ -160,13 +164,13 @@ export class JsonSchemaController {
|
||||
async migrateData(
|
||||
@Param('table') tableName: string,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() dto: MigrateDataDto,
|
||||
@Body() dto: MigrateDataDto
|
||||
) {
|
||||
return this.migrationService.migrateData(
|
||||
tableName,
|
||||
id,
|
||||
dto.targetSchemaCode,
|
||||
dto.targetVersion,
|
||||
dto.targetVersion
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
SecurityContext,
|
||||
} from './services/json-security.service';
|
||||
import { UiSchemaService } from './services/ui-schema.service';
|
||||
import { UiSchema } from './interfaces/ui-schema.interface';
|
||||
import { VirtualColumnService } from './services/virtual-column.service';
|
||||
|
||||
import {
|
||||
@@ -50,7 +51,7 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
private readonly jsonSchemaRepository: Repository<JsonSchema>,
|
||||
private readonly virtualColumnService: VirtualColumnService,
|
||||
private readonly uiSchemaService: UiSchemaService,
|
||||
private readonly jsonSecurityService: JsonSecurityService,
|
||||
private readonly jsonSecurityService: JsonSecurityService
|
||||
) {
|
||||
// กำหนดค่าเริ่มต้นให้กับ AJV Validation Engine
|
||||
this.ajv = new Ajv({
|
||||
@@ -78,7 +79,7 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
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,
|
||||
value
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -88,7 +89,7 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
keyword: 'requiredRole',
|
||||
type: 'string',
|
||||
metaSchema: { type: 'string' },
|
||||
validate: (schema: string, data: any) => true, // ผ่านเสมอในขั้น AJV (Security Service จะจัดการเอง)
|
||||
validate: (_schema: string, _data: unknown) => true, // ผ่านเสมอในขั้น AJV (Security Service จะจัดการเอง)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -99,9 +100,9 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
// 1. ตรวจสอบความถูกต้องของ JSON Schema Definition (AJV Syntax)
|
||||
try {
|
||||
this.ajv.compile(createDto.schemaDefinition);
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
throw new BadRequestException(
|
||||
`Invalid JSON Schema format: ${error.message}`,
|
||||
`Invalid JSON Schema format: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -109,13 +110,13 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
if (createDto.uiSchema) {
|
||||
// ถ้าส่งมา ให้ตรวจสอบความถูกต้องเทียบกับ Data Schema
|
||||
this.uiSchemaService.validateUiSchema(
|
||||
createDto.uiSchema as any,
|
||||
createDto.schemaDefinition,
|
||||
createDto.uiSchema as unknown as UiSchema,
|
||||
createDto.schemaDefinition
|
||||
);
|
||||
} else {
|
||||
// ถ้าไม่ส่งมา ให้สร้าง UI Schema พื้นฐานให้อัตโนมัติ
|
||||
createDto.uiSchema = this.uiSchemaService.generateDefaultUiSchema(
|
||||
createDto.schemaDefinition,
|
||||
createDto.schemaDefinition
|
||||
);
|
||||
}
|
||||
|
||||
@@ -149,7 +150,7 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
this.validators.delete(savedSchema.schemaCode);
|
||||
|
||||
this.logger.log(
|
||||
`Schema '${savedSchema.schemaCode}' created (v${savedSchema.version})`,
|
||||
`Schema '${savedSchema.schemaCode}' created (v${savedSchema.version})`
|
||||
);
|
||||
|
||||
// 5. สร้าง/อัปเดต Virtual Columns บน Database จริง (Performance Optimization)
|
||||
@@ -157,7 +158,7 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
if (savedSchema.virtualColumns && savedSchema.virtualColumns.length > 0) {
|
||||
await this.virtualColumnService.setupVirtualColumns(
|
||||
savedSchema.tableName,
|
||||
savedSchema.virtualColumns || [],
|
||||
savedSchema.virtualColumns || []
|
||||
);
|
||||
}
|
||||
|
||||
@@ -216,7 +217,7 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
*/
|
||||
async findOneByCodeAndVersion(
|
||||
code: string,
|
||||
version: number,
|
||||
version: number
|
||||
): Promise<JsonSchema> {
|
||||
const schema = await this.jsonSchemaRepository.findOne({
|
||||
where: { schemaCode: code, version },
|
||||
@@ -224,7 +225,7 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
|
||||
if (!schema) {
|
||||
throw new NotFoundException(
|
||||
`JsonSchema '${code}' version ${version} not found`,
|
||||
`JsonSchema '${code}' version ${version} not found`
|
||||
);
|
||||
}
|
||||
return schema;
|
||||
@@ -241,7 +242,7 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
|
||||
if (!schema) {
|
||||
throw new NotFoundException(
|
||||
`Active JsonSchema with code '${code}' not found`,
|
||||
`Active JsonSchema with code '${code}' not found`
|
||||
);
|
||||
}
|
||||
return schema;
|
||||
@@ -253,15 +254,17 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
*/
|
||||
async validateData(
|
||||
schemaCode: string,
|
||||
data: any,
|
||||
options: ValidationOptions = {},
|
||||
data: Record<string, unknown>,
|
||||
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));
|
||||
const dataToValidate: Record<string, unknown> = JSON.parse(
|
||||
JSON.stringify(data)
|
||||
);
|
||||
|
||||
// 3. เริ่มการตรวจสอบ (AJV จะทำการ Coerce Type และ Remove Additional Properties ให้ด้วย)
|
||||
const valid = validate(dataToValidate);
|
||||
@@ -273,7 +276,7 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
field: err.instancePath || 'root',
|
||||
message: err.message || 'Validation error',
|
||||
value: err.params,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -286,7 +289,7 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
// 5. เข้ารหัสข้อมูล (Encryption) สำหรับ Field ที่มีความลับ (x-encrypt: true)
|
||||
const secureData = this.jsonSecurityService.encryptFields(
|
||||
dataToValidate,
|
||||
schema.schemaDefinition,
|
||||
schema.schemaDefinition
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -302,9 +305,9 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
*/
|
||||
async processReadData(
|
||||
schemaCode: string,
|
||||
data: any,
|
||||
userContext: SecurityContext,
|
||||
): Promise<any> {
|
||||
data: Record<string, unknown>,
|
||||
userContext: SecurityContext
|
||||
): Promise<Record<string, unknown>> {
|
||||
if (!data) return data;
|
||||
|
||||
// ดึง Schema เพื่อดู Config การถอดรหัสและการมองเห็น
|
||||
@@ -313,7 +316,7 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
return this.jsonSecurityService.decryptAndFilterFields(
|
||||
data,
|
||||
schema.schemaDefinition,
|
||||
userContext,
|
||||
userContext
|
||||
);
|
||||
}
|
||||
|
||||
@@ -328,9 +331,9 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
try {
|
||||
validate = this.ajv.compile(schema.schemaDefinition);
|
||||
this.validators.set(schemaCode, validate);
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
throw new BadRequestException(
|
||||
`Invalid Schema Definition for '${schemaCode}': ${error.message}`,
|
||||
`Invalid Schema Definition for '${schemaCode}': ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -340,7 +343,10 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
/**
|
||||
* Wrapper เก่าสำหรับ Backward Compatibility (ถ้ามีโค้ดเก่าเรียกใช้)
|
||||
*/
|
||||
async validate(schemaCode: string, data: any): Promise<boolean> {
|
||||
async validate(
|
||||
schemaCode: string,
|
||||
data: Record<string, unknown>
|
||||
): Promise<boolean> {
|
||||
const result = await this.validateData(schemaCode, data);
|
||||
if (!result.isValid) {
|
||||
const errorMsg = result.errors
|
||||
@@ -356,7 +362,7 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
*/
|
||||
async update(
|
||||
id: number,
|
||||
updateDto: UpdateJsonSchemaDto,
|
||||
updateDto: UpdateJsonSchemaDto
|
||||
): Promise<JsonSchema> {
|
||||
const schema = await this.findOne(id);
|
||||
|
||||
@@ -364,9 +370,9 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
if (updateDto.schemaDefinition) {
|
||||
try {
|
||||
this.ajv.compile(updateDto.schemaDefinition);
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
throw new BadRequestException(
|
||||
`Invalid JSON Schema format: ${error.message}`,
|
||||
`Invalid JSON Schema format: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
this.validators.delete(schema.schemaCode); // เคลียร์ Cache เก่า
|
||||
@@ -375,8 +381,8 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
// ตรวจสอบ UI Schema
|
||||
if (updateDto.uiSchema) {
|
||||
this.uiSchemaService.validateUiSchema(
|
||||
updateDto.uiSchema as any,
|
||||
updateDto.schemaDefinition || schema.schemaDefinition,
|
||||
updateDto.uiSchema as unknown as UiSchema,
|
||||
updateDto.schemaDefinition || schema.schemaDefinition
|
||||
);
|
||||
}
|
||||
|
||||
@@ -388,7 +394,7 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
if (updateDto.virtualColumns && updatedSchema.virtualColumns) {
|
||||
await this.virtualColumnService.setupVirtualColumns(
|
||||
savedSchema.tableName,
|
||||
savedSchema.virtualColumns || [],
|
||||
savedSchema.virtualColumns || []
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,28 +13,41 @@ export class JsonSecurityService {
|
||||
/**
|
||||
* ขาเข้า (Write): เข้ารหัสข้อมูล Sensitive ก่อนบันทึก
|
||||
*/
|
||||
encryptFields(data: any, schemaDefinition: any): any {
|
||||
encryptFields(
|
||||
data: Record<string, unknown>,
|
||||
schemaDefinition: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
if (!data || typeof data !== 'object') return data;
|
||||
const processed = Array.isArray(data) ? [...data] : { ...data };
|
||||
const processed: Record<string, unknown> = { ...data };
|
||||
|
||||
// Traverse schema properties
|
||||
if (schemaDefinition.properties) {
|
||||
for (const [key, propSchema] of Object.entries<any>(
|
||||
schemaDefinition.properties,
|
||||
)) {
|
||||
const properties = schemaDefinition.properties as
|
||||
| Record<string, Record<string, unknown>>
|
||||
| undefined;
|
||||
if (properties) {
|
||||
for (const [key, propSchema] of Object.entries(properties)) {
|
||||
if (data[key] !== undefined) {
|
||||
// 1. Check encryption flag
|
||||
if (propSchema['x-encrypt'] === true) {
|
||||
processed[key] = this.cryptoService.encrypt(data[key]);
|
||||
processed[key] = this.cryptoService.encrypt(
|
||||
data[key] as string | number | boolean
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Recursive for nested objects/arrays
|
||||
if (propSchema.type === 'object' && propSchema.properties) {
|
||||
processed[key] = this.encryptFields(data[key], propSchema);
|
||||
processed[key] = this.encryptFields(
|
||||
data[key] as Record<string, unknown>,
|
||||
propSchema
|
||||
);
|
||||
} else if (propSchema.type === 'array' && propSchema.items) {
|
||||
if (Array.isArray(data[key])) {
|
||||
processed[key] = data[key].map((item: any) =>
|
||||
this.encryptFields(item, propSchema.items),
|
||||
processed[key] = (data[key] as Record<string, unknown>[]).map(
|
||||
(item) =>
|
||||
this.encryptFields(
|
||||
item,
|
||||
propSchema.items as Record<string, unknown>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -48,33 +61,34 @@ export class JsonSecurityService {
|
||||
* ขาออก (Read): ถอดรหัส และ กรองข้อมูลตามสิทธิ์
|
||||
*/
|
||||
decryptAndFilterFields(
|
||||
data: any,
|
||||
schemaDefinition: any,
|
||||
context: SecurityContext,
|
||||
): any {
|
||||
data: Record<string, unknown>,
|
||||
schemaDefinition: Record<string, unknown>,
|
||||
context: SecurityContext
|
||||
): Record<string, unknown> {
|
||||
if (!data || typeof data !== 'object') return data;
|
||||
|
||||
// Clone data to avoid mutation
|
||||
const processed = Array.isArray(data) ? [...data] : { ...data };
|
||||
const processed: Record<string, unknown> = { ...data };
|
||||
|
||||
if (schemaDefinition.properties) {
|
||||
for (const [key, propSchema] of Object.entries<any>(
|
||||
schemaDefinition.properties,
|
||||
)) {
|
||||
const properties = schemaDefinition.properties as
|
||||
| Record<string, Record<string, unknown>>
|
||||
| undefined;
|
||||
if (properties) {
|
||||
for (const [key, propSchema] of Object.entries(properties)) {
|
||||
if (data[key] !== undefined) {
|
||||
// 1. Decrypt (ถ้ามีค่าและถูกเข้ารหัสไว้)
|
||||
if (propSchema['x-encrypt'] === true) {
|
||||
processed[key] = this.cryptoService.decrypt(data[key]);
|
||||
processed[key] = this.cryptoService.decrypt(data[key] as string);
|
||||
}
|
||||
|
||||
// 2. Security Check (Role-based Access Control)
|
||||
if (propSchema['x-security']) {
|
||||
const rule = propSchema['x-security'];
|
||||
const requiredRoles = rule.roles || [];
|
||||
const rule = propSchema['x-security'] as Record<string, unknown>;
|
||||
const requiredRoles = (rule.roles as string[]) || [];
|
||||
const hasPermission = requiredRoles.some(
|
||||
(role: string) =>
|
||||
context.userRoles.includes(role) ||
|
||||
context.userRoles.includes('SUPERADMIN'),
|
||||
context.userRoles.includes('SUPERADMIN')
|
||||
);
|
||||
|
||||
if (!hasPermission) {
|
||||
@@ -93,14 +107,20 @@ export class JsonSecurityService {
|
||||
if (processed[key] !== undefined) {
|
||||
if (propSchema.type === 'object' && propSchema.properties) {
|
||||
processed[key] = this.decryptAndFilterFields(
|
||||
processed[key],
|
||||
processed[key] as Record<string, unknown>,
|
||||
propSchema,
|
||||
context,
|
||||
context
|
||||
);
|
||||
} else if (propSchema.type === 'array' && propSchema.items) {
|
||||
if (Array.isArray(processed[key])) {
|
||||
processed[key] = processed[key].map((item: any) =>
|
||||
this.decryptAndFilterFields(item, propSchema.items, context),
|
||||
processed[key] = (
|
||||
processed[key] as Record<string, unknown>[]
|
||||
).map((item) =>
|
||||
this.decryptAndFilterFields(
|
||||
item,
|
||||
propSchema.items as Record<string, unknown>,
|
||||
context
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface MigrationStep {
|
||||
| 'FIELD_ADD'
|
||||
| 'FIELD_REMOVE'
|
||||
| 'STRUCTURE_CHANGE';
|
||||
config: any;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface MigrationResult {
|
||||
@@ -27,7 +27,7 @@ export class SchemaMigrationService {
|
||||
|
||||
constructor(
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly jsonSchemaService: JsonSchemaService,
|
||||
private readonly jsonSchemaService: JsonSchemaService
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -37,7 +37,7 @@ export class SchemaMigrationService {
|
||||
entityType: string, // e.g., 'rfa_revisions', 'correspondence_revisions'
|
||||
entityId: number,
|
||||
targetSchemaCode: string,
|
||||
targetVersion?: number,
|
||||
targetVersion?: number
|
||||
): Promise<MigrationResult> {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
@@ -49,7 +49,7 @@ export class SchemaMigrationService {
|
||||
if (targetVersion) {
|
||||
targetSchema = await this.jsonSchemaService.findOneByCodeAndVersion(
|
||||
targetSchemaCode,
|
||||
targetVersion,
|
||||
targetVersion
|
||||
);
|
||||
} else {
|
||||
targetSchema =
|
||||
@@ -61,12 +61,12 @@ export class SchemaMigrationService {
|
||||
// If schema_version is not present, we assume version 1
|
||||
const entity = await queryRunner.manager.query(
|
||||
`SELECT details, schema_version FROM ${entityType} WHERE id = ?`,
|
||||
[entityId],
|
||||
[entityId]
|
||||
);
|
||||
|
||||
if (!entity || entity.length === 0) {
|
||||
throw new BadRequestException(
|
||||
`Entity ${entityType} with ID ${entityId} not found.`,
|
||||
`Entity ${entityType} with ID ${entityId} not found.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,12 +90,12 @@ export class SchemaMigrationService {
|
||||
for (let v = currentVersion + 1; v <= targetSchema.version; v++) {
|
||||
const schemaVer = await this.jsonSchemaService.findOneByCodeAndVersion(
|
||||
targetSchemaCode,
|
||||
v,
|
||||
v
|
||||
);
|
||||
|
||||
if (schemaVer && schemaVer.migrationScript) {
|
||||
this.logger.log(
|
||||
`Applying migration script for ${targetSchemaCode} v${v}...`,
|
||||
`Applying migration script for ${targetSchemaCode} v${v}...`
|
||||
);
|
||||
|
||||
const script = schemaVer.migrationScript;
|
||||
@@ -115,12 +115,12 @@ export class SchemaMigrationService {
|
||||
// 4. Validate Migrated Data against Target Schema
|
||||
const validation = await this.jsonSchemaService.validateData(
|
||||
targetSchema.schemaCode,
|
||||
migratedData,
|
||||
migratedData
|
||||
);
|
||||
|
||||
if (!validation.isValid) {
|
||||
throw new BadRequestException(
|
||||
`Migration failed: Resulting data does not match target schema v${targetSchema.version}. Errors: ${JSON.stringify(validation.errors)}`,
|
||||
`Migration failed: Resulting data does not match target schema v${targetSchema.version}. Errors: ${JSON.stringify(validation.errors)}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ export class SchemaMigrationService {
|
||||
JSON.stringify(validation.sanitizedData),
|
||||
targetSchema.version,
|
||||
entityId,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
@@ -143,9 +143,12 @@ export class SchemaMigrationService {
|
||||
toVersion: targetSchema.version,
|
||||
migratedFields: [...new Set(migratedFields)],
|
||||
};
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(`Migration failed: ${err.message}`, err.stack);
|
||||
this.logger.error(
|
||||
`Migration failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
err instanceof Error ? err.stack : undefined
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
@@ -157,40 +160,45 @@ export class SchemaMigrationService {
|
||||
*/
|
||||
private async applyMigrationStep(
|
||||
step: MigrationStep,
|
||||
data: any,
|
||||
): Promise<any> {
|
||||
data: Record<string, unknown>
|
||||
): Promise<Record<string, unknown>> {
|
||||
const newData = { ...data };
|
||||
|
||||
const field = step.config.field as string;
|
||||
const oldField = step.config.old_field as string;
|
||||
const newField = step.config.new_field as string;
|
||||
|
||||
switch (step.type) {
|
||||
case 'FIELD_RENAME':
|
||||
if (newData[step.config.old_field] !== undefined) {
|
||||
newData[step.config.new_field] = newData[step.config.old_field];
|
||||
delete newData[step.config.old_field];
|
||||
if (newData[oldField] !== undefined) {
|
||||
newData[newField] = newData[oldField];
|
||||
delete newData[oldField];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'FIELD_ADD':
|
||||
if (newData[step.config.field] === undefined) {
|
||||
newData[step.config.field] = step.config.default_value;
|
||||
if (newData[field] === undefined) {
|
||||
newData[field] = step.config.default_value;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'FIELD_REMOVE':
|
||||
delete newData[step.config.field];
|
||||
delete newData[field];
|
||||
break;
|
||||
|
||||
case 'FIELD_TRANSFORM':
|
||||
if (newData[step.config.field] !== undefined) {
|
||||
if (newData[field] !== undefined) {
|
||||
// Simple transform logic (e.g., map values)
|
||||
if (step.config.transform === 'MAP_VALUES' && step.config.mapping) {
|
||||
const oldVal = newData[step.config.field];
|
||||
newData[step.config.field] = step.config.mapping[oldVal] || oldVal;
|
||||
const oldVal = String(newData[field]);
|
||||
const mapping = step.config.mapping as Record<string, unknown>;
|
||||
newData[field] = mapping[oldVal] || newData[field];
|
||||
}
|
||||
// Type casting
|
||||
else if (step.config.transform === 'TO_NUMBER') {
|
||||
newData[step.config.field] = Number(newData[step.config.field]);
|
||||
newData[field] = Number(newData[field]);
|
||||
} else if (step.config.transform === 'TO_STRING') {
|
||||
newData[step.config.field] = String(newData[step.config.field]);
|
||||
newData[field] = String(newData[field]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -202,4 +210,3 @@ export class SchemaMigrationService {
|
||||
return newData;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// File: src/modules/json-schema/services/ui-schema.service.ts
|
||||
import { Injectable, BadRequestException, Logger } from '@nestjs/common';
|
||||
import { UiSchema, UiSchemaField } from '../interfaces/ui-schema.interface';
|
||||
import {
|
||||
UiSchema,
|
||||
UiSchemaField,
|
||||
WidgetType,
|
||||
} from '../interfaces/ui-schema.interface';
|
||||
|
||||
@Injectable()
|
||||
export class UiSchemaService {
|
||||
@@ -9,13 +13,16 @@ export class UiSchemaService {
|
||||
/**
|
||||
* ตรวจสอบความถูกต้องของ UI Schema
|
||||
*/
|
||||
validateUiSchema(uiSchema: UiSchema, dataSchema: any): boolean {
|
||||
validateUiSchema(
|
||||
uiSchema: UiSchema,
|
||||
dataSchema: Record<string, unknown>
|
||||
): boolean {
|
||||
if (!uiSchema) return true; // Optional field
|
||||
|
||||
// 1. Validate Structure เบื้องต้น
|
||||
if (!uiSchema.layout || !uiSchema.fields) {
|
||||
throw new BadRequestException(
|
||||
'UI Schema must contain "layout" and "fields" properties.',
|
||||
'UI Schema must contain "layout" and "fields" properties.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,7 +35,7 @@ export class UiSchemaService {
|
||||
layoutFields.add(fieldKey);
|
||||
if (!definedFields.has(fieldKey)) {
|
||||
throw new BadRequestException(
|
||||
`Field "${fieldKey}" used in layout "${group.title}" is not defined in "fields".`,
|
||||
`Field "${fieldKey}" used in layout "${group.title}" is not defined in "fields".`
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -42,7 +49,7 @@ export class UiSchemaService {
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
this.logger.warn(
|
||||
`Data schema properties [${missingFields.join(', ')}] are missing from UI Schema.`,
|
||||
`Data schema properties [${missingFields.join(', ')}] are missing from UI Schema.`
|
||||
);
|
||||
// ไม่ Throw Error เพราะบางทีเราอาจตั้งใจซ่อน Field (Hidden field)
|
||||
}
|
||||
@@ -55,7 +62,7 @@ export class UiSchemaService {
|
||||
* สร้าง UI Schema พื้นฐานจาก Data Schema (AJV) อัตโนมัติ
|
||||
* ใช้กรณี user ไม่ได้ส่ง UI Schema มาให้
|
||||
*/
|
||||
generateDefaultUiSchema(dataSchema: any): UiSchema {
|
||||
generateDefaultUiSchema(dataSchema: Record<string, unknown>): UiSchema {
|
||||
if (!dataSchema || !dataSchema.properties) {
|
||||
return {
|
||||
layout: { type: 'stack', groups: [] },
|
||||
@@ -66,15 +73,17 @@ export class UiSchemaService {
|
||||
const fields: { [key: string]: UiSchemaField } = {};
|
||||
const groupFields: string[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries<any>(dataSchema.properties)) {
|
||||
for (const [key, value] of Object.entries(
|
||||
dataSchema.properties as Record<string, Record<string, unknown>>
|
||||
)) {
|
||||
groupFields.push(key);
|
||||
|
||||
fields[key] = {
|
||||
type: value.type || 'string',
|
||||
title: value.title || this.humanize(key),
|
||||
description: value.description,
|
||||
required: (dataSchema.required || []).includes(key),
|
||||
widget: this.guessWidget(value),
|
||||
type: (value.type as UiSchemaField['type']) || 'string',
|
||||
title: (value.title as string) || this.humanize(key),
|
||||
description: value.description as string | undefined,
|
||||
required: ((dataSchema.required as string[]) || []).includes(key),
|
||||
widget: this.guessWidget(value) as WidgetType,
|
||||
colSpan: 12, // Default full width
|
||||
};
|
||||
}
|
||||
@@ -103,7 +112,7 @@ export class UiSchemaService {
|
||||
.trim();
|
||||
}
|
||||
|
||||
private guessWidget(schemaProp: any): any {
|
||||
private guessWidget(schemaProp: Record<string, unknown>): WidgetType {
|
||||
if (schemaProp.enum) return 'select';
|
||||
if (schemaProp.type === 'boolean') return 'checkbox';
|
||||
if (schemaProp.format === 'date') return 'date';
|
||||
@@ -112,4 +121,3 @@ export class UiSchemaService {
|
||||
return 'text';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,14 +21,14 @@ export class VirtualColumnService {
|
||||
|
||||
try {
|
||||
this.logger.log(
|
||||
`Start setting up virtual columns for table '${tableName}'...`,
|
||||
`Start setting up virtual columns for table '${tableName}'...`
|
||||
);
|
||||
|
||||
// 1. ตรวจสอบว่าตารางมีอยู่จริงไหม
|
||||
const tableExists = await queryRunner.hasTable(tableName);
|
||||
if (!tableExists) {
|
||||
this.logger.warn(
|
||||
`Table '${tableName}' not found. Skipping virtual columns.`,
|
||||
`Table '${tableName}' not found. Skipping virtual columns.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -42,12 +42,12 @@ export class VirtualColumnService {
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Finished setting up virtual columns for '${tableName}'.`,
|
||||
`Finished setting up virtual columns for '${tableName}'.`
|
||||
);
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
this.logger.error(
|
||||
`Failed to setup virtual columns: ${err.message}`,
|
||||
err.stack,
|
||||
`Failed to setup virtual columns: ${err instanceof Error ? err.message : String(err)}`,
|
||||
err instanceof Error ? err.stack : undefined
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
@@ -61,11 +61,11 @@ export class VirtualColumnService {
|
||||
private async ensureVirtualColumn(
|
||||
queryRunner: QueryRunner,
|
||||
tableName: string,
|
||||
config: VirtualColumnConfig,
|
||||
config: VirtualColumnConfig
|
||||
) {
|
||||
const hasColumn = await queryRunner.hasColumn(
|
||||
tableName,
|
||||
config.column_name,
|
||||
config.column_name
|
||||
);
|
||||
|
||||
if (!hasColumn) {
|
||||
@@ -75,7 +75,7 @@ export class VirtualColumnService {
|
||||
} else {
|
||||
// TODO: (Advance) ถ้ามี Column แล้ว แต่ Definition เปลี่ยน อาจต้อง ALTER MODIFY
|
||||
this.logger.debug(
|
||||
`Column '${config.column_name}' already exists in '${tableName}'.`,
|
||||
`Column '${config.column_name}' already exists in '${tableName}'.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ export class VirtualColumnService {
|
||||
private async ensureIndex(
|
||||
queryRunner: QueryRunner,
|
||||
tableName: string,
|
||||
config: VirtualColumnConfig,
|
||||
config: VirtualColumnConfig
|
||||
) {
|
||||
const indexName = `idx_${tableName}_${config.column_name}`;
|
||||
|
||||
@@ -116,7 +116,7 @@ export class VirtualColumnService {
|
||||
*/
|
||||
private generateAddColumnSql(
|
||||
tableName: string,
|
||||
config: VirtualColumnConfig,
|
||||
config: VirtualColumnConfig
|
||||
): string {
|
||||
const dbType = this.mapDataTypeToSql(config.data_type);
|
||||
// JSON_UNQUOTE(JSON_EXTRACT(details, '$.path'))
|
||||
@@ -149,4 +149,3 @@ export class VirtualColumnService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user