@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +256 -0
  3. package/dist/cli.js +26 -0
  4. package/dist/commands/createApp.js +2 -0
  5. package/dist/commands/generateAll.js +153 -29
  6. package/dist/commands/migrateCommit.d.ts +1 -0
  7. package/dist/commands/migrateCommit.js +201 -0
  8. package/dist/generators/controllerGenerator.d.ts +7 -0
  9. package/dist/generators/controllerGenerator.js +60 -29
  10. package/dist/generators/domainModelGenerator.d.ts +7 -0
  11. package/dist/generators/domainModelGenerator.js +57 -3
  12. package/dist/generators/serviceGenerator.d.ts +16 -1
  13. package/dist/generators/serviceGenerator.js +125 -12
  14. package/dist/generators/storeGenerator.d.ts +8 -0
  15. package/dist/generators/storeGenerator.js +133 -7
  16. package/dist/generators/templateGenerator.d.ts +19 -0
  17. package/dist/generators/templateGenerator.js +216 -11
  18. package/dist/generators/templates/appTemplates.d.ts +8 -7
  19. package/dist/generators/templates/appTemplates.js +11 -1572
  20. package/dist/generators/templates/data/appTsTemplate +39 -0
  21. package/dist/generators/templates/data/appYamlTemplate +4 -0
  22. package/dist/generators/templates/data/cursorRulesTemplate +671 -0
  23. package/dist/generators/templates/data/errorTemplate +28 -0
  24. package/dist/generators/templates/data/frontendScriptTemplate +739 -0
  25. package/dist/generators/templates/data/mainViewTemplate +16 -0
  26. package/dist/generators/templates/data/translationsTemplate +68 -0
  27. package/dist/generators/templates/data/tsConfigTemplate +19 -0
  28. package/dist/generators/templates/viewTemplates.d.ts +10 -1
  29. package/dist/generators/templates/viewTemplates.js +138 -6
  30. package/dist/generators/validationGenerator.d.ts +5 -0
  31. package/dist/generators/validationGenerator.js +51 -0
  32. package/dist/utils/constants.d.ts +3 -0
  33. package/dist/utils/constants.js +5 -2
  34. package/dist/utils/migrationUtils.d.ts +49 -0
  35. package/dist/utils/migrationUtils.js +291 -0
  36. package/howto.md +157 -65
  37. 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
- return `export class ${className} {
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
- config.models.forEach(m => {
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: any[];
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
- return sortedFields
333
+ const args = [];
334
+ sortedFields
313
335
  .filter(field => !field.auto && field.name !== 'id')
314
- .map(field => `${entityLower}Data.${field.name}`)
315
- .join(', ');
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
- generateUpdateSetterCalls(moduleConfig, entityName) {
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
- return model.fields
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
- const methodName = `set${field.name.charAt(0).toUpperCase() + field.name.slice(1)}`;
328
- return `if (${entityLower}Data.${field.name} !== undefined) {
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
- const tsType = this.mapType(field.type);
66
- const isOptional = !field.required && !field.auto;
67
- const fieldDef = ` ${field.name}${isOptional ? '?' : ''}: ${tsType};`;
68
- fields.push(fieldDef);
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
- const storeClass = this.replaceTemplateVars(storeTemplates_1.storeTemplates.storeClass, {
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;