251128:1700 Backend to T3.1.1

This commit is contained in:
admin
2025-11-28 17:12:05 +07:00
parent b22d00877e
commit f7a43600a3
50 changed files with 4891 additions and 2849 deletions

View File

@@ -0,0 +1,115 @@
// 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';
@Injectable()
export class UiSchemaService {
private readonly logger = new Logger(UiSchemaService.name);
/**
* ตรวจสอบความถูกต้องของ UI Schema
*/
validateUiSchema(uiSchema: UiSchema, dataSchema: any): 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.',
);
}
// 2. ตรวจสอบว่า Fields ใน Layout มีคำนิยามครบถ้วน
const definedFields = new Set(Object.keys(uiSchema.fields));
const layoutFields = new Set<string>();
uiSchema.layout.groups.forEach((group) => {
group.fields.forEach((fieldKey) => {
layoutFields.add(fieldKey);
if (!definedFields.has(fieldKey)) {
throw new BadRequestException(
`Field "${fieldKey}" used in layout "${group.title}" is not defined in "fields".`,
);
}
});
});
// 3. (Optional) ตรวจสอบว่า Fields ใน Data Schema (AJV) มีครบใน UI Schema หรือไม่
// ถ้า Data Schema บอกว่ามี field 'title' แต่ UI Schema ไม่มี -> Frontend อาจจะไม่เรนเดอร์
if (dataSchema && dataSchema.properties) {
const dataKeys = Object.keys(dataSchema.properties);
const missingFields = dataKeys.filter((key) => !definedFields.has(key));
if (missingFields.length > 0) {
this.logger.warn(
`Data schema properties [${missingFields.join(', ')}] are missing from UI Schema.`,
);
// ไม่ Throw Error เพราะบางทีเราอาจตั้งใจซ่อน Field (Hidden field)
}
}
return true;
}
/**
* สร้าง UI Schema พื้นฐานจาก Data Schema (AJV) อัตโนมัติ
* ใช้กรณี user ไม่ได้ส่ง UI Schema มาให้
*/
generateDefaultUiSchema(dataSchema: any): UiSchema {
if (!dataSchema || !dataSchema.properties) {
return {
layout: { type: 'stack', groups: [] },
fields: {},
};
}
const fields: { [key: string]: UiSchemaField } = {};
const groupFields: string[] = [];
for (const [key, value] of Object.entries<any>(dataSchema.properties)) {
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),
colSpan: 12, // Default full width
};
}
return {
layout: {
type: 'stack',
groups: [
{
id: 'default',
title: 'General Information',
type: 'section',
fields: groupFields,
},
],
},
fields,
};
}
// Helpers
private humanize(str: string): string {
return str
.replace(/([A-Z])/g, ' $1')
.replace(/^./, (str) => str.toUpperCase())
.trim();
}
private guessWidget(schemaProp: any): any {
if (schemaProp.enum) return 'select';
if (schemaProp.type === 'boolean') return 'checkbox';
if (schemaProp.format === 'date') return 'date';
if (schemaProp.format === 'date-time') return 'datetime';
if (schemaProp.format === 'binary') return 'file-upload';
return 'text';
}
}