@currentjs/gen 0.2.2 → 0.3.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/CHANGELOG.md +80 -0
- package/README.md +256 -0
- package/dist/cli.js +26 -0
- package/dist/commands/createApp.js +2 -0
- package/dist/commands/generateAll.js +153 -29
- package/dist/commands/migrateCommit.d.ts +1 -0
- package/dist/commands/migrateCommit.js +201 -0
- package/dist/generators/controllerGenerator.d.ts +7 -0
- package/dist/generators/controllerGenerator.js +60 -29
- package/dist/generators/domainModelGenerator.d.ts +7 -0
- package/dist/generators/domainModelGenerator.js +57 -3
- package/dist/generators/serviceGenerator.d.ts +16 -1
- package/dist/generators/serviceGenerator.js +125 -12
- package/dist/generators/storeGenerator.d.ts +8 -0
- package/dist/generators/storeGenerator.js +133 -7
- package/dist/generators/templateGenerator.d.ts +19 -0
- package/dist/generators/templateGenerator.js +216 -11
- package/dist/generators/templates/appTemplates.d.ts +8 -7
- package/dist/generators/templates/appTemplates.js +11 -1572
- package/dist/generators/templates/data/appTsTemplate +39 -0
- package/dist/generators/templates/data/appYamlTemplate +4 -0
- package/dist/generators/templates/data/cursorRulesTemplate +671 -0
- package/dist/generators/templates/data/errorTemplate +28 -0
- package/dist/generators/templates/data/frontendScriptTemplate +739 -0
- package/dist/generators/templates/data/mainViewTemplate +16 -0
- package/dist/generators/templates/data/translationsTemplate +68 -0
- package/dist/generators/templates/data/tsConfigTemplate +19 -0
- package/dist/generators/templates/viewTemplates.d.ts +10 -1
- package/dist/generators/templates/viewTemplates.js +138 -6
- package/dist/generators/validationGenerator.d.ts +5 -0
- package/dist/generators/validationGenerator.js +51 -0
- package/dist/utils/constants.d.ts +3 -0
- package/dist/utils/constants.js +5 -2
- package/dist/utils/migrationUtils.d.ts +49 -0
- package/dist/utils/migrationUtils.js +291 -0
- package/howto.md +157 -65
- package/package.json +3 -2
|
@@ -50,6 +50,7 @@ class DomainModelGenerator {
|
|
|
50
50
|
array: 'any[]',
|
|
51
51
|
object: 'object'
|
|
52
52
|
};
|
|
53
|
+
this.availableModels = new Set();
|
|
53
54
|
}
|
|
54
55
|
getDefaultValue(type) {
|
|
55
56
|
switch (type) {
|
|
@@ -71,8 +72,27 @@ class DomainModelGenerator {
|
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
mapType(yamlType) {
|
|
75
|
+
// Check if this is a known model (relationship)
|
|
76
|
+
if (this.availableModels.has(yamlType)) {
|
|
77
|
+
return yamlType;
|
|
78
|
+
}
|
|
74
79
|
return this.typeMapping[yamlType] || 'any';
|
|
75
80
|
}
|
|
81
|
+
setAvailableModels(models) {
|
|
82
|
+
this.availableModels.clear();
|
|
83
|
+
models.forEach(model => {
|
|
84
|
+
this.availableModels.add(model.name);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
getRelatedModelImports(modelConfig) {
|
|
88
|
+
const imports = [];
|
|
89
|
+
modelConfig.fields.forEach(field => {
|
|
90
|
+
if (this.availableModels.has(field.type) && field.type !== modelConfig.name) {
|
|
91
|
+
imports.push(`import { ${field.type} } from './${field.type}';`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
return imports;
|
|
95
|
+
}
|
|
76
96
|
generateConstructorParameter(field) {
|
|
77
97
|
const tsType = this.mapType(field.type);
|
|
78
98
|
const isOptional = !field.required && !field.auto;
|
|
@@ -87,14 +107,34 @@ class DomainModelGenerator {
|
|
|
87
107
|
}
|
|
88
108
|
return param;
|
|
89
109
|
}
|
|
110
|
+
isRelationshipField(field) {
|
|
111
|
+
return this.availableModels.has(field.type);
|
|
112
|
+
}
|
|
113
|
+
getForeignKeyFieldName(field) {
|
|
114
|
+
// Convention: fieldName + 'Id' (e.g., owner -> ownerId)
|
|
115
|
+
return field.name + 'Id';
|
|
116
|
+
}
|
|
117
|
+
generateForeignKeyParameter(field) {
|
|
118
|
+
const foreignKeyName = this.getForeignKeyFieldName(field);
|
|
119
|
+
const isOptional = !field.required && !field.auto;
|
|
120
|
+
let param = `public ${foreignKeyName}`;
|
|
121
|
+
if (isOptional) {
|
|
122
|
+
param += '?';
|
|
123
|
+
}
|
|
124
|
+
param += ': number';
|
|
125
|
+
return param;
|
|
126
|
+
}
|
|
90
127
|
generateSetterMethods(modelConfig) {
|
|
91
128
|
const setterMethods = [];
|
|
92
129
|
modelConfig.fields.forEach(field => {
|
|
93
130
|
if (!field.auto && field.name !== 'id') {
|
|
94
131
|
const tsType = this.mapType(field.type);
|
|
95
132
|
const methodName = `set${field.name.charAt(0).toUpperCase() + field.name.slice(1)}`;
|
|
133
|
+
// For all fields (including relationships), generate simple setter
|
|
134
|
+
// Domain model doesn't care about FKs - that's infrastructure concern
|
|
135
|
+
const isOptional = !field.required && !field.auto;
|
|
96
136
|
const setter = `
|
|
97
|
-
${methodName}(${field.name}: ${tsType}): void {
|
|
137
|
+
${methodName}(${field.name}: ${tsType}${isOptional ? ' | undefined' : ''}): void {
|
|
98
138
|
this.${field.name} = ${field.name};
|
|
99
139
|
}`;
|
|
100
140
|
setterMethods.push(setter);
|
|
@@ -121,11 +161,16 @@ class DomainModelGenerator {
|
|
|
121
161
|
const sortedFields = this.sortFieldsByRequired(modelConfig.fields);
|
|
122
162
|
// Process other fields
|
|
123
163
|
sortedFields.forEach(field => {
|
|
164
|
+
// For relationship fields, only add the relationship object (not FK)
|
|
165
|
+
// Domain model works with objects only - FK is infrastructure concern
|
|
124
166
|
constructorParams.push(this.generateConstructorParameter(field));
|
|
125
167
|
});
|
|
126
168
|
const constructorParamsStr = constructorParams.join(',\n ');
|
|
127
169
|
const setterMethods = this.generateSetterMethods(modelConfig);
|
|
128
|
-
|
|
170
|
+
// Generate imports for related models
|
|
171
|
+
const imports = this.getRelatedModelImports(modelConfig);
|
|
172
|
+
const importsStr = imports.length > 0 ? imports.join('\n') + '\n\n' : '';
|
|
173
|
+
return `${importsStr}export class ${className} {
|
|
129
174
|
public constructor(
|
|
130
175
|
${constructorParamsStr}
|
|
131
176
|
) { }
|
|
@@ -143,6 +188,8 @@ ${setterMethods}
|
|
|
143
188
|
const app = config;
|
|
144
189
|
Object.values(app.modules).forEach(moduleConfig => {
|
|
145
190
|
if (moduleConfig.models && moduleConfig.models.length > 0) {
|
|
191
|
+
// Set available models for relationship detection
|
|
192
|
+
this.setAvailableModels(moduleConfig.models);
|
|
146
193
|
moduleConfig.models.forEach(m => {
|
|
147
194
|
result[m.name] = this.generateModel(m);
|
|
148
195
|
});
|
|
@@ -151,6 +198,8 @@ ${setterMethods}
|
|
|
151
198
|
}
|
|
152
199
|
else if (config.models) {
|
|
153
200
|
const module = config;
|
|
201
|
+
// Set available models for relationship detection
|
|
202
|
+
this.setAvailableModels(module.models);
|
|
154
203
|
module.models.forEach(m => {
|
|
155
204
|
result[m.name] = this.generateModel(m);
|
|
156
205
|
});
|
|
@@ -162,6 +211,8 @@ ${setterMethods}
|
|
|
162
211
|
if (config.modules) {
|
|
163
212
|
Object.values(config.modules).forEach(moduleConfig => {
|
|
164
213
|
if (moduleConfig.models && moduleConfig.models.length > 0) {
|
|
214
|
+
// Set available models for relationship detection
|
|
215
|
+
this.setAvailableModels(moduleConfig.models);
|
|
165
216
|
moduleConfig.models.forEach(m => {
|
|
166
217
|
result[m.name] = this.generateModel(m);
|
|
167
218
|
});
|
|
@@ -169,7 +220,10 @@ ${setterMethods}
|
|
|
169
220
|
});
|
|
170
221
|
}
|
|
171
222
|
else if (config.models) {
|
|
172
|
-
|
|
223
|
+
const module = config;
|
|
224
|
+
// Set available models for relationship detection
|
|
225
|
+
this.setAvailableModels(module.models);
|
|
226
|
+
module.models.forEach(m => {
|
|
173
227
|
result[m.name] = this.generateModel(m);
|
|
174
228
|
});
|
|
175
229
|
}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
interface FieldConfig {
|
|
2
|
+
name: string;
|
|
3
|
+
type: string;
|
|
4
|
+
required?: boolean;
|
|
5
|
+
unique?: boolean;
|
|
6
|
+
auto?: boolean;
|
|
7
|
+
displayFields?: string[];
|
|
8
|
+
}
|
|
1
9
|
interface ActionConfig {
|
|
2
10
|
handlers: string[];
|
|
3
11
|
}
|
|
@@ -7,7 +15,7 @@ interface PermissionConfig {
|
|
|
7
15
|
}
|
|
8
16
|
interface ModelConfig {
|
|
9
17
|
name: string;
|
|
10
|
-
fields:
|
|
18
|
+
fields: FieldConfig[];
|
|
11
19
|
}
|
|
12
20
|
type ModuleConfig = {
|
|
13
21
|
models?: ModelConfig[];
|
|
@@ -15,6 +23,10 @@ type ModuleConfig = {
|
|
|
15
23
|
permissions?: PermissionConfig[];
|
|
16
24
|
};
|
|
17
25
|
export declare class ServiceGenerator {
|
|
26
|
+
private availableModels;
|
|
27
|
+
private setAvailableModels;
|
|
28
|
+
private isRelationshipField;
|
|
29
|
+
private getForeignKeyFieldName;
|
|
18
30
|
private hasPermissions;
|
|
19
31
|
private getActionPermissions;
|
|
20
32
|
private generatePermissionCheck;
|
|
@@ -26,12 +38,15 @@ export declare class ServiceGenerator {
|
|
|
26
38
|
private getMethodCallParams;
|
|
27
39
|
private sortFieldsByRequired;
|
|
28
40
|
private generateConstructorArgs;
|
|
41
|
+
private generateRelationshipLoading;
|
|
29
42
|
private generateUpdateSetterCalls;
|
|
30
43
|
private replaceTemplateVars;
|
|
31
44
|
private getServiceMethodName;
|
|
32
45
|
private generateHandlerMethod;
|
|
33
46
|
generateServiceForModel(model: ModelConfig, moduleName: string, moduleConfig: ModuleConfig, hasGlobalPermissions: boolean): string;
|
|
34
47
|
generateService(moduleName: string, moduleConfig: ModuleConfig, hasGlobalPermissions: boolean): string;
|
|
48
|
+
private generateForeignStoreImports;
|
|
49
|
+
private generateForeignStoreConstructorParams;
|
|
35
50
|
private generateCustomImports;
|
|
36
51
|
generateFromYamlFile(yamlFilePath: string): Record<string, string>;
|
|
37
52
|
generateAndSaveFiles(yamlFilePath?: string, outputDir?: string, opts?: {
|
|
@@ -42,6 +42,22 @@ const generationRegistry_1 = require("../utils/generationRegistry");
|
|
|
42
42
|
const colors_1 = require("../utils/colors");
|
|
43
43
|
const constants_1 = require("../utils/constants");
|
|
44
44
|
class ServiceGenerator {
|
|
45
|
+
constructor() {
|
|
46
|
+
this.availableModels = new Set();
|
|
47
|
+
}
|
|
48
|
+
setAvailableModels(models) {
|
|
49
|
+
this.availableModels.clear();
|
|
50
|
+
models.forEach(model => {
|
|
51
|
+
this.availableModels.add(model.name);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
isRelationshipField(field) {
|
|
55
|
+
return this.availableModels.has(field.type);
|
|
56
|
+
}
|
|
57
|
+
getForeignKeyFieldName(field) {
|
|
58
|
+
// Convention: fieldName + 'Id' (e.g., owner -> ownerId)
|
|
59
|
+
return field.name + 'Id';
|
|
60
|
+
}
|
|
45
61
|
hasPermissions(config) {
|
|
46
62
|
if (config.modules) {
|
|
47
63
|
return Object.values(config.modules).some(module => module.permissions && module.permissions.length > 0);
|
|
@@ -198,12 +214,17 @@ class ServiceGenerator {
|
|
|
198
214
|
.replace(/{{ENTITY_LOWER}}/g, entityLower);
|
|
199
215
|
// Handle constructor args for create action
|
|
200
216
|
if (actionName === 'create') {
|
|
217
|
+
const relationshipLoading = this.generateRelationshipLoading(moduleConfig, entityName);
|
|
201
218
|
const constructorArgs = this.generateConstructorArgs(moduleConfig, entityName);
|
|
219
|
+
if (relationshipLoading) {
|
|
220
|
+
// Insert relationship loading before the entity creation
|
|
221
|
+
processedTemplate = relationshipLoading + '\n ' + processedTemplate;
|
|
222
|
+
}
|
|
202
223
|
processedTemplate = processedTemplate.replace(/{{CONSTRUCTOR_ARGS}}/g, constructorArgs);
|
|
203
224
|
}
|
|
204
225
|
// Handle setter calls for update action
|
|
205
226
|
if (actionName === 'update') {
|
|
206
|
-
const setterCalls = this.generateUpdateSetterCalls(moduleConfig, entityName);
|
|
227
|
+
const setterCalls = this.generateUpdateSetterCalls(moduleConfig, entityName, true);
|
|
207
228
|
processedTemplate = processedTemplate.replace(/{{UPDATE_SETTER_CALLS}}/g, setterCalls);
|
|
208
229
|
}
|
|
209
230
|
// Special-case: list action with only owner role → fetch by userId
|
|
@@ -305,31 +326,88 @@ class ServiceGenerator {
|
|
|
305
326
|
return '';
|
|
306
327
|
}
|
|
307
328
|
// Find the correct model by entityName instead of always using first model
|
|
308
|
-
const model = moduleConfig.models.find(m => m.name === entityName) || moduleConfig.models[0];
|
|
329
|
+
const model = moduleConfig.models.find((m) => m.name === entityName) || moduleConfig.models[0];
|
|
309
330
|
const entityLower = entityName.toLowerCase();
|
|
310
331
|
// Sort fields to match the constructor parameter order
|
|
311
332
|
const sortedFields = this.sortFieldsByRequired(model.fields);
|
|
312
|
-
|
|
333
|
+
const args = [];
|
|
334
|
+
sortedFields
|
|
313
335
|
.filter(field => !field.auto && field.name !== 'id')
|
|
314
|
-
.
|
|
315
|
-
|
|
336
|
+
.forEach(field => {
|
|
337
|
+
// For relationship fields, reference the loaded object variable
|
|
338
|
+
if (this.isRelationshipField(field)) {
|
|
339
|
+
args.push(`${field.name}Object`);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
args.push(`${entityLower}Data.${field.name}`);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
return args.join(', ');
|
|
316
346
|
}
|
|
317
|
-
|
|
347
|
+
generateRelationshipLoading(moduleConfig, entityName) {
|
|
348
|
+
if (!moduleConfig.models || moduleConfig.models.length === 0) {
|
|
349
|
+
return '';
|
|
350
|
+
}
|
|
351
|
+
const model = moduleConfig.models.find((m) => m.name === entityName) || moduleConfig.models[0];
|
|
352
|
+
const entityLower = entityName.toLowerCase();
|
|
353
|
+
const relationshipFields = model.fields.filter(f => this.isRelationshipField(f));
|
|
354
|
+
if (relationshipFields.length === 0) {
|
|
355
|
+
return '';
|
|
356
|
+
}
|
|
357
|
+
const loadingCode = relationshipFields.map(field => {
|
|
358
|
+
const foreignKeyName = this.getForeignKeyFieldName(field);
|
|
359
|
+
const relatedModel = field.type;
|
|
360
|
+
const relatedModelLower = relatedModel.toLowerCase();
|
|
361
|
+
const varName = `${field.name}Object`;
|
|
362
|
+
if (field.required) {
|
|
363
|
+
return `const ${varName} = await this.${relatedModelLower}Store.getById(${entityLower}Data.${foreignKeyName});
|
|
364
|
+
if (!${varName}) {
|
|
365
|
+
throw new Error('${relatedModel} not found with id ' + ${entityLower}Data.${foreignKeyName});
|
|
366
|
+
}`;
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
return `const ${varName} = ${entityLower}Data.${foreignKeyName}
|
|
370
|
+
? await this.${relatedModelLower}Store.getById(${entityLower}Data.${foreignKeyName})
|
|
371
|
+
: null;`;
|
|
372
|
+
}
|
|
373
|
+
}).join('\n ');
|
|
374
|
+
return ' // Load relationship objects\n ' + loadingCode;
|
|
375
|
+
}
|
|
376
|
+
generateUpdateSetterCalls(moduleConfig, entityName, includeRelationshipLoading = false) {
|
|
318
377
|
if (!moduleConfig.models || moduleConfig.models.length === 0) {
|
|
319
378
|
return '';
|
|
320
379
|
}
|
|
321
380
|
// Find the correct model by entityName instead of always using first model
|
|
322
381
|
const model = moduleConfig.models.find(m => m.name === entityName) || moduleConfig.models[0];
|
|
323
382
|
const entityLower = entityName.toLowerCase();
|
|
324
|
-
|
|
383
|
+
let code = '';
|
|
384
|
+
// Add relationship loading if requested
|
|
385
|
+
if (includeRelationshipLoading) {
|
|
386
|
+
const relationshipLoading = this.generateRelationshipLoading(moduleConfig, entityName);
|
|
387
|
+
if (relationshipLoading) {
|
|
388
|
+
code = relationshipLoading + '\n ';
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const setterCalls = model.fields
|
|
325
392
|
.filter(field => !field.auto && field.name !== 'id')
|
|
326
393
|
.map(field => {
|
|
327
|
-
|
|
328
|
-
|
|
394
|
+
// For relationship fields, set the loaded object
|
|
395
|
+
if (this.isRelationshipField(field)) {
|
|
396
|
+
const foreignKeyName = this.getForeignKeyFieldName(field);
|
|
397
|
+
const methodName = `set${field.name.charAt(0).toUpperCase() + field.name.slice(1)}`;
|
|
398
|
+
return `if (${entityLower}Data.${foreignKeyName} !== undefined) {
|
|
399
|
+
existing${entityName}.${methodName}(${field.name}Object);
|
|
400
|
+
}`;
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
const methodName = `set${field.name.charAt(0).toUpperCase() + field.name.slice(1)}`;
|
|
404
|
+
return `if (${entityLower}Data.${field.name} !== undefined) {
|
|
329
405
|
existing${entityName}.${methodName}(${entityLower}Data.${field.name});
|
|
330
406
|
}`;
|
|
407
|
+
}
|
|
331
408
|
})
|
|
332
409
|
.join('\n ');
|
|
410
|
+
return code + setterCalls;
|
|
333
411
|
}
|
|
334
412
|
replaceTemplateVars(template, variables) {
|
|
335
413
|
let result = template;
|
|
@@ -407,12 +485,17 @@ class ServiceGenerator {
|
|
|
407
485
|
.replace(/{{ENTITY_LOWER}}/g, entityLower);
|
|
408
486
|
// Handle constructor args for create action
|
|
409
487
|
if (actionName === 'create') {
|
|
488
|
+
const relationshipLoading = this.generateRelationshipLoading(moduleConfig, entityName);
|
|
410
489
|
const constructorArgs = this.generateConstructorArgs(moduleConfig, entityName);
|
|
490
|
+
if (relationshipLoading) {
|
|
491
|
+
// Insert relationship loading before the entity creation
|
|
492
|
+
methodImplementation = relationshipLoading + '\n ' + methodImplementation;
|
|
493
|
+
}
|
|
411
494
|
methodImplementation = methodImplementation.replace(/{{CONSTRUCTOR_ARGS}}/g, constructorArgs);
|
|
412
495
|
}
|
|
413
496
|
// Handle setter calls for update action
|
|
414
497
|
if (actionName === 'update') {
|
|
415
|
-
const setterCalls = this.generateUpdateSetterCalls(moduleConfig, entityName);
|
|
498
|
+
const setterCalls = this.generateUpdateSetterCalls(moduleConfig, entityName, true);
|
|
416
499
|
methodImplementation = methodImplementation.replace(/{{UPDATE_SETTER_CALLS}}/g, setterCalls);
|
|
417
500
|
}
|
|
418
501
|
// Special-case: list action with only owner role → fetch by userId
|
|
@@ -478,17 +561,20 @@ class ServiceGenerator {
|
|
|
478
561
|
})
|
|
479
562
|
.filter(method => method) // Remove empty methods
|
|
480
563
|
.join('\n\n');
|
|
564
|
+
// Add foreign store constructor params
|
|
565
|
+
const foreignStoreParams = this.generateForeignStoreConstructorParams(model);
|
|
481
566
|
const serviceClass = this.replaceTemplateVars(serviceTemplates_1.serviceTemplates.serviceClass, {
|
|
482
567
|
ENTITY_NAME: entityName,
|
|
483
568
|
ENTITY_LOWER: entityLower,
|
|
484
|
-
AUTH_SERVICE_PARAM:
|
|
569
|
+
AUTH_SERVICE_PARAM: foreignStoreParams,
|
|
485
570
|
SERVICE_METHODS: serviceMethods
|
|
486
571
|
});
|
|
487
572
|
const customImports = this.generateCustomImports(moduleConfig);
|
|
573
|
+
const foreignStoreImports = this.generateForeignStoreImports(model);
|
|
488
574
|
return this.replaceTemplateVars(serviceTemplates_1.serviceFileTemplate, {
|
|
489
575
|
ENTITY_NAME: entityName,
|
|
490
576
|
PERMISSIONS_IMPORT: hasPermissions ? "\nimport type { AuthenticatedUser } from '@currentjs/router';" : '',
|
|
491
|
-
CUSTOM_IMPORTS: customImports,
|
|
577
|
+
CUSTOM_IMPORTS: customImports + foreignStoreImports,
|
|
492
578
|
SERVICE_CLASS: serviceClass
|
|
493
579
|
});
|
|
494
580
|
}
|
|
@@ -499,6 +585,29 @@ class ServiceGenerator {
|
|
|
499
585
|
}
|
|
500
586
|
return this.generateServiceForModel(moduleConfig.models[0], moduleName, moduleConfig, hasGlobalPermissions);
|
|
501
587
|
}
|
|
588
|
+
generateForeignStoreImports(model) {
|
|
589
|
+
const relationshipFields = model.fields.filter(f => this.isRelationshipField(f));
|
|
590
|
+
if (relationshipFields.length === 0) {
|
|
591
|
+
return '';
|
|
592
|
+
}
|
|
593
|
+
const imports = relationshipFields.map(field => {
|
|
594
|
+
const relatedModel = field.type;
|
|
595
|
+
return `import { ${relatedModel}Store } from '../../infrastructure/stores/${relatedModel}Store';`;
|
|
596
|
+
});
|
|
597
|
+
return '\n' + imports.join('\n');
|
|
598
|
+
}
|
|
599
|
+
generateForeignStoreConstructorParams(model) {
|
|
600
|
+
const relationshipFields = model.fields.filter(f => this.isRelationshipField(f));
|
|
601
|
+
if (relationshipFields.length === 0) {
|
|
602
|
+
return '';
|
|
603
|
+
}
|
|
604
|
+
const params = relationshipFields.map(field => {
|
|
605
|
+
const relatedModel = field.type;
|
|
606
|
+
const relatedModelLower = relatedModel.toLowerCase();
|
|
607
|
+
return `,\n private ${relatedModelLower}Store: ${relatedModel}Store`;
|
|
608
|
+
});
|
|
609
|
+
return params.join('');
|
|
610
|
+
}
|
|
502
611
|
generateCustomImports(moduleConfig) {
|
|
503
612
|
if (!moduleConfig.actions)
|
|
504
613
|
return '';
|
|
@@ -524,6 +633,8 @@ class ServiceGenerator {
|
|
|
524
633
|
if (config.modules) {
|
|
525
634
|
Object.entries(config.modules).forEach(([moduleName, moduleConfig]) => {
|
|
526
635
|
if (moduleConfig.models && moduleConfig.models.length > 0) {
|
|
636
|
+
// Set available models for relationship detection
|
|
637
|
+
this.setAvailableModels(moduleConfig.models);
|
|
527
638
|
// Generate a service for each model
|
|
528
639
|
moduleConfig.models.forEach(model => {
|
|
529
640
|
const serviceCode = this.generateServiceForModel(model, moduleName, moduleConfig, hasGlobalPermissions);
|
|
@@ -538,6 +649,8 @@ class ServiceGenerator {
|
|
|
538
649
|
const moduleName = 'Module';
|
|
539
650
|
const moduleConfig = config;
|
|
540
651
|
if (moduleConfig.models && moduleConfig.models.length > 0) {
|
|
652
|
+
// Set available models for relationship detection
|
|
653
|
+
this.setAvailableModels(moduleConfig.models);
|
|
541
654
|
// Generate a service for each model
|
|
542
655
|
moduleConfig.models.forEach(model => {
|
|
543
656
|
const serviceCode = this.generateServiceForModel(model, moduleName, moduleConfig, hasGlobalPermissions);
|
|
@@ -4,6 +4,7 @@ interface FieldConfig {
|
|
|
4
4
|
required?: boolean;
|
|
5
5
|
unique?: boolean;
|
|
6
6
|
auto?: boolean;
|
|
7
|
+
displayFields?: string[];
|
|
7
8
|
}
|
|
8
9
|
interface ModelConfig {
|
|
9
10
|
name: string;
|
|
@@ -11,7 +12,11 @@ interface ModelConfig {
|
|
|
11
12
|
}
|
|
12
13
|
export declare class StoreGenerator {
|
|
13
14
|
private typeMapping;
|
|
15
|
+
private availableModels;
|
|
14
16
|
private mapType;
|
|
17
|
+
private setAvailableModels;
|
|
18
|
+
private isRelationshipField;
|
|
19
|
+
private getForeignKeyFieldName;
|
|
15
20
|
private generateRowFields;
|
|
16
21
|
private generateFilterableFields;
|
|
17
22
|
private generateFilterableFieldsArray;
|
|
@@ -21,6 +26,9 @@ export declare class StoreGenerator {
|
|
|
21
26
|
private generateModelToRowMapping;
|
|
22
27
|
private replaceTemplateVars;
|
|
23
28
|
generateStoreInterface(): string;
|
|
29
|
+
private generateRelationshipMethods;
|
|
30
|
+
private generateStoreConstructorParams;
|
|
31
|
+
private generateRelationshipImports;
|
|
24
32
|
generateStore(modelConfig: ModelConfig): string;
|
|
25
33
|
generateStores(models: ModelConfig[]): Record<string, string>;
|
|
26
34
|
generateFromYamlFile(yamlFilePath: string): Record<string, string>;
|
|
@@ -52,20 +52,52 @@ class StoreGenerator {
|
|
|
52
52
|
array: 'any[]',
|
|
53
53
|
object: 'object'
|
|
54
54
|
};
|
|
55
|
+
this.availableModels = new Set();
|
|
55
56
|
}
|
|
56
|
-
mapType(yamlType) {
|
|
57
|
+
mapType(yamlType, isRelationship = false) {
|
|
58
|
+
// For relationships, we store the foreign key (number) in the database
|
|
59
|
+
if (isRelationship) {
|
|
60
|
+
return 'number';
|
|
61
|
+
}
|
|
62
|
+
// Check if this is a known model (relationship) - should use foreign key type
|
|
63
|
+
if (this.availableModels.has(yamlType)) {
|
|
64
|
+
return 'number'; // Foreign keys are numbers
|
|
65
|
+
}
|
|
57
66
|
return this.typeMapping[yamlType] || 'any';
|
|
58
67
|
}
|
|
68
|
+
setAvailableModels(models) {
|
|
69
|
+
this.availableModels.clear();
|
|
70
|
+
models.forEach(model => {
|
|
71
|
+
this.availableModels.add(model.name);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
isRelationshipField(field) {
|
|
75
|
+
return this.availableModels.has(field.type);
|
|
76
|
+
}
|
|
77
|
+
getForeignKeyFieldName(field) {
|
|
78
|
+
// Convention: fieldName + 'Id' (e.g., owner -> ownerId)
|
|
79
|
+
return field.name + 'Id';
|
|
80
|
+
}
|
|
59
81
|
generateRowFields(modelConfig) {
|
|
60
82
|
const fields = [];
|
|
61
83
|
modelConfig.fields.forEach(field => {
|
|
62
84
|
if (field.name === 'createdAt') {
|
|
63
85
|
return;
|
|
64
86
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
87
|
+
// For relationship fields, store the foreign key instead
|
|
88
|
+
if (this.isRelationshipField(field)) {
|
|
89
|
+
const foreignKeyName = this.getForeignKeyFieldName(field);
|
|
90
|
+
const tsType = 'number'; // Foreign keys are always numbers
|
|
91
|
+
const isOptional = !field.required && !field.auto;
|
|
92
|
+
const fieldDef = ` ${foreignKeyName}${isOptional ? '?' : ''}: ${tsType};`;
|
|
93
|
+
fields.push(fieldDef);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
const tsType = this.mapType(field.type);
|
|
97
|
+
const isOptional = !field.required && !field.auto;
|
|
98
|
+
const fieldDef = ` ${field.name}${isOptional ? '?' : ''}: ${tsType};`;
|
|
99
|
+
fields.push(fieldDef);
|
|
100
|
+
}
|
|
69
101
|
});
|
|
70
102
|
return fields.join('\n');
|
|
71
103
|
}
|
|
@@ -106,6 +138,10 @@ class StoreGenerator {
|
|
|
106
138
|
if (field.name === 'createdAt') {
|
|
107
139
|
return ' row.created_at';
|
|
108
140
|
}
|
|
141
|
+
// For relationship fields, we pass null - will be loaded separately
|
|
142
|
+
if (this.isRelationshipField(field)) {
|
|
143
|
+
return ' null as any'; // Placeholder, loaded via loadRelationships
|
|
144
|
+
}
|
|
109
145
|
return ` row.${field.name}`;
|
|
110
146
|
});
|
|
111
147
|
return mappings.join(',\n');
|
|
@@ -115,6 +151,11 @@ class StoreGenerator {
|
|
|
115
151
|
if (field.name === 'createdAt') {
|
|
116
152
|
return ' created_at: model.createdAt';
|
|
117
153
|
}
|
|
154
|
+
// For relationship fields, extract ID from the object to store as FK
|
|
155
|
+
if (this.isRelationshipField(field)) {
|
|
156
|
+
const foreignKeyName = this.getForeignKeyFieldName(field);
|
|
157
|
+
return ` ${foreignKeyName}: model.${field.name}?.id`;
|
|
158
|
+
}
|
|
118
159
|
return ` ${field.name}: model.${field.name}`;
|
|
119
160
|
});
|
|
120
161
|
return mappings.join(',\n');
|
|
@@ -130,6 +171,76 @@ class StoreGenerator {
|
|
|
130
171
|
generateStoreInterface() {
|
|
131
172
|
return storeTemplates_1.fileTemplates.storeInterface;
|
|
132
173
|
}
|
|
174
|
+
generateRelationshipMethods(modelConfig) {
|
|
175
|
+
const relationshipFields = modelConfig.fields.filter(f => this.isRelationshipField(f));
|
|
176
|
+
if (relationshipFields.length === 0) {
|
|
177
|
+
return '';
|
|
178
|
+
}
|
|
179
|
+
const entityName = modelConfig.name;
|
|
180
|
+
const methods = [];
|
|
181
|
+
// Generate loadRelationships method
|
|
182
|
+
const loadCalls = relationshipFields.map(field => {
|
|
183
|
+
const foreignKeyName = this.getForeignKeyFieldName(field);
|
|
184
|
+
const relatedModel = field.type;
|
|
185
|
+
const relatedModelLower = relatedModel.toLowerCase();
|
|
186
|
+
return ` if (entity.${field.name} === null && row.${foreignKeyName}) {
|
|
187
|
+
const ${field.name} = await this.${relatedModelLower}Store.getById(row.${foreignKeyName});
|
|
188
|
+
if (${field.name}) {
|
|
189
|
+
entity.set${field.name.charAt(0).toUpperCase() + field.name.slice(1)}(${field.name});
|
|
190
|
+
}
|
|
191
|
+
}`;
|
|
192
|
+
}).join('\n');
|
|
193
|
+
methods.push(`
|
|
194
|
+
async loadRelationships(entity: ${entityName}, row: ${entityName}Row): Promise<${entityName}> {
|
|
195
|
+
${loadCalls}
|
|
196
|
+
return entity;
|
|
197
|
+
}`);
|
|
198
|
+
// Generate getByIdWithRelationships method
|
|
199
|
+
methods.push(`
|
|
200
|
+
async getByIdWithRelationships(id: number): Promise<${entityName} | null> {
|
|
201
|
+
try {
|
|
202
|
+
const query = 'SELECT * FROM ${entityName.toLowerCase()}s WHERE id = :id AND deleted_at IS NULL';
|
|
203
|
+
const result = await this.db.query(query, { id });
|
|
204
|
+
|
|
205
|
+
if (!result.success || result.data.length === 0) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const row = result.data[0] as ${entityName}Row;
|
|
210
|
+
const entity = ${entityName}Store.rowToModel(row);
|
|
211
|
+
return await this.loadRelationships(entity, row);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (error instanceof MySQLConnectionError) {
|
|
214
|
+
throw new Error(\`Database connection error while fetching ${entityName} with id \${id}: \${error.message}\`);
|
|
215
|
+
} else if (error instanceof MySQLQueryError) {
|
|
216
|
+
throw new Error(\`Query error while fetching ${entityName} with id \${id}: \${error.message}\`);
|
|
217
|
+
}
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}`);
|
|
221
|
+
return methods.join('\n');
|
|
222
|
+
}
|
|
223
|
+
generateStoreConstructorParams(modelConfig) {
|
|
224
|
+
const relationshipFields = modelConfig.fields.filter(f => this.isRelationshipField(f));
|
|
225
|
+
const params = ['private db: ISqlProvider'];
|
|
226
|
+
relationshipFields.forEach(field => {
|
|
227
|
+
const relatedModel = field.type;
|
|
228
|
+
const relatedModelLower = relatedModel.toLowerCase();
|
|
229
|
+
params.push(`private ${relatedModelLower}Store: ${relatedModel}Store`);
|
|
230
|
+
});
|
|
231
|
+
return params.join(', ');
|
|
232
|
+
}
|
|
233
|
+
generateRelationshipImports(modelConfig) {
|
|
234
|
+
const relationshipFields = modelConfig.fields.filter(f => this.isRelationshipField(f));
|
|
235
|
+
if (relationshipFields.length === 0) {
|
|
236
|
+
return '';
|
|
237
|
+
}
|
|
238
|
+
const imports = relationshipFields.map(field => {
|
|
239
|
+
const relatedModel = field.type;
|
|
240
|
+
return `import { ${relatedModel}Store } from './${relatedModel}Store';`;
|
|
241
|
+
});
|
|
242
|
+
return '\n' + imports.join('\n');
|
|
243
|
+
}
|
|
133
244
|
generateStore(modelConfig) {
|
|
134
245
|
const entityName = modelConfig.name;
|
|
135
246
|
const tableName = entityName.toLowerCase() + 's';
|
|
@@ -145,15 +256,26 @@ class StoreGenerator {
|
|
|
145
256
|
};
|
|
146
257
|
const rowInterface = this.replaceTemplateVars(storeTemplates_1.storeTemplates.rowInterface, variables);
|
|
147
258
|
const conversionMethods = this.replaceTemplateVars(storeTemplates_1.storeTemplates.conversionMethods, variables);
|
|
148
|
-
|
|
259
|
+
// Replace constructor in storeClass template
|
|
260
|
+
let storeClass = this.replaceTemplateVars(storeTemplates_1.storeTemplates.storeClass, {
|
|
149
261
|
...variables,
|
|
150
262
|
CONVERSION_METHODS: conversionMethods
|
|
151
263
|
});
|
|
264
|
+
// Update constructor to include foreign store dependencies
|
|
265
|
+
const constructorParams = this.generateStoreConstructorParams(modelConfig);
|
|
266
|
+
storeClass = storeClass.replace('constructor(private db: ISqlProvider) {}', `constructor(${constructorParams}) {}`);
|
|
267
|
+
// Add relationship methods before the closing brace
|
|
268
|
+
const relationshipMethods = this.generateRelationshipMethods(modelConfig);
|
|
269
|
+
if (relationshipMethods) {
|
|
270
|
+
storeClass = storeClass.replace(/}$/, `${relationshipMethods}\n}`);
|
|
271
|
+
}
|
|
272
|
+
// Add relationship store imports
|
|
273
|
+
const relationshipImports = this.generateRelationshipImports(modelConfig);
|
|
152
274
|
return this.replaceTemplateVars(storeTemplates_1.fileTemplates.storeFile, {
|
|
153
275
|
ENTITY_NAME: entityName,
|
|
154
276
|
ROW_INTERFACE: rowInterface,
|
|
155
277
|
STORE_CLASS: storeClass
|
|
156
|
-
});
|
|
278
|
+
}) + relationshipImports;
|
|
157
279
|
}
|
|
158
280
|
generateStores(models) {
|
|
159
281
|
const result = {};
|
|
@@ -169,6 +291,8 @@ class StoreGenerator {
|
|
|
169
291
|
if (config.modules) {
|
|
170
292
|
Object.values(config.modules).forEach(moduleConfig => {
|
|
171
293
|
if (moduleConfig.models && moduleConfig.models.length > 0) {
|
|
294
|
+
// Set available models for relationship detection
|
|
295
|
+
this.setAvailableModels(moduleConfig.models);
|
|
172
296
|
const stores = this.generateStores(moduleConfig.models);
|
|
173
297
|
Object.assign(result, stores);
|
|
174
298
|
}
|
|
@@ -177,6 +301,8 @@ class StoreGenerator {
|
|
|
177
301
|
else if (config.models) {
|
|
178
302
|
const module = config;
|
|
179
303
|
if (module.models && module.models.length > 0) {
|
|
304
|
+
// Set available models for relationship detection
|
|
305
|
+
this.setAvailableModels(module.models);
|
|
180
306
|
const stores = this.generateStores(module.models);
|
|
181
307
|
Object.assign(result, stores);
|
|
182
308
|
}
|
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
export declare class TemplateGenerator {
|
|
2
|
+
/**
|
|
3
|
+
* Helper method to infer model from action handlers
|
|
4
|
+
*/
|
|
5
|
+
private inferModelFromAction;
|
|
6
|
+
/**
|
|
7
|
+
* Find the actual API endpoint path for a given action and model
|
|
8
|
+
*/
|
|
9
|
+
private findApiEndpointPath;
|
|
10
|
+
/**
|
|
11
|
+
* Build relationship context for finding create routes and list API endpoints
|
|
12
|
+
*/
|
|
13
|
+
private buildRelationshipContext;
|
|
14
|
+
/**
|
|
15
|
+
* Generate templates for a single routes configuration
|
|
16
|
+
*/
|
|
17
|
+
private generateForRoutesConfig;
|
|
18
|
+
/**
|
|
19
|
+
* Generate templates for a module (handles both single routes object and array)
|
|
20
|
+
*/
|
|
2
21
|
private generateForModule;
|
|
3
22
|
generateFromYamlFile(yamlFilePath: string): Record<string, {
|
|
4
23
|
file: string;
|