260320:1131 Refactor Overrall #01
Build and Deploy / deploy (push) Has been cancelled

This commit is contained in:
admin
2026-03-20 11:31:27 +07:00
parent f1b81a7d0d
commit 1d3479770b
147 changed files with 1745 additions and 1567 deletions
@@ -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 {
}
}
}