scaffold-engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -0
- package/engine.project.example.json +23 -0
- package/package.json +49 -0
- package/scripts/postinstall.cjs +42 -0
- package/specs/catalogs/action-templates.yaml +189 -0
- package/specs/catalogs/child-templates.yaml +54 -0
- package/specs/catalogs/field-fragments.yaml +203 -0
- package/specs/catalogs/object-catalog.yaml +35 -0
- package/specs/catalogs/object-name-suggestions.yaml +30 -0
- package/specs/catalogs/object-templates.yaml +45 -0
- package/specs/catalogs/pattern-catalog.yaml +48 -0
- package/specs/catalogs/status-templates.yaml +16 -0
- package/specs/projects/crm-pilot/customer.yaml +122 -0
- package/specs/projects/crm-pilot/lead.from-nl.yaml +76 -0
- package/specs/projects/crm-pilot/lead.yaml +82 -0
- package/specs/projects/generated-from-nl/crm-customer.yaml +158 -0
- package/specs/projects/generated-from-nl/crm-lead.yaml +76 -0
- package/specs/projects/generated-from-nl/crm-opportunity.yaml +78 -0
- package/specs/projects/generated-from-nl/crm-quote.yaml +78 -0
- package/specs/projects/generated-from-nl/custom-documentLines.yaml +125 -0
- package/specs/projects/generated-from-nl/custom-treeEntity.yaml +78 -0
- package/specs/projects/generated-from-nl/erp-material-pattern-test.yaml +79 -0
- package/specs/projects/generated-from-nl/erp-material.yaml +78 -0
- package/specs/projects/generated-from-nl/hr-orgUnit.yaml +100 -0
- package/specs/projects/pattern-examples/document-lines-demo.yaml +125 -0
- package/specs/projects/pattern-examples/tree-entity-demo.yaml +79 -0
- package/specs/rules/business-model.schema.json +262 -0
- package/specs/rules/extension-boundaries.json +26 -0
- package/specs/rules/requirement-draft.schema.json +75 -0
- package/specs/rules/spec-governance.json +29 -0
- package/specs/templates/crm/customer.template.yaml +121 -0
- package/specs/templates/crm/lead.template.yaml +82 -0
- package/tools/analyze-requirement.cjs +950 -0
- package/tools/cli.cjs +59 -0
- package/tools/create-draft.cjs +18 -0
- package/tools/engine.cjs +47 -0
- package/tools/generate-draft.cjs +33 -0
- package/tools/generate-module.cjs +1218 -0
- package/tools/init-project.cjs +194 -0
- package/tools/lib/draft-toolkit.cjs +357 -0
- package/tools/lib/model-toolkit.cjs +482 -0
- package/tools/lib/pattern-renderers.cjs +166 -0
- package/tools/lib/renderers/detail-page-renderer.cjs +327 -0
- package/tools/lib/renderers/form-page-renderer.cjs +553 -0
- package/tools/lib/renderers/list-page-renderer.cjs +371 -0
- package/tools/lib/runtime-config.cjs +154 -0
- package/tools/patch-draft.cjs +57 -0
- package/tools/prompts/business-model-prompt.md +58 -0
- package/tools/run-requirement.cjs +672 -0
- package/tools/validate-draft.cjs +32 -0
- package/tools/validate-model.cjs +140 -0
- package/tools/verify-patterns.cjs +67 -0
|
@@ -0,0 +1,1218 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const {
|
|
4
|
+
buildContext,
|
|
5
|
+
loadRuntimeConfig,
|
|
6
|
+
resolveScaffoldPath,
|
|
7
|
+
validateModelFile,
|
|
8
|
+
writeGeneratedFile,
|
|
9
|
+
snakeCase,
|
|
10
|
+
} = require('./lib/model-toolkit.cjs');
|
|
11
|
+
const {
|
|
12
|
+
getAdminHeaderIcon,
|
|
13
|
+
getPatternCreateLabel,
|
|
14
|
+
getPatternDetailSubtitle,
|
|
15
|
+
getPatternFieldPriority,
|
|
16
|
+
getPatternFormSubtitle,
|
|
17
|
+
getPatternId,
|
|
18
|
+
getPatternListSubtitle,
|
|
19
|
+
getPatternListTitle,
|
|
20
|
+
getPatternSearchPlaceholder,
|
|
21
|
+
} = require('./lib/pattern-renderers.cjs');
|
|
22
|
+
const { renderAdminListPage: renderAdminListPageModule } = require('./lib/renderers/list-page-renderer.cjs');
|
|
23
|
+
const { renderAdminFormPage: renderAdminFormPageModule } = require('./lib/renderers/form-page-renderer.cjs');
|
|
24
|
+
const { renderAdminDetailPage: renderAdminDetailPageModule } = require('./lib/renderers/detail-page-renderer.cjs');
|
|
25
|
+
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
const force = args.includes('--force');
|
|
28
|
+
const dryRun = args.includes('--dry-run');
|
|
29
|
+
const configPath = readArgValue(args, '--config');
|
|
30
|
+
const specArg = args.find((item) => !item.startsWith('--'));
|
|
31
|
+
const specPath = specArg
|
|
32
|
+
? resolveInputPath(specArg)
|
|
33
|
+
: resolveScaffoldPath('specs', 'projects', 'crm-pilot', 'customer.yaml');
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const runtimeConfig = loadRuntimeConfig({ configPath });
|
|
37
|
+
const validation = validateModelFile(specPath, runtimeConfig);
|
|
38
|
+
if (!validation.valid) {
|
|
39
|
+
console.error('\n❌ 模型未通过校验,无法生成模块。请先执行 model:validate 修复问题。');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const context = buildContext(validation.model);
|
|
44
|
+
const targets = buildTargets(context, runtimeConfig);
|
|
45
|
+
const results = [];
|
|
46
|
+
|
|
47
|
+
for (const target of targets) {
|
|
48
|
+
results.push(writeGeneratedFile(target.filePath, target.content, { force, dryRun }));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const written = results.filter((item) => item.status === 'written' || item.status === 'planned');
|
|
52
|
+
const skipped = results.filter((item) => item.status === 'skipped');
|
|
53
|
+
|
|
54
|
+
console.log(`\n🧩 已处理模型: ${validation.absolutePath}`);
|
|
55
|
+
console.log(`📁 目标模块: ${context.domainKebab}/${context.objectKebab}`);
|
|
56
|
+
console.log(`📝 新生成文件: ${written.length}`);
|
|
57
|
+
console.log(`⏭️ 跳过文件: ${skipped.length}`);
|
|
58
|
+
|
|
59
|
+
for (const item of written) {
|
|
60
|
+
console.log(`- [${item.status}] ${path.relative(resolveScaffoldPath(), item.filePath)}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const item of skipped) {
|
|
64
|
+
console.log(`- [skipped] ${path.relative(resolveScaffoldPath(), item.filePath)}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log('\n✅ 模块骨架生成完成');
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error(`\n❌ 模块生成失败: ${error.message}`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildTargets(context, runtimeConfig) {
|
|
74
|
+
const scaffoldRoot = runtimeConfig.scaffoldRoot;
|
|
75
|
+
const apiBase = runtimeConfig.targets.apiSrcDir;
|
|
76
|
+
const adminBase = runtimeConfig.targets.adminSrcDir;
|
|
77
|
+
const generatedHeader = `/**\n * AUTO-GENERATED FILE.\n * Source spec: ${context.meta.domain}/${context.meta.object}\n * Edit the business model first, then regenerate.\n */\n`;
|
|
78
|
+
|
|
79
|
+
return [
|
|
80
|
+
{
|
|
81
|
+
filePath: path.join(apiBase, 'entity', 'generated', context.domainKebab, `${context.objectKebab}.entity.ts`),
|
|
82
|
+
content: generatedHeader + renderRootEntity(context),
|
|
83
|
+
},
|
|
84
|
+
...context.childTables.map((child) => ({
|
|
85
|
+
filePath: path.join(apiBase, 'entity', 'generated', context.domainKebab, `${child.fileName}.entity.ts`),
|
|
86
|
+
content: generatedHeader + renderChildEntity(context, child),
|
|
87
|
+
})),
|
|
88
|
+
{
|
|
89
|
+
filePath: path.join(apiBase, 'dto', 'generated', context.domainKebab, `${context.objectKebab}.dto.ts`),
|
|
90
|
+
content: generatedHeader + renderDto(context),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
filePath: path.join(apiBase, 'mapper', 'generated', context.domainKebab, `${context.objectKebab}.mapper.ts`),
|
|
94
|
+
content: generatedHeader + renderMapper(context),
|
|
95
|
+
},
|
|
96
|
+
...context.childTables.map((child) => ({
|
|
97
|
+
filePath: path.join(apiBase, 'mapper', 'generated', context.domainKebab, `${child.fileName}.mapper.ts`),
|
|
98
|
+
content: generatedHeader + renderChildMapper(context, child),
|
|
99
|
+
})),
|
|
100
|
+
{
|
|
101
|
+
filePath: path.join(apiBase, 'service', 'generated', context.domainKebab, `${context.objectKebab}.service.ts`),
|
|
102
|
+
content: generatedHeader + renderService(context),
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
filePath: path.join(apiBase, 'controller', 'generated', context.domainKebab, `${context.objectKebab}.controller.ts`),
|
|
106
|
+
content: generatedHeader + renderController(context),
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
filePath: path.join(apiBase, 'scripts', 'generated', context.domainKebab, `${context.objectKebab}.init.ts`),
|
|
110
|
+
content: generatedHeader + renderInitScript(context),
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
filePath: path.join(adminBase, 'pages', 'generated', context.domainKebab, context.objectKebab, 'ListPage.tsx'),
|
|
114
|
+
content: generatedHeader + renderAdminListPage(context),
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
filePath: path.join(adminBase, 'pages', 'generated', context.domainKebab, context.objectKebab, `${context.objectKebab}-ui.ts`),
|
|
118
|
+
content: generatedHeader + renderAdminUiHelper(context),
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
filePath: path.join(adminBase, 'pages', 'generated', context.domainKebab, context.objectKebab, 'FormPage.tsx'),
|
|
122
|
+
content: generatedHeader + renderAdminFormPage(context),
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
filePath: path.join(adminBase, 'pages', 'generated', context.domainKebab, context.objectKebab, 'DetailPage.tsx'),
|
|
126
|
+
content: generatedHeader + renderAdminDetailPage(context),
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
filePath: path.join(adminBase, 'routes', 'modules', `${context.moduleSlug}.ts`),
|
|
130
|
+
content: generatedHeader + renderAdminRouteModule(context),
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function readArgValue(argv, flag) {
|
|
136
|
+
const index = argv.indexOf(flag);
|
|
137
|
+
if (index === -1) {
|
|
138
|
+
return '';
|
|
139
|
+
}
|
|
140
|
+
return argv[index + 1] || '';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function renderRootEntity(context) {
|
|
144
|
+
const fieldLines = context.fields.map((field) => renderEntityField(field)).join('\n\n');
|
|
145
|
+
return `import { Entity, TableField, TableId } from '@ai-partner-x/aiko-boot-starter-orm';
|
|
146
|
+
|
|
147
|
+
@Entity({ tableName: '${context.data.table}' })
|
|
148
|
+
export class ${context.objectPascal} {
|
|
149
|
+
@TableId({ type: 'AUTO' })
|
|
150
|
+
id!: number;
|
|
151
|
+
|
|
152
|
+
${indent(fieldLines, 2)}
|
|
153
|
+
|
|
154
|
+
@TableField({ column: 'created_at' })
|
|
155
|
+
createdAt?: Date;
|
|
156
|
+
|
|
157
|
+
@TableField({ column: 'updated_at' })
|
|
158
|
+
updatedAt?: Date;
|
|
159
|
+
}
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function renderChildEntity(context, child) {
|
|
164
|
+
const fieldLines = child.fields.map((field) => renderEntityField(field)).join('\n\n');
|
|
165
|
+
return `import { Entity, TableField, TableId } from '@ai-partner-x/aiko-boot-starter-orm';
|
|
166
|
+
|
|
167
|
+
@Entity({ tableName: '${child.table}' })
|
|
168
|
+
export class ${child.className} {
|
|
169
|
+
@TableId({ type: 'AUTO' })
|
|
170
|
+
id!: number;
|
|
171
|
+
|
|
172
|
+
@TableField({ column: '${snakeCase(child.foreignKey)}' })
|
|
173
|
+
${child.foreignKey}!: number;
|
|
174
|
+
|
|
175
|
+
${indent(fieldLines, 2)}
|
|
176
|
+
|
|
177
|
+
@TableField({ column: 'created_at' })
|
|
178
|
+
createdAt?: Date;
|
|
179
|
+
|
|
180
|
+
@TableField({ column: 'updated_at' })
|
|
181
|
+
updatedAt?: Date;
|
|
182
|
+
}
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function renderDto(context) {
|
|
187
|
+
const createFields = context.fields.map((field) => renderDtoField(field, field.required)).join('\n');
|
|
188
|
+
const updateFields = context.fields.map((field) => renderDtoField(field, false)).join('\n');
|
|
189
|
+
const pageFields = ((context.ui.list && context.ui.list.filters) || []).map((fieldName) => {
|
|
190
|
+
const field = context.fields.find((item) => item.name === fieldName);
|
|
191
|
+
const tsType = field ? normalizeDtoType(field.tsType) : 'string';
|
|
192
|
+
return ` ${fieldName}?: ${tsType};`;
|
|
193
|
+
}).join('\n');
|
|
194
|
+
const childClasses = context.childTables.map((child) => {
|
|
195
|
+
const lines = child.fields.map((field) => renderDtoField(field, false)).join('\n');
|
|
196
|
+
return `export class ${child.className}Input {\n${lines}\n}\n`;
|
|
197
|
+
}).join('\n');
|
|
198
|
+
const childCreateProps = context.childTables.map((child) => ` ${child.variableName}?: ${child.className}Input[];`).join('\n');
|
|
199
|
+
|
|
200
|
+
return `${childClasses}export class Create${context.objectPascal}Dto {
|
|
201
|
+
${createFields}
|
|
202
|
+
${childCreateProps ? `${childCreateProps}\n` : ''}}
|
|
203
|
+
|
|
204
|
+
export class Update${context.objectPascal}Dto {
|
|
205
|
+
${updateFields}
|
|
206
|
+
${childCreateProps ? `${childCreateProps}\n` : ''}}
|
|
207
|
+
|
|
208
|
+
export class ${context.objectPascal}PageDto {
|
|
209
|
+
pageNo: number = 1;
|
|
210
|
+
pageSize: number = 10;
|
|
211
|
+
${pageFields ? `${pageFields}\n` : ''}}
|
|
212
|
+
|
|
213
|
+
export interface ${context.objectPascal}Vo {
|
|
214
|
+
id: number;
|
|
215
|
+
${context.fields.map((field) => ` ${field.name}${field.required ? '' : '?'}: ${normalizeDtoType(field.tsType)};`).join('\n')}
|
|
216
|
+
${context.childTables.map((child) => ` ${child.variableName}?: ${child.className}Input[];`).join('\n')}
|
|
217
|
+
createdAt?: Date | string;
|
|
218
|
+
updatedAt?: Date | string;
|
|
219
|
+
}
|
|
220
|
+
`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function renderMapper(context) {
|
|
224
|
+
return `import 'reflect-metadata';
|
|
225
|
+
import { BaseMapper, Mapper } from '@ai-partner-x/aiko-boot-starter-orm';
|
|
226
|
+
import { ${context.objectPascal} } from '../../../entity/generated/${context.domainKebab}/${context.objectKebab}.entity.js';
|
|
227
|
+
|
|
228
|
+
@Mapper(${context.objectPascal})
|
|
229
|
+
export class ${context.objectPascal}Mapper extends BaseMapper<${context.objectPascal}> {}
|
|
230
|
+
`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function renderChildMapper(context, child) {
|
|
234
|
+
return `import 'reflect-metadata';
|
|
235
|
+
import { BaseMapper, Mapper } from '@ai-partner-x/aiko-boot-starter-orm';
|
|
236
|
+
import { ${child.className} } from '../../../entity/generated/${context.domainKebab}/${child.fileName}.entity.js';
|
|
237
|
+
|
|
238
|
+
@Mapper(${child.className})
|
|
239
|
+
export class ${child.className}Mapper extends BaseMapper<${child.className}> {}
|
|
240
|
+
`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function renderService(context) {
|
|
244
|
+
const childDtoImports = context.childTables.map((child) => `${child.className}Input`).join(',\n ');
|
|
245
|
+
const actionMethods = [];
|
|
246
|
+
|
|
247
|
+
if (context.hasAction('activate')) {
|
|
248
|
+
actionMethods.push(` async activate${context.objectPascal}(id: number) {
|
|
249
|
+
const entity = await this.${context.objectCamel}Mapper.selectById(id);
|
|
250
|
+
if (!entity) {
|
|
251
|
+
throw new Error('${context.label}不存在');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const nextEntity = { ...entity, status: 'active', updatedAt: new Date().toISOString() } as any;
|
|
255
|
+
await this.${context.objectCamel}Mapper.updateById(nextEntity);
|
|
256
|
+
return this.get${context.objectPascal}ById(id);
|
|
257
|
+
}\n`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (context.hasAction('deactivate')) {
|
|
261
|
+
actionMethods.push(` async deactivate${context.objectPascal}(id: number) {
|
|
262
|
+
const entity = await this.${context.objectCamel}Mapper.selectById(id);
|
|
263
|
+
if (!entity) {
|
|
264
|
+
throw new Error('${context.label}不存在');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const nextEntity = { ...entity, status: 'inactive', updatedAt: new Date().toISOString() } as any;
|
|
268
|
+
await this.${context.objectCamel}Mapper.updateById(nextEntity);
|
|
269
|
+
return this.get${context.objectPascal}ById(id);
|
|
270
|
+
}\n`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return `import 'reflect-metadata';
|
|
274
|
+
import { Autowired, Injectable } from '@ai-partner-x/aiko-boot/di/server';
|
|
275
|
+
import type {
|
|
276
|
+
Create${context.objectPascal}Dto,
|
|
277
|
+
Update${context.objectPascal}Dto,
|
|
278
|
+
${context.objectPascal}PageDto,
|
|
279
|
+
${context.objectPascal}Vo,
|
|
280
|
+
${childDtoImports ? ` ${childDtoImports}` : ''}
|
|
281
|
+
} from '../../../dto/generated/${context.domainKebab}/${context.objectKebab}.dto.js';
|
|
282
|
+
import { ${context.objectPascal}Mapper } from '../../../mapper/generated/${context.domainKebab}/${context.objectKebab}.mapper.js';
|
|
283
|
+
${context.childTables.map((child) => `import { ${child.className}Mapper } from '../../../mapper/generated/${context.domainKebab}/${child.fileName}.mapper.js';`).join('\n')}
|
|
284
|
+
|
|
285
|
+
@Injectable()
|
|
286
|
+
export class ${context.objectPascal}Service {
|
|
287
|
+
@Autowired()
|
|
288
|
+
private ${context.objectCamel}Mapper!: ${context.objectPascal}Mapper;
|
|
289
|
+
|
|
290
|
+
${context.childTables.map((child) => ` @Autowired()\n private ${child.variableName}Mapper!: ${child.className}Mapper;\n`).join('\n')}
|
|
291
|
+
|
|
292
|
+
async page${context.objectPascal}s(params: ${context.objectPascal}PageDto) {
|
|
293
|
+
const records = await this.${context.objectCamel}Mapper.selectList({}, [{ field: 'id', direction: 'desc' }]);
|
|
294
|
+
let filtered = records.map((item) => this.normalizeRootEntity(item));
|
|
295
|
+
${context.fields.map((field) => {
|
|
296
|
+
const filterEnabled = (context.ui.list && context.ui.list.filters || []).includes(field.name);
|
|
297
|
+
if (!filterEnabled) return '';
|
|
298
|
+
if (field.tsType === 'number') {
|
|
299
|
+
return ` if (params.${field.name} !== undefined && params.${field.name} !== null && params.${field.name} !== '' as any) {
|
|
300
|
+
filtered = filtered.filter((item) => Number(item.${field.name}) === Number(params.${field.name}));
|
|
301
|
+
}`;
|
|
302
|
+
}
|
|
303
|
+
return ` if (params.${field.name}) {
|
|
304
|
+
filtered = filtered.filter((item) => String(item.${field.name} || '').toLowerCase().includes(String(params.${field.name}).toLowerCase()));
|
|
305
|
+
}`;
|
|
306
|
+
}).filter(Boolean).join('\n')}
|
|
307
|
+
|
|
308
|
+
const pageNo = params.pageNo || 1;
|
|
309
|
+
const pageSize = params.pageSize || 10;
|
|
310
|
+
const total = filtered.length;
|
|
311
|
+
const start = (pageNo - 1) * pageSize;
|
|
312
|
+
const pageRecords = filtered.slice(start, start + pageSize);
|
|
313
|
+
const pageItems = [];
|
|
314
|
+
|
|
315
|
+
for (const record of pageRecords) {
|
|
316
|
+
pageItems.push(await this.toVo(record));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
records: pageItems,
|
|
321
|
+
total,
|
|
322
|
+
pageNo,
|
|
323
|
+
pageSize,
|
|
324
|
+
totalPages: Math.ceil(total / pageSize),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async get${context.objectPascal}ById(id: number): Promise<${context.objectPascal}Vo> {
|
|
329
|
+
const entity = await this.${context.objectCamel}Mapper.selectById(id);
|
|
330
|
+
if (!entity) {
|
|
331
|
+
throw new Error('${context.label}不存在');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return this.toVo(this.normalizeRootEntity(entity));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async create${context.objectPascal}(dto: Create${context.objectPascal}Dto) {
|
|
338
|
+
await this.assertUniqueConstraints(dto);
|
|
339
|
+
|
|
340
|
+
const now = new Date().toISOString();
|
|
341
|
+
await this.${context.objectCamel}Mapper.insert({
|
|
342
|
+
${context.fields.map((field) => ` ${field.name}: dto.${field.name},`).join('\n')}
|
|
343
|
+
createdAt: now,
|
|
344
|
+
updatedAt: now,
|
|
345
|
+
} as any);
|
|
346
|
+
|
|
347
|
+
const created = await this.findCreatedEntity(dto);
|
|
348
|
+
if (!created) {
|
|
349
|
+
throw new Error('创建${context.label}失败');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
${context.childTables.map((child) => ` if (dto.${child.variableName}) {
|
|
353
|
+
await this.replace${child.className}(created.id, dto.${child.variableName});
|
|
354
|
+
}`).join('\n')}
|
|
355
|
+
|
|
356
|
+
return this.get${context.objectPascal}ById(Number(created.id));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async update${context.objectPascal}(id: number, dto: Update${context.objectPascal}Dto) {
|
|
360
|
+
const current = await this.${context.objectCamel}Mapper.selectById(id);
|
|
361
|
+
if (!current) {
|
|
362
|
+
throw new Error('${context.label}不存在');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
await this.assertUniqueConstraints(dto, id);
|
|
366
|
+
const nextEntity = {
|
|
367
|
+
...this.normalizeRootEntity(current),
|
|
368
|
+
${context.fields.map((field) => ` ${field.name}: dto.${field.name} !== undefined ? dto.${field.name} : current.${field.name},`).join('\n')}
|
|
369
|
+
updatedAt: new Date().toISOString(),
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
await this.${context.objectCamel}Mapper.updateById(nextEntity as any);
|
|
373
|
+
|
|
374
|
+
${context.childTables.map((child) => ` if (dto.${child.variableName}) {
|
|
375
|
+
await this.replace${child.className}(id, dto.${child.variableName});
|
|
376
|
+
}`).join('\n')}
|
|
377
|
+
|
|
378
|
+
return this.get${context.objectPascal}ById(id);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
${actionMethods.join('\n')}
|
|
382
|
+
private normalizeRootEntity(entity: any) {
|
|
383
|
+
return {
|
|
384
|
+
...entity,
|
|
385
|
+
createdAt: entity.createdAt instanceof Date ? entity.createdAt.toISOString() : entity.createdAt,
|
|
386
|
+
updatedAt: entity.updatedAt instanceof Date ? entity.updatedAt.toISOString() : entity.updatedAt,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private async assertUniqueConstraints(dto: Partial<Create${context.objectPascal}Dto>, excludeId?: number) {
|
|
391
|
+
${context.fields.filter((field) => field.unique).map((field) => ` if (dto.${field.name} !== undefined) {
|
|
392
|
+
const existing = await this.${context.objectCamel}Mapper.selectOne({ ${field.name}: dto.${field.name} } as any);
|
|
393
|
+
if (existing && Number(existing.id) !== Number(excludeId || 0)) {
|
|
394
|
+
throw new Error('${field.label || field.name}已存在');
|
|
395
|
+
}
|
|
396
|
+
}`).join('\n')}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private async findCreatedEntity(dto: Create${context.objectPascal}Dto) {
|
|
400
|
+
${renderFindCreatedEntity(context)}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
${context.childTables.map((child) => ` private async replace${child.className}(rootId: number, rows: ${child.className}Input[]) {
|
|
404
|
+
await this.${child.variableName}Mapper.delete({ ${child.foreignKey}: rootId } as any);
|
|
405
|
+
for (const row of rows) {
|
|
406
|
+
await this.${child.variableName}Mapper.insert({
|
|
407
|
+
${child.foreignKey}: rootId,
|
|
408
|
+
${child.fields.map((field) => ` ${field.name}: ${renderPersistExpression(field, `row.${field.name}`)},`).join('\n')}
|
|
409
|
+
createdAt: new Date().toISOString(),
|
|
410
|
+
updatedAt: new Date().toISOString(),
|
|
411
|
+
} as any);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private async load${child.className}(rootId: number): Promise<${child.className}Input[]> {
|
|
416
|
+
const rows = await this.${child.variableName}Mapper.selectList({ ${child.foreignKey}: rootId } as any, [{ field: 'id', direction: 'asc' }]);
|
|
417
|
+
return rows.map((item: any) => ({
|
|
418
|
+
${child.fields.map((field) => ` ${field.name}: ${renderReadExpression(field, `item.${field.name}`)},`).join('\n')}
|
|
419
|
+
}));
|
|
420
|
+
}
|
|
421
|
+
`).join('\n')}
|
|
422
|
+
private async toVo(entity: any): Promise<${context.objectPascal}Vo> {
|
|
423
|
+
return {
|
|
424
|
+
id: Number(entity.id),
|
|
425
|
+
${context.fields.map((field) => ` ${field.name}: entity.${field.name},`).join('\n')}
|
|
426
|
+
${context.childTables.map((child) => ` ${child.variableName}: await this.load${child.className}(Number(entity.id)),`).join('\n')}
|
|
427
|
+
createdAt: entity.createdAt,
|
|
428
|
+
updatedAt: entity.updatedAt,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function renderController(context) {
|
|
436
|
+
const filterParams = ((context.ui.list && context.ui.list.filters) || []).map((fieldName) => {
|
|
437
|
+
return ` @RequestParam('${fieldName}') ${fieldName}: string,`;
|
|
438
|
+
}).join('\n');
|
|
439
|
+
|
|
440
|
+
const pageAssignments = ((context.ui.list && context.ui.list.filters) || []).map((fieldName) => {
|
|
441
|
+
const field = context.fields.find((item) => item.name === fieldName);
|
|
442
|
+
const valueExpr = field && ['number'].includes(normalizeDtoType(field.tsType))
|
|
443
|
+
? `${fieldName} ? Number(${fieldName}) : undefined`
|
|
444
|
+
: `${fieldName} || undefined`;
|
|
445
|
+
return ` ${fieldName}: ${valueExpr},`;
|
|
446
|
+
}).join('\n');
|
|
447
|
+
|
|
448
|
+
const actionMethods = [];
|
|
449
|
+
|
|
450
|
+
if (context.hasAction('activate')) {
|
|
451
|
+
actionMethods.push(` @PutMapping('/:id/activate')
|
|
452
|
+
async activate(@PathVariable('id') id: string) {
|
|
453
|
+
return this.${context.objectCamel}Service.activate${context.objectPascal}(Number(id));
|
|
454
|
+
}\n`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (context.hasAction('deactivate')) {
|
|
458
|
+
actionMethods.push(` @PutMapping('/:id/deactivate')
|
|
459
|
+
async deactivate(@PathVariable('id') id: string) {
|
|
460
|
+
return this.${context.objectCamel}Service.deactivate${context.objectPascal}(Number(id));
|
|
461
|
+
}\n`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return `import 'reflect-metadata';
|
|
465
|
+
import {
|
|
466
|
+
GetMapping,
|
|
467
|
+
PathVariable,
|
|
468
|
+
PostMapping,
|
|
469
|
+
PutMapping,
|
|
470
|
+
RequestBody,
|
|
471
|
+
RequestParam,
|
|
472
|
+
RestController,
|
|
473
|
+
} from '@ai-partner-x/aiko-boot-starter-web';
|
|
474
|
+
import { Autowired } from '@ai-partner-x/aiko-boot/di/server';
|
|
475
|
+
import { ${context.objectPascal}Service } from '../../../service/generated/${context.domainKebab}/${context.objectKebab}.service.js';
|
|
476
|
+
import type {
|
|
477
|
+
Create${context.objectPascal}Dto,
|
|
478
|
+
Update${context.objectPascal}Dto,
|
|
479
|
+
${context.objectPascal}PageDto,
|
|
480
|
+
} from '../../../dto/generated/${context.domainKebab}/${context.objectKebab}.dto.js';
|
|
481
|
+
|
|
482
|
+
@RestController({ path: '${context.apiBasePath}' })
|
|
483
|
+
export class ${context.objectPascal}Controller {
|
|
484
|
+
@Autowired()
|
|
485
|
+
private ${context.objectCamel}Service!: ${context.objectPascal}Service;
|
|
486
|
+
|
|
487
|
+
@GetMapping('/page')
|
|
488
|
+
async page(
|
|
489
|
+
@RequestParam('pageNo') pageNo: string,
|
|
490
|
+
@RequestParam('pageSize') pageSize: string,
|
|
491
|
+
${filterParams ? `${filterParams}\n` : ''} ) {
|
|
492
|
+
const params: ${context.objectPascal}PageDto = {
|
|
493
|
+
pageNo: pageNo ? Number(pageNo) : 1,
|
|
494
|
+
pageSize: pageSize ? Number(pageSize) : 10,
|
|
495
|
+
${pageAssignments ? `${pageAssignments}\n` : ''} };
|
|
496
|
+
return this.${context.objectCamel}Service.page${context.objectPascal}s(params);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
@GetMapping('/:id')
|
|
500
|
+
async getById(@PathVariable('id') id: string) {
|
|
501
|
+
return this.${context.objectCamel}Service.get${context.objectPascal}ById(Number(id));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
@PostMapping()
|
|
505
|
+
async create(@RequestBody() dto: Create${context.objectPascal}Dto) {
|
|
506
|
+
return this.${context.objectCamel}Service.create${context.objectPascal}(dto);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
@PutMapping('/:id')
|
|
510
|
+
async update(@PathVariable('id') id: string, @RequestBody() dto: Update${context.objectPascal}Dto) {
|
|
511
|
+
return this.${context.objectCamel}Service.update${context.objectPascal}(Number(id), dto);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
${actionMethods.join('\n')}
|
|
515
|
+
}
|
|
516
|
+
`;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function renderInitScript(context) {
|
|
520
|
+
const rootColumns = context.fields.map((field) => renderColumn(field, 4)).join('\n');
|
|
521
|
+
const childTables = context.childTables.map((child) => {
|
|
522
|
+
const childColumns = child.fields.map((field) => renderColumn(field, 4)).join('\n');
|
|
523
|
+
return ` await db.schema.createTable('${child.table}').ifNotExists()
|
|
524
|
+
.addColumn('id', 'integer', (col: any) => col.primaryKey().autoIncrement())
|
|
525
|
+
.addColumn('${snakeCase(child.foreignKey)}', 'integer', (col: any) => col.notNull())
|
|
526
|
+
${childColumns}
|
|
527
|
+
.addColumn('created_at', 'datetime')
|
|
528
|
+
.addColumn('updated_at', 'datetime')
|
|
529
|
+
.execute();
|
|
530
|
+
`;
|
|
531
|
+
}).join('\n');
|
|
532
|
+
|
|
533
|
+
return `export async function registerModuleTables(db: any) {
|
|
534
|
+
await db.schema.createTable('${context.data.table}').ifNotExists()
|
|
535
|
+
.addColumn('id', 'integer', (col: any) => col.primaryKey().autoIncrement())
|
|
536
|
+
${rootColumns}
|
|
537
|
+
.addColumn('created_at', 'datetime')
|
|
538
|
+
.addColumn('updated_at', 'datetime')
|
|
539
|
+
.execute();
|
|
540
|
+
|
|
541
|
+
${childTables}}
|
|
542
|
+
`;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function renderAdminRouteModule(context) {
|
|
546
|
+
const listImport = `${context.domainKebab}/${context.objectKebab}/ListPage`;
|
|
547
|
+
const formImport = `${context.domainKebab}/${context.objectKebab}/FormPage`;
|
|
548
|
+
const detailImport = `${context.domainKebab}/${context.objectKebab}/DetailPage`;
|
|
549
|
+
const headerIcon = getAdminHeaderIcon(context);
|
|
550
|
+
|
|
551
|
+
return `import { createElement, lazy } from "react";
|
|
552
|
+
import { ${headerIcon} } from "lucide-react";
|
|
553
|
+
import type { RouteConfig } from "../index";
|
|
554
|
+
import { withSuspense } from "../withSuspense";
|
|
555
|
+
|
|
556
|
+
const ${context.objectPascal}ListPage = lazy(() => import("@/pages/generated/${listImport}"));
|
|
557
|
+
const ${context.objectPascal}FormPage = lazy(() => import("@/pages/generated/${formImport}"));
|
|
558
|
+
const ${context.objectPascal}DetailPage = lazy(() => import("@/pages/generated/${detailImport}"));
|
|
559
|
+
|
|
560
|
+
export const routes: RouteConfig[] = [
|
|
561
|
+
{
|
|
562
|
+
path: "${context.routePath}",
|
|
563
|
+
label: "${context.label}",
|
|
564
|
+
icon: createElement(${headerIcon}, { className: "size-[18px]" }),
|
|
565
|
+
group: "business",
|
|
566
|
+
groupName: "${context.domainUpper}",
|
|
567
|
+
groupOrder: 5,
|
|
568
|
+
useGuard: false,
|
|
569
|
+
children: [
|
|
570
|
+
{ index: true, element: withSuspense(${context.objectPascal}ListPage) },
|
|
571
|
+
{ path: "create", element: withSuspense(${context.objectPascal}FormPage) },
|
|
572
|
+
{ path: ":id", element: withSuspense(${context.objectPascal}DetailPage) },
|
|
573
|
+
{ path: ":id/edit", element: withSuspense(${context.objectPascal}FormPage) }
|
|
574
|
+
],
|
|
575
|
+
},
|
|
576
|
+
];
|
|
577
|
+
`;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function renderAdminListPage(context) {
|
|
581
|
+
return renderAdminListPageModule(context, {
|
|
582
|
+
findFieldByPriority,
|
|
583
|
+
getAdminHeaderIcon,
|
|
584
|
+
getFieldLabel,
|
|
585
|
+
getPatternCreateLabel,
|
|
586
|
+
getPatternFieldPriority,
|
|
587
|
+
getPatternId,
|
|
588
|
+
getPatternListSubtitle,
|
|
589
|
+
getPatternListTitle,
|
|
590
|
+
getPatternSearchPlaceholder,
|
|
591
|
+
getPrimarySearchField,
|
|
592
|
+
getPrimaryTitleField,
|
|
593
|
+
indent,
|
|
594
|
+
renderDisplayValue,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function renderAdminFormPage(context) {
|
|
599
|
+
return renderAdminFormPageModule(context, {
|
|
600
|
+
getAdminHeaderIcon,
|
|
601
|
+
getChildEnumOptionConst,
|
|
602
|
+
getFormFields,
|
|
603
|
+
getPatternFormSubtitle,
|
|
604
|
+
getPrimaryTitleField,
|
|
605
|
+
indent,
|
|
606
|
+
renderFieldInput,
|
|
607
|
+
renderFormHeaderFields,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function renderAdminDetailPage(context) {
|
|
612
|
+
return renderAdminDetailPageModule(context, {
|
|
613
|
+
getAdminHeaderIcon,
|
|
614
|
+
getChildEnumOptionConst,
|
|
615
|
+
getPatternDetailSubtitle,
|
|
616
|
+
getPrimaryTitleField,
|
|
617
|
+
indent,
|
|
618
|
+
renderDetailFields,
|
|
619
|
+
renderDetailHeaderFields,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function getPrimaryTitleField(context) {
|
|
624
|
+
const patternCandidates = getPatternFieldPriority(context, 'title');
|
|
625
|
+
return findFieldByPriority(context, patternCandidates)?.name
|
|
626
|
+
|| findFieldByPriority(context, ['leadName', 'opportunityName', 'quoteName', 'contractName', 'customerName', 'materialName'])?.name
|
|
627
|
+
|| context.fields.find((field) => field.name.toLowerCase().endsWith('name'))?.name
|
|
628
|
+
|| context.fields[0]?.name;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function getPrimarySearchField(context) {
|
|
632
|
+
return findFieldByPriority(context, getPatternFieldPriority(context, 'search'))?.name
|
|
633
|
+
|| getPrimaryTitleField(context);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function getFormFields(context) {
|
|
637
|
+
const groups = (context.ui.form && context.ui.form.groups) || [];
|
|
638
|
+
const ordered = [];
|
|
639
|
+
const seen = new Set();
|
|
640
|
+
|
|
641
|
+
for (const group of groups) {
|
|
642
|
+
for (const fieldName of group.fields || []) {
|
|
643
|
+
const field = context.fields.find((item) => item.name === fieldName);
|
|
644
|
+
if (field && !seen.has(field.name)) {
|
|
645
|
+
ordered.push(field);
|
|
646
|
+
seen.add(field.name);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
for (const field of context.fields) {
|
|
652
|
+
if (!seen.has(field.name)) {
|
|
653
|
+
ordered.push(field);
|
|
654
|
+
seen.add(field.name);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return ordered;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function getFieldLabel(context, fieldName) {
|
|
662
|
+
return context.fields.find((field) => field.name === fieldName)?.label || fieldName;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function renderFieldInput(context, field, formRef) {
|
|
666
|
+
if (field.type === 'enum') {
|
|
667
|
+
return `<div className="space-y-2">
|
|
668
|
+
<Label>${field.label || field.name}${field.required ? ' <span className="text-destructive">*</span>' : ''}</Label>
|
|
669
|
+
<Select value={${formRef}.${field.name}} onValueChange={(value) => updateField("${field.name}", value)}>
|
|
670
|
+
<SelectTrigger className="w-full">
|
|
671
|
+
<SelectValue placeholder="请选择${field.label || field.name}" />
|
|
672
|
+
</SelectTrigger>
|
|
673
|
+
<SelectContent>
|
|
674
|
+
{${field.name.toUpperCase()}_OPTIONS.map((item) => (
|
|
675
|
+
<SelectItem key={item.value} value={item.value}>
|
|
676
|
+
{item.label}
|
|
677
|
+
</SelectItem>
|
|
678
|
+
))}
|
|
679
|
+
</SelectContent>
|
|
680
|
+
</Select>
|
|
681
|
+
</div>`;
|
|
682
|
+
}
|
|
683
|
+
return `<div className="space-y-2">
|
|
684
|
+
<Label>${field.label || field.name}${field.required ? ' <span className="text-destructive">*</span>' : ''}</Label>
|
|
685
|
+
<Input
|
|
686
|
+
value={${formRef}.${field.name}}
|
|
687
|
+
onChange={(event) => updateField("${field.name}", event.target.value)}
|
|
688
|
+
placeholder="请输入${field.label || field.name}"
|
|
689
|
+
/>
|
|
690
|
+
</div>`;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function renderFormHeaderFields(context) {
|
|
694
|
+
const picks = getHeaderFieldCandidates(context).slice(0, 4);
|
|
695
|
+
return picks.map((field) => {
|
|
696
|
+
const renderer = field.type === 'enum'
|
|
697
|
+
? `getOptionLabel(${field.name.toUpperCase()}_OPTIONS, form.${field.name})`
|
|
698
|
+
: renderDisplayValue(field, `form.${field.name}`, `"${field.name === 'ownerName' ? '未分配' : '-'}"`);
|
|
699
|
+
return `{ icon: <${getAdminHeaderIcon(context)} className="h-4 w-4" />, label: "${field.label || field.name}", value: ${renderer} },`;
|
|
700
|
+
}).join('\n');
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function renderDetailHeaderFields(context) {
|
|
704
|
+
const picks = getHeaderFieldCandidates(context).slice(0, 3);
|
|
705
|
+
const lines = picks.map((field) => {
|
|
706
|
+
const renderer = field.type === 'enum'
|
|
707
|
+
? `getOptionLabel(${field.name.toUpperCase()}_OPTIONS, ${context.objectCamel}.${field.name})`
|
|
708
|
+
: renderDisplayValue(field, `${context.objectCamel}.${field.name}`, `"${field.name === 'ownerName' ? '未分配' : '-'}"`);
|
|
709
|
+
return `{ icon: <${getAdminHeaderIcon(context)} className="h-4 w-4" />, label: "${field.label || field.name}", value: ${renderer} },`;
|
|
710
|
+
});
|
|
711
|
+
lines.push(`{ icon: <${getAdminHeaderIcon(context)} className="h-4 w-4" />, label: "创建时间", value: formatDateTime(${context.objectCamel}.createdAt) },`);
|
|
712
|
+
return lines.join('\n');
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function renderDetailFields(context) {
|
|
716
|
+
return getFormFields(context).map((field) => {
|
|
717
|
+
const value = field.name === 'status'
|
|
718
|
+
? `statusMeta.label`
|
|
719
|
+
: field.type === 'enum'
|
|
720
|
+
? `getOptionLabel(${field.name.toUpperCase()}_OPTIONS, ${context.objectCamel}.${field.name})`
|
|
721
|
+
: renderDisplayValue(field, `${context.objectCamel}.${field.name}`);
|
|
722
|
+
return `<div>
|
|
723
|
+
<p className="mb-1 text-xs text-muted-foreground">${field.label || field.name}</p>
|
|
724
|
+
<p className="text-sm${field.name === getPrimaryTitleField(context) ? ' font-medium' : ''}">{${value}}</p>
|
|
725
|
+
</div>`;
|
|
726
|
+
}).join('\n');
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function getHeaderFieldCandidates(context) {
|
|
730
|
+
const preferred = [
|
|
731
|
+
...getPatternFieldPriority(context, 'header'),
|
|
732
|
+
'leadCode', 'customerCode', 'opportunityCode', 'quoteCode', 'materialCode',
|
|
733
|
+
'ownerName', 'phone', 'source', 'stage', 'materialType', 'status',
|
|
734
|
+
];
|
|
735
|
+
const ordered = [];
|
|
736
|
+
for (const name of preferred) {
|
|
737
|
+
const field = context.fields.find((item) => item.name === name);
|
|
738
|
+
if (field) {
|
|
739
|
+
ordered.push(field);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
for (const field of context.fields) {
|
|
743
|
+
if (!ordered.some((item) => item.name === field.name)) {
|
|
744
|
+
ordered.push(field);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return ordered;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function renderAdminDefaultValue(field) {
|
|
751
|
+
if (field.tsType === 'boolean') {
|
|
752
|
+
return field.default === undefined ? 'false' : String(Boolean(field.default));
|
|
753
|
+
}
|
|
754
|
+
if (field.tsType === 'number') {
|
|
755
|
+
return field.default === undefined ? '""' : JSON.stringify(String(field.default));
|
|
756
|
+
}
|
|
757
|
+
if (field.default !== undefined) {
|
|
758
|
+
return JSON.stringify(String(field.default));
|
|
759
|
+
}
|
|
760
|
+
return '""';
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function renderFormStateType(field) {
|
|
764
|
+
if (field.tsType === 'boolean') {
|
|
765
|
+
return 'boolean';
|
|
766
|
+
}
|
|
767
|
+
return 'string';
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function renderPayloadExpression(field, required) {
|
|
771
|
+
if (field.tsType === 'boolean') {
|
|
772
|
+
return required ? `Boolean(form.${field.name})` : `form.${field.name}`;
|
|
773
|
+
}
|
|
774
|
+
if (field.tsType === 'number') {
|
|
775
|
+
return required
|
|
776
|
+
? `Number(form.${field.name})`
|
|
777
|
+
: `(form.${field.name} === "" ? undefined : Number(form.${field.name}))`;
|
|
778
|
+
}
|
|
779
|
+
if (required) {
|
|
780
|
+
return `form.${field.name}.trim()`;
|
|
781
|
+
}
|
|
782
|
+
return `form.${field.name}.trim() || undefined`;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function renderEnumOptions(field) {
|
|
786
|
+
return (field.options || []).map((value) => `{
|
|
787
|
+
value: ${JSON.stringify(String(value))},
|
|
788
|
+
label: ${JSON.stringify(String(value))},
|
|
789
|
+
}`).join(',\n');
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function renderStatusMeta(context) {
|
|
793
|
+
const statusField = context.fields.find((field) => field.name === 'status');
|
|
794
|
+
const options = statusField && statusField.options ? statusField.options : ['draft', 'active', 'inactive'];
|
|
795
|
+
return options.map((value) => {
|
|
796
|
+
const meta = getStatusPreset(String(value));
|
|
797
|
+
return `${JSON.stringify(String(value))}: {
|
|
798
|
+
label: ${JSON.stringify(meta.label)},
|
|
799
|
+
badgeClassName: ${JSON.stringify(meta.badgeClassName)},
|
|
800
|
+
dotClassName: ${JSON.stringify(meta.dotClassName)},
|
|
801
|
+
objectPageColor: ${JSON.stringify(meta.objectPageColor)},
|
|
802
|
+
},`;
|
|
803
|
+
}).join('\n');
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function getStatusPreset(value) {
|
|
807
|
+
const key = String(value).toLowerCase();
|
|
808
|
+
if (['active', 'converted'].includes(key)) {
|
|
809
|
+
return { label: value, badgeClassName: 'bg-emerald-500/10 text-emerald-700', dotClassName: 'bg-emerald-500', objectPageColor: 'green' };
|
|
810
|
+
}
|
|
811
|
+
if (['inactive', 'closed', 'lost'].includes(key)) {
|
|
812
|
+
return { label: value, badgeClassName: 'bg-red-500/10 text-red-600', dotClassName: 'bg-red-500', objectPageColor: 'red' };
|
|
813
|
+
}
|
|
814
|
+
if (['following', 'proposal', 'qualifying'].includes(key)) {
|
|
815
|
+
return { label: value, badgeClassName: 'bg-blue-500/10 text-blue-700', dotClassName: 'bg-blue-500', objectPageColor: 'blue' };
|
|
816
|
+
}
|
|
817
|
+
return { label: value, badgeClassName: 'bg-muted text-muted-foreground', dotClassName: 'bg-muted-foreground', objectPageColor: 'gray' };
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function getChildEnumOptionConst(child, field) {
|
|
821
|
+
return `${child.className.toUpperCase()}_${field.name.toUpperCase()}_OPTIONS`;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function renderChildRowDefaultValue(field) {
|
|
825
|
+
if (field.tsType === 'boolean') {
|
|
826
|
+
return field.default === undefined ? 'false' : String(Boolean(field.default));
|
|
827
|
+
}
|
|
828
|
+
if (field.tsType === 'number') {
|
|
829
|
+
return field.default === undefined ? '""' : JSON.stringify(String(field.default));
|
|
830
|
+
}
|
|
831
|
+
if (field.default !== undefined) {
|
|
832
|
+
return JSON.stringify(String(field.default));
|
|
833
|
+
}
|
|
834
|
+
return '""';
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function renderChildFormAssignment(field, sourceExpr) {
|
|
838
|
+
if (field.tsType === 'boolean') {
|
|
839
|
+
return `Boolean(${sourceExpr})`;
|
|
840
|
+
}
|
|
841
|
+
if (field.tsType === 'number') {
|
|
842
|
+
return `${sourceExpr} === undefined || ${sourceExpr} === null ? ${renderChildRowDefaultValue(field)} : String(${sourceExpr})`;
|
|
843
|
+
}
|
|
844
|
+
if (field.tsType === 'Date') {
|
|
845
|
+
return `toFormDateValue(${sourceExpr})`;
|
|
846
|
+
}
|
|
847
|
+
return `${sourceExpr} ?? ${renderChildRowDefaultValue(field)}`;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function renderNormalizeChildRowFields(child) {
|
|
851
|
+
const hasPrimaryField = child.fields.some((field) => field.name === 'isPrimary');
|
|
852
|
+
const lines = child.fields.map((field) => {
|
|
853
|
+
if (field.tsType === 'boolean') {
|
|
854
|
+
if (field.name === 'isPrimary') {
|
|
855
|
+
return ` ${field.name}: list.some((item) => item.isPrimary) ? Boolean(row.${field.name}) : index === 0,`;
|
|
856
|
+
}
|
|
857
|
+
return ` ${field.name}: Boolean(row.${field.name}),`;
|
|
858
|
+
}
|
|
859
|
+
if (field.tsType === 'number') {
|
|
860
|
+
return ` ${field.name}: row.${field.name} === "" ? undefined : Number(row.${field.name}),`;
|
|
861
|
+
}
|
|
862
|
+
return ` ${field.name}: row.${field.name}?.trim() || undefined,`;
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
return ` return {\n${lines.join('\n')}\n };`;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function renderChildFilterCondition(child) {
|
|
869
|
+
const conditions = child.fields.map((field) => {
|
|
870
|
+
if (field.tsType === 'boolean') {
|
|
871
|
+
return `Boolean(row.${field.name})`;
|
|
872
|
+
}
|
|
873
|
+
if (field.tsType === 'number') {
|
|
874
|
+
return `row.${field.name} !== undefined && row.${field.name} !== null`;
|
|
875
|
+
}
|
|
876
|
+
return `Boolean(row.${field.name})`;
|
|
877
|
+
});
|
|
878
|
+
return conditions.join(' || ') || 'true';
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function renderChildValidation(child) {
|
|
882
|
+
const requiredFields = child.fields.filter((field) => field.required);
|
|
883
|
+
if (requiredFields.length === 0) {
|
|
884
|
+
return '';
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const hasAnyValue = child.fields.map((field) => {
|
|
888
|
+
if (field.tsType === 'boolean') {
|
|
889
|
+
return `Boolean(row.${field.name})`;
|
|
890
|
+
}
|
|
891
|
+
if (field.tsType === 'number') {
|
|
892
|
+
return `row.${field.name} !== "" && row.${field.name} !== undefined && row.${field.name} !== null`;
|
|
893
|
+
}
|
|
894
|
+
return `Boolean(row.${field.name}?.trim())`;
|
|
895
|
+
}).join(' || ');
|
|
896
|
+
|
|
897
|
+
const missingRequired = requiredFields.map((field) => {
|
|
898
|
+
if (field.tsType === 'boolean') {
|
|
899
|
+
return `row.${field.name} === undefined || row.${field.name} === null`;
|
|
900
|
+
}
|
|
901
|
+
if (field.tsType === 'number') {
|
|
902
|
+
return `row.${field.name} === "" || row.${field.name} === undefined || row.${field.name} === null`;
|
|
903
|
+
}
|
|
904
|
+
return `!row.${field.name}?.trim()`;
|
|
905
|
+
}).join(' || ');
|
|
906
|
+
|
|
907
|
+
return ` const invalid${child.className} = form.${child.variableName}.find((row) => {
|
|
908
|
+
const hasAnyValue = ${hasAnyValue};
|
|
909
|
+
return hasAnyValue && (${missingRequired});
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
if (invalid${child.className}) {
|
|
913
|
+
return "${child.label || child.name}${requiredFields[0].label || requiredFields[0].name}不能为空";
|
|
914
|
+
}`;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function findFieldByPriority(context, names) {
|
|
918
|
+
for (const name of names) {
|
|
919
|
+
const field = context.fields.find((item) => item.name === name);
|
|
920
|
+
if (field) {
|
|
921
|
+
return field;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
function renderAdminUiHelper(context) {
|
|
929
|
+
const enumFields = context.fields.filter((field) => field.type === 'enum');
|
|
930
|
+
const childEnumFields = context.childTables.flatMap((child) =>
|
|
931
|
+
child.fields
|
|
932
|
+
.filter((field) => field.type === 'enum')
|
|
933
|
+
.map((field) => ({ child, field }))
|
|
934
|
+
);
|
|
935
|
+
const childFormRowTypes = context.childTables.map((child) => `export interface ${child.className}FormRow {\n rowId: string;\n${child.fields.map((field) => ` ${field.name}: ${renderFormStateType(field)};`).join('\n')}\n}`).join('\n\n');
|
|
936
|
+
const defaultFieldLines = context.fields.map((field) => ` ${field.name}: ${renderAdminDefaultValue(field)},`).join('\n');
|
|
937
|
+
const childDefaultLines = context.childTables.map((child) => ` ${child.variableName}: [],`).join('\n');
|
|
938
|
+
const formStateLines = [
|
|
939
|
+
...context.fields.map((field) => ` ${field.name}: ${renderFormStateType(field)};`),
|
|
940
|
+
...context.childTables.map((child) => ` ${child.variableName}: ${child.className}FormRow[];`),
|
|
941
|
+
].join('\n');
|
|
942
|
+
const formStateAssignments = context.fields.map((field) => ` ${field.name}: ${renderRootFormAssignment(field, `${context.objectCamel}.${field.name}`, `form.${field.name}`)},`).join('\n');
|
|
943
|
+
const childAssignments = context.childTables.map((child) => ` ${child.variableName}: (${context.objectCamel}.${child.variableName} ?? []).map((row) =>
|
|
944
|
+
createEmpty${child.className}Row({
|
|
945
|
+
${indent(child.fields.map((field) => ` ${field.name}: ${renderChildFormAssignment(field, `row.${field.name}`)},`).join('\n'), 0)}
|
|
946
|
+
})
|
|
947
|
+
),`).join('\n');
|
|
948
|
+
const createPayloadLines = context.fields.map((field) => ` ${field.name}: ${renderPayloadExpression(field, true)},`).join('\n');
|
|
949
|
+
const childCreateLines = context.childTables.map((child) => ` ${child.variableName}: normalize${child.className}Rows(form.${child.variableName}),`).join('\n');
|
|
950
|
+
const updatePayloadLines = context.fields.map((field) => ` ${field.name}: ${renderPayloadExpression(field, false)},`).join('\n');
|
|
951
|
+
const childUpdateLines = context.childTables.map((child) => ` ${child.variableName}: normalize${child.className}Rows(form.${child.variableName}),`).join('\n');
|
|
952
|
+
const validations = context.fields.filter((field) => field.required).map((field) => ` if (!form.${field.name}.trim()) {\n return "${field.label || field.name}不能为空";\n }`).join('\n');
|
|
953
|
+
const childValidations = context.childTables.map((child) => renderChildValidation(child)).filter(Boolean).join('\n\n');
|
|
954
|
+
const statusMeta = context.fields.some((field) => field.name === 'status')
|
|
955
|
+
? `\nexport interface ${context.objectPascal}StatusMeta {\n label: string;\n badgeClassName: string;\n dotClassName: string;\n objectPageColor: "blue" | "green" | "yellow" | "red" | "gray";\n}\n\nconst ${context.objectPascal.toUpperCase()}_STATUS_META: Record<string, ${context.objectPascal}StatusMeta> = {\n${indent(renderStatusMeta(context), 2)}\n};\n`
|
|
956
|
+
: '';
|
|
957
|
+
const statusGetter = context.fields.some((field) => field.name === 'status')
|
|
958
|
+
? `\nexport function get${context.objectPascal}StatusMeta(status?: string): ${context.objectPascal}StatusMeta {\n if (!status) {\n return {\n label: "未设置",\n badgeClassName: "bg-muted text-muted-foreground",\n dotClassName: "bg-muted-foreground",\n objectPageColor: "gray",\n };\n }\n\n return ${context.objectPascal.toUpperCase()}_STATUS_META[status] ?? {\n label: status,\n badgeClassName: "bg-primary/10 text-primary",\n dotClassName: "bg-primary",\n objectPageColor: "blue",\n };\n}\n`
|
|
959
|
+
: '';
|
|
960
|
+
|
|
961
|
+
const childTypeImports = context.childTables.map((child) => `${child.className}Input`).join(', ');
|
|
962
|
+
const dtoTypeImports = [`Create${context.objectPascal}Dto`, `${context.objectPascal}Vo`, `Update${context.objectPascal}Dto`];
|
|
963
|
+
if (childTypeImports) {
|
|
964
|
+
dtoTypeImports.splice(1, 0, childTypeImports);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
return `import { ${context.objectPascal}Api, type ${dtoTypeImports.join(', type ')} } from "@scaffold/api/client";
|
|
968
|
+
|
|
969
|
+
export interface ${context.objectPascal}PageResult {
|
|
970
|
+
records: ${context.objectPascal}Vo[];
|
|
971
|
+
total: number;
|
|
972
|
+
pageNo: number;
|
|
973
|
+
pageSize: number;
|
|
974
|
+
totalPages: number;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
export interface ${context.objectPascal}Filters {
|
|
978
|
+
${context.fields.filter((field) => ((context.ui.list && context.ui.list.filters) || []).includes(field.name)).map((field) => ` ${field.name}: string;`).join('\n')}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
${childFormRowTypes ? `${childFormRowTypes}\n\n` : ''}export interface ${context.objectPascal}FormState {
|
|
982
|
+
${formStateLines}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
export interface OptionItem {
|
|
986
|
+
value: string;
|
|
987
|
+
label: string;
|
|
988
|
+
}
|
|
989
|
+
${statusMeta}
|
|
990
|
+
${enumFields.map((field) => `export const ${field.name.toUpperCase()}_OPTIONS: OptionItem[] = [\n${indent(renderEnumOptions(field), 2)}\n];`).join('\n\n')}${childEnumFields.length ? '\n\n' : ''}${childEnumFields.map(({ child, field }) => `export const ${getChildEnumOptionConst(child, field)}: OptionItem[] = [\n${indent(renderEnumOptions(field), 2)}\n];`).join('\n\n')}
|
|
991
|
+
|
|
992
|
+
const DEFAULT_API_BASE_URL = "http://localhost:3001";
|
|
993
|
+
const apiBaseUrl = (import.meta.env.VITE_API_URL || DEFAULT_API_BASE_URL).replace(/\\/$/, "");
|
|
994
|
+
|
|
995
|
+
export function create${context.objectPascal}Api() {
|
|
996
|
+
return new ${context.objectPascal}Api(apiBaseUrl);
|
|
997
|
+
}
|
|
998
|
+
${statusGetter}
|
|
999
|
+
|
|
1000
|
+
export function getOptionLabel(options: OptionItem[], value?: string) {
|
|
1001
|
+
if (!value) {
|
|
1002
|
+
return "-";
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
return options.find((item) => item.value === value)?.label ?? value;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
export function createEmpty${context.objectPascal}Form(): ${context.objectPascal}FormState {
|
|
1009
|
+
return {
|
|
1010
|
+
${defaultFieldLines}
|
|
1011
|
+
${childDefaultLines ? `${childDefaultLines}\n` : ''} };
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
${context.childTables.map((child) => `export function createEmpty${child.className}Row(overrides?: Partial<${child.className}FormRow>): ${child.className}FormRow {
|
|
1015
|
+
return {
|
|
1016
|
+
rowId: crypto.randomUUID(),
|
|
1017
|
+
${child.fields.map((field) => ` ${field.name}: ${renderChildRowDefaultValue(field)},`).join('\n')}
|
|
1018
|
+
...overrides,
|
|
1019
|
+
};
|
|
1020
|
+
}`).join('\n\n')}
|
|
1021
|
+
|
|
1022
|
+
export function to${context.objectPascal}FormState(${context.objectCamel}?: Partial<${context.objectPascal}Vo> | null): ${context.objectPascal}FormState {
|
|
1023
|
+
const form = createEmpty${context.objectPascal}Form();
|
|
1024
|
+
if (!${context.objectCamel}) {
|
|
1025
|
+
return form;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
return {
|
|
1029
|
+
${formStateAssignments}
|
|
1030
|
+
${childAssignments ? `${childAssignments}\n` : ''} };
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
export function toCreate${context.objectPascal}Payload(form: ${context.objectPascal}FormState): Create${context.objectPascal}Dto {
|
|
1034
|
+
return {
|
|
1035
|
+
${createPayloadLines}
|
|
1036
|
+
${childCreateLines ? `${childCreateLines}\n` : ''} };
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
export function toUpdate${context.objectPascal}Payload(form: ${context.objectPascal}FormState): Update${context.objectPascal}Dto {
|
|
1040
|
+
return {
|
|
1041
|
+
${updatePayloadLines}
|
|
1042
|
+
${childUpdateLines ? `${childUpdateLines}\n` : ''} };
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
export function validate${context.objectPascal}Form(form: ${context.objectPascal}FormState): string | null {
|
|
1046
|
+
${validations || ' return null;'}
|
|
1047
|
+
${childValidations ? `\n${childValidations}` : ''}
|
|
1048
|
+
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
${context.childTables.map((child) => `export function normalize${child.className}Rows(rows: ${child.className}FormRow[]): ${child.className}Input[] {
|
|
1053
|
+
const normalized = rows
|
|
1054
|
+
.map((row${child.fields.some((field) => field.name === 'isPrimary') ? ', index, list' : ''}) => {
|
|
1055
|
+
${indent(renderNormalizeChildRowFields(child), 6)}
|
|
1056
|
+
})
|
|
1057
|
+
.filter((row) => ${renderChildFilterCondition(child)});
|
|
1058
|
+
${child.fields.some((field) => field.name === 'isPrimary') ? `
|
|
1059
|
+
|
|
1060
|
+
if (normalized.length > 0 && !normalized.some((row) => row.isPrimary)) {
|
|
1061
|
+
normalized[0] = {
|
|
1062
|
+
...normalized[0],
|
|
1063
|
+
isPrimary: true,
|
|
1064
|
+
};
|
|
1065
|
+
}` : ''}
|
|
1066
|
+
|
|
1067
|
+
return normalized;
|
|
1068
|
+
}`).join('\n\n')}
|
|
1069
|
+
|
|
1070
|
+
export function formatDateTime(value?: string | Date) {
|
|
1071
|
+
if (!value) {
|
|
1072
|
+
return "-";
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
1076
|
+
if (Number.isNaN(date.getTime())) {
|
|
1077
|
+
return String(value);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
return date.toLocaleString("zh-CN", {
|
|
1081
|
+
year: "numeric",
|
|
1082
|
+
month: "2-digit",
|
|
1083
|
+
day: "2-digit",
|
|
1084
|
+
hour: "2-digit",
|
|
1085
|
+
minute: "2-digit",
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
export function toFormDateValue(value?: string | Date | null) {
|
|
1090
|
+
if (!value) {
|
|
1091
|
+
return "";
|
|
1092
|
+
}
|
|
1093
|
+
if (typeof value === "string") {
|
|
1094
|
+
return value;
|
|
1095
|
+
}
|
|
1096
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
1097
|
+
return value.toISOString().slice(0, 10);
|
|
1098
|
+
}
|
|
1099
|
+
return String(value);
|
|
1100
|
+
}
|
|
1101
|
+
`;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function renderRootFormAssignment(field, sourceExpr, fallbackExpr) {
|
|
1105
|
+
if (field.tsType === 'boolean') {
|
|
1106
|
+
return `Boolean(${sourceExpr})`;
|
|
1107
|
+
}
|
|
1108
|
+
if (field.tsType === 'number') {
|
|
1109
|
+
return `${sourceExpr} === undefined || ${sourceExpr} === null ? ${fallbackExpr} : String(${sourceExpr})`;
|
|
1110
|
+
}
|
|
1111
|
+
if (field.tsType === 'Date') {
|
|
1112
|
+
return `toFormDateValue(${sourceExpr}) || ${fallbackExpr}`;
|
|
1113
|
+
}
|
|
1114
|
+
return `${sourceExpr} ?? ${fallbackExpr}`;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function renderDisplayValue(field, sourceExpr, fallbackText = '"-"') {
|
|
1118
|
+
if (field.tsType === 'Date' || field.name === 'createdAt' || field.name === 'updatedAt') {
|
|
1119
|
+
return `formatDateTime(${sourceExpr})`;
|
|
1120
|
+
}
|
|
1121
|
+
if (field.tsType === 'number') {
|
|
1122
|
+
return `${sourceExpr} === undefined || ${sourceExpr} === null ? ${fallbackText} : String(${sourceExpr})`;
|
|
1123
|
+
}
|
|
1124
|
+
return `${sourceExpr} || ${fallbackText}`;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function resolveInputPath(filePath) {
|
|
1128
|
+
if (path.isAbsolute(filePath)) {
|
|
1129
|
+
return filePath;
|
|
1130
|
+
}
|
|
1131
|
+
const normalized = String(filePath).replace(/\\/g, '/');
|
|
1132
|
+
if (normalized.startsWith('scaffold/')) {
|
|
1133
|
+
return path.resolve(process.cwd(), normalized.slice('scaffold/'.length));
|
|
1134
|
+
}
|
|
1135
|
+
if (normalized.startsWith('specs/')) {
|
|
1136
|
+
return resolveScaffoldPath(...normalized.split('/'));
|
|
1137
|
+
}
|
|
1138
|
+
return path.resolve(filePath);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function renderEntityField(field) {
|
|
1142
|
+
const optionalFlag = field.required ? '!' : '?';
|
|
1143
|
+
return `@TableField({ column: '${field.column}' })
|
|
1144
|
+
${field.name}${optionalFlag}: ${normalizeEntityType(field.tsType)};`;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
function renderDtoField(field, required) {
|
|
1148
|
+
if (required) {
|
|
1149
|
+
return ` ${field.name}!: ${normalizeDtoType(field.tsType)};`;
|
|
1150
|
+
}
|
|
1151
|
+
return ` ${field.name}?: ${normalizeDtoType(field.tsType)};`;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function normalizeEntityType(type) {
|
|
1155
|
+
if (type === 'Date') {
|
|
1156
|
+
return 'Date';
|
|
1157
|
+
}
|
|
1158
|
+
return type;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function normalizeDtoType(type) {
|
|
1162
|
+
if (type === 'Date') {
|
|
1163
|
+
return 'string | Date';
|
|
1164
|
+
}
|
|
1165
|
+
return type;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function renderColumn(field, indentSize) {
|
|
1169
|
+
const modifiers = [];
|
|
1170
|
+
if (field.required) {
|
|
1171
|
+
modifiers.push('notNull()');
|
|
1172
|
+
}
|
|
1173
|
+
if (field.unique) {
|
|
1174
|
+
modifiers.push('unique()');
|
|
1175
|
+
}
|
|
1176
|
+
if (field.default !== undefined) {
|
|
1177
|
+
modifiers.push(`defaultTo(${JSON.stringify(field.default)})`);
|
|
1178
|
+
}
|
|
1179
|
+
const modifierText = modifiers.length > 0
|
|
1180
|
+
? `, (col: any) => col.${modifiers.join('.')}`
|
|
1181
|
+
: '';
|
|
1182
|
+
return `${' '.repeat(indentSize)}.addColumn('${field.column}', '${field.sqlType}'${modifierText})`;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function renderFindCreatedEntity(context) {
|
|
1186
|
+
const uniqueField = context.fields.find((field) => field.unique);
|
|
1187
|
+
if (uniqueField) {
|
|
1188
|
+
return ` const entity = await this.${context.objectCamel}Mapper.selectOne({ ${uniqueField.name}: dto.${uniqueField.name} } as any);
|
|
1189
|
+
return entity ? this.normalizeRootEntity(entity) : null;`;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
return ` const list = await this.${context.objectCamel}Mapper.selectList({}, [{ field: 'id', direction: 'desc' }]);
|
|
1193
|
+
if (list.length === 0) {
|
|
1194
|
+
return null;
|
|
1195
|
+
}
|
|
1196
|
+
return this.normalizeRootEntity(list[0]);`;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function renderPersistExpression(field, sourceExpr) {
|
|
1200
|
+
if (field.tsType === 'boolean') {
|
|
1201
|
+
return `${sourceExpr} ? 1 : 0`;
|
|
1202
|
+
}
|
|
1203
|
+
return sourceExpr;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function renderReadExpression(field, sourceExpr) {
|
|
1207
|
+
if (field.tsType === 'boolean') {
|
|
1208
|
+
return `Boolean(${sourceExpr})`;
|
|
1209
|
+
}
|
|
1210
|
+
return sourceExpr;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function indent(text, size) {
|
|
1214
|
+
return String(text)
|
|
1215
|
+
.split('\n')
|
|
1216
|
+
.map((line) => (line ? `${' '.repeat(size)}${line}` : line))
|
|
1217
|
+
.join('\n');
|
|
1218
|
+
}
|