@currentjs/gen 0.3.1 → 0.5.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 (69) hide show
  1. package/CHANGELOG.md +8 -289
  2. package/README.md +623 -427
  3. package/dist/cli.js +2 -1
  4. package/dist/commands/commit.js +25 -42
  5. package/dist/commands/createApp.js +1 -0
  6. package/dist/commands/createModule.js +151 -45
  7. package/dist/commands/diff.js +27 -40
  8. package/dist/commands/generateAll.js +141 -291
  9. package/dist/commands/migrateCommit.js +6 -18
  10. package/dist/commands/migratePush.d.ts +1 -0
  11. package/dist/commands/migratePush.js +135 -0
  12. package/dist/commands/migrateUpdate.d.ts +1 -0
  13. package/dist/commands/migrateUpdate.js +147 -0
  14. package/dist/commands/newGenerateAll.d.ts +4 -0
  15. package/dist/commands/newGenerateAll.js +336 -0
  16. package/dist/generators/controllerGenerator.d.ts +43 -19
  17. package/dist/generators/controllerGenerator.js +547 -329
  18. package/dist/generators/domainLayerGenerator.d.ts +21 -0
  19. package/dist/generators/domainLayerGenerator.js +276 -0
  20. package/dist/generators/dtoGenerator.d.ts +21 -0
  21. package/dist/generators/dtoGenerator.js +518 -0
  22. package/dist/generators/newControllerGenerator.d.ts +55 -0
  23. package/dist/generators/newControllerGenerator.js +644 -0
  24. package/dist/generators/newServiceGenerator.d.ts +19 -0
  25. package/dist/generators/newServiceGenerator.js +266 -0
  26. package/dist/generators/newStoreGenerator.d.ts +39 -0
  27. package/dist/generators/newStoreGenerator.js +408 -0
  28. package/dist/generators/newTemplateGenerator.d.ts +29 -0
  29. package/dist/generators/newTemplateGenerator.js +510 -0
  30. package/dist/generators/serviceGenerator.d.ts +16 -51
  31. package/dist/generators/serviceGenerator.js +167 -586
  32. package/dist/generators/storeGenerator.d.ts +35 -32
  33. package/dist/generators/storeGenerator.js +291 -238
  34. package/dist/generators/storeGeneratorV2.d.ts +31 -0
  35. package/dist/generators/storeGeneratorV2.js +190 -0
  36. package/dist/generators/templateGenerator.d.ts +21 -21
  37. package/dist/generators/templateGenerator.js +393 -268
  38. package/dist/generators/templates/appTemplates.d.ts +3 -1
  39. package/dist/generators/templates/appTemplates.js +15 -10
  40. package/dist/generators/templates/data/appYamlTemplate +5 -2
  41. package/dist/generators/templates/data/cursorRulesTemplate +315 -221
  42. package/dist/generators/templates/data/frontendScriptTemplate +76 -47
  43. package/dist/generators/templates/data/mainViewTemplate +1 -1
  44. package/dist/generators/templates/data/systemTsTemplate +5 -0
  45. package/dist/generators/templates/index.d.ts +0 -3
  46. package/dist/generators/templates/index.js +0 -3
  47. package/dist/generators/templates/newStoreTemplates.d.ts +5 -0
  48. package/dist/generators/templates/newStoreTemplates.js +141 -0
  49. package/dist/generators/templates/storeTemplates.d.ts +1 -5
  50. package/dist/generators/templates/storeTemplates.js +102 -219
  51. package/dist/generators/templates/viewTemplates.js +1 -1
  52. package/dist/generators/useCaseGenerator.d.ts +13 -0
  53. package/dist/generators/useCaseGenerator.js +188 -0
  54. package/dist/types/configTypes.d.ts +148 -0
  55. package/dist/types/configTypes.js +10 -0
  56. package/dist/utils/childEntityUtils.d.ts +18 -0
  57. package/dist/utils/childEntityUtils.js +78 -0
  58. package/dist/utils/commandUtils.d.ts +43 -0
  59. package/dist/utils/commandUtils.js +124 -0
  60. package/dist/utils/commitUtils.d.ts +4 -1
  61. package/dist/utils/constants.d.ts +10 -0
  62. package/dist/utils/constants.js +13 -1
  63. package/dist/utils/diResolver.d.ts +32 -0
  64. package/dist/utils/diResolver.js +204 -0
  65. package/dist/utils/new_parts_of_migrationUtils.d.ts +0 -0
  66. package/dist/utils/new_parts_of_migrationUtils.js +164 -0
  67. package/dist/utils/typeUtils.d.ts +19 -0
  68. package/dist/utils/typeUtils.js +70 -0
  69. package/package.json +7 -3
@@ -38,308 +38,433 @@ const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const yaml_1 = require("yaml");
40
40
  const generationRegistry_1 = require("../utils/generationRegistry");
41
- const viewTemplates_1 = require("./templates/viewTemplates");
42
41
  const colors_1 = require("../utils/colors");
42
+ const configTypes_1 = require("../types/configTypes");
43
+ const childEntityUtils_1 = require("../utils/childEntityUtils");
44
+ const typeUtils_1 = require("../utils/typeUtils");
43
45
  class TemplateGenerator {
46
+ constructor() {
47
+ this.valueObjects = {};
48
+ }
44
49
  /**
45
- * Helper method to infer model from action handlers
50
+ * Convert a route prefix like "/invoice/:invoiceId/items" into a
51
+ * template-ready path like "/invoice/{{ invoiceId }}/items".
46
52
  */
47
- inferModelFromAction(action, moduleConfig) {
48
- var _a;
49
- if (!moduleConfig.actions || !moduleConfig.actions[action]) {
50
- return null;
51
- }
52
- const handlers = moduleConfig.actions[action].handlers;
53
- if (!handlers || handlers.length === 0) {
54
- return null;
55
- }
56
- // Get the first handler and extract model name
57
- const firstHandler = handlers[0];
58
- const parts = firstHandler.split(':');
59
- if (parts.length < 2) {
60
- return null;
61
- }
62
- const modelName = parts[0];
63
- const model = (_a = moduleConfig.models) === null || _a === void 0 ? void 0 : _a.find(m => m.name === modelName || m.name.toLowerCase() === modelName.toLowerCase());
64
- return model || null;
53
+ prefixToTemplatePath(prefix) {
54
+ return prefix.replace(/:([a-zA-Z_]\w*)/g, '{{ $1 }}');
65
55
  }
66
56
  /**
67
- * Find the actual API endpoint path for a given action and model
57
+ * Replace a single path param in prefix with a template expression (e.g. for list: item.id, for detail: id).
68
58
  */
69
- findApiEndpointPath(action, modelName, moduleConfig) {
70
- if (!moduleConfig.api) {
71
- return null;
72
- }
73
- // Map web route actions to API operation types
74
- const actionMap = {
75
- 'empty': ['create'], // empty form -> create operation
76
- 'create': ['create'],
77
- 'update': ['update'],
78
- 'delete': ['delete'],
79
- 'list': ['list'],
80
- 'get': ['get', 'update'] // get can be for detail view or edit form
81
- };
82
- // Get possible API action names to look for
83
- const searchActions = actionMap[action] || [action];
84
- // Also check if there's a specific action defined in the actions section that handles this model
85
- if (moduleConfig.actions) {
86
- for (const [actionName, actionConfig] of Object.entries(moduleConfig.actions)) {
87
- if (actionConfig.handlers && actionConfig.handlers.length > 0) {
88
- const firstHandler = actionConfig.handlers[0];
89
- const parts = firstHandler.split(':');
90
- const handlerModel = parts[0];
91
- // If this action's handler targets our model, include it in search
92
- if (handlerModel === modelName || handlerModel.toLowerCase() === modelName.toLowerCase()) {
93
- // If it's a default handler, get the operation type
94
- if (parts.length === 3 && parts[1] === 'default') {
95
- const operation = parts[2]; // e.g., 'create', 'list', etc.
96
- if (searchActions.includes(operation)) {
97
- searchActions.push(actionName);
98
- }
99
- }
59
+ prefixWithParam(prefix, paramName, templateExpr) {
60
+ return prefix.replace(new RegExp(':' + paramName + '(?=/|$)'), templateExpr);
61
+ }
62
+ renderListTemplate(modelName, viewName, fields, basePath, withChildChildren) {
63
+ const fieldHeaders = fields
64
+ .filter(([name]) => name !== 'id')
65
+ .slice(0, 5)
66
+ .map(([name]) => ` <th>${(0, typeUtils_1.capitalize)(name)}</th>`)
67
+ .join('\n');
68
+ const fieldCells = fields
69
+ .filter(([name]) => name !== 'id')
70
+ .slice(0, 5)
71
+ .map(([name, config]) => {
72
+ const voConfig = this.valueObjects[(0, typeUtils_1.capitalize)((config.type || 'string'))];
73
+ if (voConfig) {
74
+ const parts = Object.keys(voConfig.fields)
75
+ .map(sub => `{{ item.${name}.${sub} }}`)
76
+ .join(' ');
77
+ return ` <td>${parts}</td>`;
78
+ }
79
+ return ` <td>{{ item.${name} }}</td>`;
80
+ })
81
+ .join('\n');
82
+ const childLinkHeaders = (withChildChildren || [])
83
+ .map(child => ` <th>${child.childEntityName}</th>`)
84
+ .join('\n');
85
+ const childLinkCells = (withChildChildren || [])
86
+ .map(child => {
87
+ const childPath = child.childWebPrefix
88
+ ? this.prefixWithParam(child.childWebPrefix, child.parentIdField, '{{ item.id }}')
89
+ : '#';
90
+ return ` <td><a href="${childPath}" class="btn btn-sm btn-outline-secondary">Items</a></td>`;
91
+ })
92
+ .join('\n');
93
+ const childHeaderBlock = childLinkHeaders ? '\n' + childLinkHeaders : '';
94
+ const childCellBlock = childLinkCells ? '\n' + childLinkCells : '';
95
+ return `<!-- @template name="${viewName}" -->
96
+ <div class="container mt-4">
97
+ <h1>${modelName} List</h1>
98
+
99
+ <div class="mb-3">
100
+ <a href="${basePath}/create" class="btn btn-primary">Create New ${modelName}</a>
101
+ </div>
102
+
103
+ <table class="table table-striped">
104
+ <thead>
105
+ <tr>
106
+ ${fieldHeaders}
107
+ <th>Actions</th>${childHeaderBlock}
108
+ </tr>
109
+ </thead>
110
+ <tbody x-for="items" x-row="item">
111
+ <tr>
112
+ ${fieldCells}
113
+ <td>
114
+ <a href="${basePath}/{{ item.id }}" class="btn btn-sm btn-info">View</a>
115
+ <a href="${basePath}/{{ item.id }}/edit" class="btn btn-sm btn-warning">Edit</a>
116
+ </td>${childCellBlock}
117
+ </tr>
118
+ </tbody>
119
+ </table>
120
+
121
+ <div x-if="total > limit">
122
+ <nav>
123
+ <ul class="pagination">
124
+ <!-- Pagination controls -->
125
+ </ul>
126
+ </nav>
127
+ </div>
128
+ </div>`;
129
+ }
130
+ renderChildTableSection(child, parentIdTemplateExpr) {
131
+ const childVar = child.childEntityName.charAt(0).toLowerCase() + child.childEntityName.slice(1);
132
+ const childItemsKey = `${childVar}Items`;
133
+ const childBasePath = child.childWebPrefix
134
+ ? this.prefixWithParam(child.childWebPrefix, child.parentIdField, parentIdTemplateExpr)
135
+ : '';
136
+ const fieldEntries = Object.entries(child.childFields).filter(([name]) => name !== 'id').slice(0, 5);
137
+ const headers = fieldEntries.map(([name]) => ` <th>${(0, typeUtils_1.capitalize)(name)}</th>`).join('\n');
138
+ const cells = fieldEntries.map(([name, config]) => {
139
+ const voConfig = this.valueObjects[(0, typeUtils_1.capitalize)(((config === null || config === void 0 ? void 0 : config.type) || 'string'))];
140
+ if (voConfig) {
141
+ const parts = Object.keys(voConfig.fields)
142
+ .map(sub => `{{ childItem.${name}.${sub} }}`)
143
+ .join(' ');
144
+ return ` <td>${parts}</td>`;
145
+ }
146
+ return ` <td>{{ childItem.${name} }}</td>`;
147
+ }).join('\n');
148
+ const addLink = childBasePath
149
+ ? ` <div class="mb-3">
150
+ <a href="${childBasePath}/create" class="btn btn-primary btn-sm">Add ${child.childEntityName}</a>
151
+ </div>`
152
+ : '';
153
+ const actionLinks = childBasePath
154
+ ? ` <td>
155
+ <a href="${childBasePath}/{{ childItem.id }}" class="btn btn-sm btn-info">View</a>
156
+ <a href="${childBasePath}/{{ childItem.id }}/edit" class="btn btn-sm btn-warning">Edit</a>
157
+ </td>`
158
+ : ' <td></td>';
159
+ return `
160
+ <h2 class="mt-4">${child.childEntityName} List</h2>
161
+ ${addLink}
162
+ <table class="table table-striped">
163
+ <thead>
164
+ <tr>
165
+ ${headers}
166
+ <th>Actions</th>
167
+ </tr>
168
+ </thead>
169
+ <tbody x-for="${childItemsKey}" x-row="childItem">
170
+ <tr>
171
+ ${cells}
172
+ ${actionLinks}
173
+ </tr>
174
+ </tbody>
175
+ </table>`;
176
+ }
177
+ renderDetailTemplate(modelName, viewName, fields, basePath, withChildChildren) {
178
+ const fieldRows = fields
179
+ .map(([name, config]) => {
180
+ const voConfig = this.valueObjects[(0, typeUtils_1.capitalize)((config.type || 'string'))];
181
+ if (voConfig) {
182
+ const parts = Object.keys(voConfig.fields)
183
+ .map(sub => `{{ ${name}.${sub} }}`)
184
+ .join(' ');
185
+ return ` <div class="row mb-2">
186
+ <div class="col-4"><strong>${(0, typeUtils_1.capitalize)(name)}:</strong></div>
187
+ <div class="col-8">${parts}</div>
188
+ </div>`;
189
+ }
190
+ return ` <div class="row mb-2">
191
+ <div class="col-4"><strong>${(0, typeUtils_1.capitalize)(name)}:</strong></div>
192
+ <div class="col-8">{{ ${name} }}</div>
193
+ </div>`;
194
+ })
195
+ .join('\n');
196
+ const childSections = (withChildChildren || [])
197
+ .map(child => this.renderChildTableSection(child, '{{ id }}'))
198
+ .join('');
199
+ return `<!-- @template name="${viewName}" -->
200
+ <div class="container mt-4">
201
+ <h1>${modelName} Details</h1>
202
+
203
+ <div class="card">
204
+ <div class="card-body">
205
+ ${fieldRows}
206
+ </div>
207
+ </div>
208
+
209
+ <div class="mt-3">
210
+ <a href="${basePath}/{{ id }}/edit" class="btn btn-warning">Edit</a>
211
+ <a href="${basePath}" class="btn btn-secondary">Back to List</a>
212
+ </div>${childSections}
213
+ </div>`;
214
+ }
215
+ buildFieldTypesJson(safeFields) {
216
+ return JSON.stringify(safeFields.reduce((acc, [name, config]) => {
217
+ const typeStr = typeof (config === null || config === void 0 ? void 0 : config.type) === 'string' ? config.type : 'string';
218
+ const capitalizedType = (0, typeUtils_1.capitalize)(typeStr);
219
+ const voConfig = this.valueObjects[capitalizedType];
220
+ if (voConfig) {
221
+ for (const [subName, subConfig] of Object.entries(voConfig.fields)) {
222
+ if (typeof subConfig === 'object' && 'values' in subConfig) {
223
+ acc[`${name}.${subName}`] = 'enum';
224
+ }
225
+ else {
226
+ acc[`${name}.${subName}`] = subConfig.type || 'string';
100
227
  }
101
228
  }
102
229
  }
230
+ else {
231
+ acc[name] = typeStr;
232
+ }
233
+ return acc;
234
+ }, {}));
235
+ }
236
+ renderFormTemplate(mode, modelName, viewName, fields, basePath, onSuccess, onError, enumValuesMap = {}) {
237
+ const safeFields = fields.filter(([name, config]) => name !== 'id' && !config.auto);
238
+ const isEdit = mode === 'edit';
239
+ const formFields = safeFields
240
+ .map(([name, config]) => this.renderFormField(name, config, enumValuesMap[name] || [], isEdit))
241
+ .join('\n');
242
+ const fieldTypesJson = this.buildFieldTypesJson(safeFields);
243
+ const strategies = [];
244
+ if (onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess.toast)
245
+ strategies.push('toast');
246
+ if (onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess.back)
247
+ strategies.push('back');
248
+ if (mode === 'create' && (onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess.redirect))
249
+ strategies.push('redirect');
250
+ const strategyAttr = strategies.length > 0 ? `data-strategy='${JSON.stringify(strategies)}'` : '';
251
+ const redirectAttr = mode === 'create' && (onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess.redirect)
252
+ ? `data-redirect="${this.prefixToTemplatePath(onSuccess.redirect)}"`
253
+ : '';
254
+ const title = mode === 'create' ? `Create ${modelName}` : `Edit ${modelName}`;
255
+ const formAction = mode === 'create' ? `${basePath}/create` : `${basePath}/{{ id }}/edit`;
256
+ const submitLabel = mode === 'create' ? 'Create' : 'Update';
257
+ const cancelHref = mode === 'create' ? basePath : `${basePath}/{{ id }}`;
258
+ return `<!-- @template name="${viewName}" -->
259
+ <div class="container mt-4">
260
+ <h1>${title}</h1>
261
+
262
+ <form method="POST" action="${formAction}" ${strategyAttr} ${redirectAttr} data-entity-name="${modelName}" data-field-types='${fieldTypesJson}'>
263
+ ${formFields}
264
+
265
+ <div class="d-flex gap-2">
266
+ <button type="submit" class="btn btn-primary">${submitLabel}</button>
267
+ <a href="${cancelHref}" class="btn btn-secondary">Cancel</a>
268
+ </div>
269
+ </form>
270
+ </div>`;
271
+ }
272
+ renderCreateTemplate(modelName, viewName, fields, basePath, onSuccess, onError, enumValuesMap = {}) {
273
+ return this.renderFormTemplate('create', modelName, viewName, fields, basePath, onSuccess, onError, enumValuesMap);
274
+ }
275
+ renderEditTemplate(modelName, viewName, fields, basePath, onSuccess, onError, enumValuesMap = {}) {
276
+ return this.renderFormTemplate('edit', modelName, viewName, fields, basePath, onSuccess, onError, enumValuesMap);
277
+ }
278
+ getInputType(fieldType) {
279
+ switch (fieldType) {
280
+ case 'string': return 'text';
281
+ case 'number':
282
+ case 'integer':
283
+ case 'float':
284
+ case 'decimal':
285
+ case 'id': return 'number';
286
+ case 'datetime':
287
+ case 'date': return 'datetime-local';
288
+ default: return 'text';
289
+ }
290
+ }
291
+ renderValueObjectField(name, label, voConfig, required, isEdit) {
292
+ const subFields = Object.entries(voConfig.fields);
293
+ const columns = subFields.map(([subName, subConfig]) => {
294
+ const fullName = `${name}.${subName}`;
295
+ const subLabel = (0, typeUtils_1.capitalize)(subName);
296
+ if (typeof subConfig === 'object' && 'values' in subConfig) {
297
+ const uniqueValues = [...new Set(subConfig.values)];
298
+ const options = uniqueValues.map(v => {
299
+ const sel = isEdit ? ` {{ ${name}.${subName} === '${v}' ? 'selected' : '' }}` : '';
300
+ return ` <option value="${v}"${sel}>${v}</option>`;
301
+ }).join('\n');
302
+ return ` <div class="col-auto">
303
+ <select class="form-select" id="${fullName}" name="${fullName}" ${required}>
304
+ <option value="">-- ${subLabel} --</option>
305
+ ${options}
306
+ </select>
307
+ </div>`;
308
+ }
309
+ else {
310
+ const type = this.getInputType(subConfig.type);
311
+ const value = isEdit ? ` value="{{ ${name}.${subName} || '' }}"` : '';
312
+ return ` <div class="col">
313
+ <input type="${type}" class="form-control" id="${fullName}" name="${fullName}" placeholder="${subLabel}"${value} ${required}>
314
+ </div>`;
315
+ }
316
+ }).join('\n');
317
+ return ` <div class="mb-3">
318
+ <label class="form-label">${label}</label>
319
+ <div class="row g-2">
320
+ ${columns}
321
+ </div>
322
+ </div>`;
323
+ }
324
+ renderFormField(name, config, enumValues = [], isEdit = false) {
325
+ const required = config.required ? 'required' : '';
326
+ const label = (0, typeUtils_1.capitalize)(name);
327
+ const fieldType = (config.type || 'string').toLowerCase();
328
+ const capitalizedType = (0, typeUtils_1.capitalize)(fieldType);
329
+ const voConfig = this.valueObjects[capitalizedType];
330
+ if (voConfig) {
331
+ return this.renderValueObjectField(name, label, voConfig, required, isEdit);
103
332
  }
104
- // Support both single api object and array
105
- const apiConfigs = Array.isArray(moduleConfig.api) ? moduleConfig.api : [moduleConfig.api];
106
- for (const apiConfig of apiConfigs) {
107
- if (!apiConfig.endpoints)
108
- continue;
109
- // Look for an endpoint with matching action
110
- for (const endpoint of apiConfig.endpoints) {
111
- if (!searchActions.includes(endpoint.action))
112
- continue;
113
- // Check if this endpoint's model matches (explicit or default)
114
- const endpointModel = endpoint.model || apiConfig.model || (moduleConfig.models && moduleConfig.models[0] ? moduleConfig.models[0].name : null);
115
- if (endpointModel === modelName || (endpointModel === null || endpointModel === void 0 ? void 0 : endpointModel.toLowerCase()) === modelName.toLowerCase()) {
116
- // Found a match - construct the full path
117
- const prefix = (apiConfig.prefix || `/api/${modelName.toLowerCase()}`).replace(/\/$/, '');
118
- const path = endpoint.path || '';
119
- // Return full path without trailing slash
120
- return `${prefix}${path}`.replace(/\/$/, '');
333
+ switch (fieldType) {
334
+ case 'boolean':
335
+ case 'bool': {
336
+ const checked = isEdit ? ` {{ ${name} ? 'checked' : '' }}` : '';
337
+ return ` <div class="mb-3">
338
+ <div class="form-check">
339
+ <input type="checkbox" class="form-check-input" id="${name}" name="${name}" value="true"${checked} ${required}>
340
+ <label for="${name}" class="form-check-label">${label}</label>
341
+ </div>
342
+ </div>`;
343
+ }
344
+ case 'enum': {
345
+ if (enumValues.length > 0) {
346
+ const options = enumValues.map(v => {
347
+ const sel = isEdit ? ` {{ ${name} === '${v}' ? 'selected' : '' }}` : '';
348
+ return ` <option value="${v}"${sel}>${(0, typeUtils_1.capitalize)(v)}</option>`;
349
+ }).join('\n');
350
+ return ` <div class="mb-3">
351
+ <label for="${name}" class="form-label">${label}</label>
352
+ <select class="form-select" id="${name}" name="${name}" ${required}>
353
+ <option value="">-- Select ${label} --</option>
354
+ ${options}
355
+ </select>
356
+ </div>`;
121
357
  }
358
+ const value = isEdit ? ` value="{{ ${name} || '' }}"` : '';
359
+ return ` <div class="mb-3">
360
+ <label for="${name}" class="form-label">${label}</label>
361
+ <input type="text" class="form-control" id="${name}" name="${name}"${value} ${required}>
362
+ </div>`;
363
+ }
364
+ default: {
365
+ const type = this.getInputType(config.type);
366
+ const value = isEdit ? ` value="{{ ${name} || '' }}"` : '';
367
+ return ` <div class="mb-3">
368
+ <label for="${name}" class="form-label">${label}</label>
369
+ <input type="${type}" class="form-control" id="${name}" name="${name}"${value} ${required}>
370
+ </div>`;
122
371
  }
123
372
  }
124
- return null;
125
373
  }
126
- /**
127
- * Build relationship context for finding create routes and list API endpoints
128
- */
129
- buildRelationshipContext(moduleConfig) {
130
- const routePaths = new Map();
131
- const apiPaths = new Map();
132
- // Build route paths for create actions
133
- const routeConfigs = moduleConfig.routes
134
- ? (Array.isArray(moduleConfig.routes) ? moduleConfig.routes : [moduleConfig.routes])
135
- : [];
136
- for (const routeConfig of routeConfigs) {
137
- const prefix = (routeConfig.prefix || '').replace(/\/$/, '');
138
- const configModel = routeConfig.model || (moduleConfig.models && moduleConfig.models[0] ? moduleConfig.models[0].name : null);
139
- for (const endpoint of routeConfig.endpoints || []) {
140
- const endpointModel = endpoint.model || configModel;
141
- if (!endpointModel)
142
- continue;
143
- // Look for create or empty actions (both lead to create forms)
144
- if (endpoint.action === 'empty' || endpoint.action === 'create') {
145
- const fullPath = `${prefix}${endpoint.path}`;
146
- // Store only if we haven't found a path for this model yet, or if this is more specific
147
- if (!routePaths.has(endpointModel) || endpoint.action === 'empty') {
148
- routePaths.set(endpointModel, fullPath);
149
- }
150
- }
374
+ getEnumValuesMap(config, resourceName) {
375
+ var _a;
376
+ const enumMap = {};
377
+ const aggregate = config.domain.aggregates[resourceName];
378
+ if (!aggregate)
379
+ return enumMap;
380
+ for (const [fieldName, fieldConfig] of Object.entries(aggregate.fields)) {
381
+ if (fieldConfig.type === 'enum' && fieldConfig.values) {
382
+ enumMap[fieldName] = fieldConfig.values;
151
383
  }
152
384
  }
153
- // Build API paths for list actions
154
- const apiConfigs = moduleConfig.api
155
- ? (Array.isArray(moduleConfig.api) ? moduleConfig.api : [moduleConfig.api])
156
- : [];
157
- for (const apiConfig of apiConfigs) {
158
- const prefix = (apiConfig.prefix || '').replace(/\/$/, '');
159
- const configModel = apiConfig.model || (moduleConfig.models && moduleConfig.models[0] ? moduleConfig.models[0].name : null);
160
- for (const endpoint of apiConfig.endpoints || []) {
161
- const endpointModel = endpoint.model || configModel;
162
- if (!endpointModel)
163
- continue;
164
- // Look for list actions
165
- if (endpoint.action === 'list' || endpoint.action === 'getOwner' || endpoint.action.toLowerCase().includes('list')) {
166
- const fullPath = `${prefix}${endpoint.path || ''}`;
167
- // Store only if we haven't found a path for this model yet
168
- if (!apiPaths.has(endpointModel)) {
169
- apiPaths.set(endpointModel, fullPath);
385
+ const modelUseCases = config.useCases[resourceName];
386
+ if (modelUseCases) {
387
+ for (const useCase of Object.values(modelUseCases)) {
388
+ if ((_a = useCase.input) === null || _a === void 0 ? void 0 : _a.filters) {
389
+ for (const [filterName, filterConfig] of Object.entries(useCase.input.filters)) {
390
+ if (filterConfig.enum && !enumMap[filterName]) {
391
+ enumMap[filterName] = filterConfig.enum;
392
+ }
170
393
  }
171
394
  }
172
395
  }
173
396
  }
174
- (0, viewTemplates_1.setRelationshipContext)({ routePaths, apiPaths });
397
+ return enumMap;
175
398
  }
176
- /**
177
- * Generate templates for a single routes configuration
178
- */
179
- generateForRoutesConfig(routesConfig, moduleConfig, seenLayouts) {
399
+ generateFromConfig(config) {
180
400
  const result = {};
181
- if (!moduleConfig.models || moduleConfig.models.length === 0)
401
+ this.valueObjects = config.domain.valueObjects || {};
402
+ if (!config.web) {
182
403
  return result;
183
- // Set available models for relationship detection in view templates
184
- const modelNames = moduleConfig.models.map(m => m.name);
185
- (0, viewTemplates_1.setAvailableModels)(modelNames);
186
- // Build context for relationship field paths
187
- this.buildRelationshipContext(moduleConfig);
188
- // Get top-level model for this routes config
189
- const topLevelModelName = routesConfig.model || moduleConfig.models[0].name;
190
- const topLevelModel = moduleConfig.models.find(m => m.name === topLevelModelName) || moduleConfig.models[0];
191
- const defaultEntityName = topLevelModel.name || 'Item';
192
- const defaultEntityLower = defaultEntityName.toLowerCase();
193
- const basePath = (routesConfig.prefix || `/${defaultEntityLower}`).replace(/\/$/, '');
194
- const strategy = (routesConfig.strategy && Array.isArray(routesConfig.strategy) && routesConfig.strategy.length > 0)
195
- ? routesConfig.strategy
196
- : ['back', 'toast'];
197
- for (const ep of routesConfig.endpoints || []) {
198
- if (!ep.view)
199
- continue;
200
- // Determine which model to use for this endpoint (Option A)
201
- let model;
202
- let entityName;
203
- let entityLower;
204
- let fields;
205
- let apiBase;
206
- if (ep.model) {
207
- // 1. Use endpoint-specific model if provided
208
- const endpointModel = moduleConfig.models.find(m => m.name === ep.model);
209
- if (endpointModel) {
210
- model = endpointModel;
211
- entityName = model.name;
212
- entityLower = entityName.toLowerCase();
213
- fields = model.fields || [];
214
- // Find actual API endpoint or use default
215
- apiBase = this.findApiEndpointPath(ep.action, entityName, moduleConfig) || `/api/${entityLower}`;
404
+ }
405
+ Object.entries(config.web).forEach(([resourceName, resourceConfig]) => {
406
+ const aggregate = config.domain.aggregates[resourceName];
407
+ if (!aggregate) {
408
+ console.warn(`Warning: No aggregate found for resource ${resourceName}`);
409
+ return;
410
+ }
411
+ const fields = Object.entries(aggregate.fields);
412
+ const enumValuesMap = this.getEnumValuesMap(config, resourceName);
413
+ const basePath = this.prefixToTemplatePath(resourceConfig.prefix);
414
+ const withChildChildren = (0, childEntityUtils_1.getChildrenOfParent)(config, resourceName);
415
+ resourceConfig.pages.forEach(page => {
416
+ var _a;
417
+ if (!page.view)
418
+ return;
419
+ // Determine template type from path and method
420
+ if (page.method === 'POST') {
421
+ // Skip POST endpoints - they don't need templates
422
+ return;
216
423
  }
217
- else {
218
- // Fallback to top-level model if specified model not found
219
- model = topLevelModel;
220
- entityName = defaultEntityName;
221
- entityLower = defaultEntityLower;
222
- fields = model.fields || [];
223
- apiBase = this.findApiEndpointPath(ep.action, entityName, moduleConfig) || `/api/${entityLower}`;
424
+ const useCaseWithChild = (() => {
425
+ var _a, _b;
426
+ if (!page.useCase)
427
+ return false;
428
+ const [model, action] = page.useCase.split(':');
429
+ return ((_b = (_a = config.useCases[model]) === null || _a === void 0 ? void 0 : _a[action]) === null || _b === void 0 ? void 0 : _b.withChild) === true;
430
+ })();
431
+ const childrenForTemplate = useCaseWithChild && withChildChildren.length > 0 ? withChildChildren : undefined;
432
+ if (page.path === '/' && ((_a = page.useCase) === null || _a === void 0 ? void 0 : _a.endsWith(':list'))) {
433
+ result[page.view] = this.renderListTemplate(resourceName, page.view, fields, basePath, childrenForTemplate);
224
434
  }
225
- }
226
- else {
227
- // 2. Try to infer model from action handler
228
- const inferredModel = this.inferModelFromAction(ep.action, moduleConfig);
229
- if (inferredModel) {
230
- model = inferredModel;
231
- entityName = model.name;
232
- entityLower = entityName.toLowerCase();
233
- fields = model.fields || [];
234
- // Find actual API endpoint or use default
235
- apiBase = this.findApiEndpointPath(ep.action, entityName, moduleConfig) || `/api/${entityLower}`;
435
+ else if (page.path.includes(':id') && !page.path.includes('edit')) {
436
+ result[page.view] = this.renderDetailTemplate(resourceName, page.view, fields, basePath, childrenForTemplate);
236
437
  }
237
- else {
238
- // 3. Use top-level model as fallback
239
- model = topLevelModel;
240
- entityName = defaultEntityName;
241
- entityLower = defaultEntityLower;
242
- fields = model.fields || [];
243
- apiBase = this.findApiEndpointPath(ep.action, entityName, moduleConfig) || `/api/${entityLower}`;
438
+ else if (page.path.includes('/create')) {
439
+ // Find corresponding POST endpoint for onSuccess/onError
440
+ const postEndpoint = resourceConfig.pages.find(p => p.path === page.path && p.method === 'POST');
441
+ result[page.view] = this.renderCreateTemplate(resourceName, page.view, fields, basePath, postEndpoint === null || postEndpoint === void 0 ? void 0 : postEndpoint.onSuccess, postEndpoint === null || postEndpoint === void 0 ? void 0 : postEndpoint.onError, enumValuesMap);
244
442
  }
245
- }
246
- const tplName = ep.view;
247
- let content = '';
248
- switch (ep.action) {
249
- case 'list':
250
- content = (0, viewTemplates_1.renderListTemplate)(entityName, tplName, basePath, fields, apiBase);
251
- break;
252
- case 'get':
253
- // If the path is an edit page or the template name suggests update, render the update form
254
- if (/\/edit$/i.test(ep.path) || /update$/i.test(tplName)) {
255
- content = (0, viewTemplates_1.renderUpdateTemplate)(entityName, tplName, apiBase, fields, strategy, basePath);
256
- }
257
- else {
258
- content = (0, viewTemplates_1.renderDetailTemplate)(entityName, tplName, fields);
259
- }
260
- break;
261
- case 'create':
262
- content = (0, viewTemplates_1.renderCreateTemplate)(entityName, tplName, apiBase, fields, strategy, basePath);
263
- break;
264
- case 'update':
265
- content = (0, viewTemplates_1.renderUpdateTemplate)(entityName, tplName, apiBase, fields, strategy, basePath);
266
- break;
267
- case 'empty':
268
- // treat as create form page without populated values
269
- content = (0, viewTemplates_1.renderCreateTemplate)(entityName, tplName, apiBase, fields, strategy, basePath);
270
- break;
271
- case 'delete':
272
- content = (0, viewTemplates_1.renderDeleteTemplate)(entityName, tplName, apiBase, strategy, basePath);
273
- break;
274
- default:
275
- content = `<!-- @template name="${tplName}" -->\n<pre>{{ JSON.stringify($root, null, 2) }}</pre>\n`;
276
- }
277
- result[tplName] = content;
278
- if (ep.layout && !seenLayouts.has(ep.layout)) {
279
- result[`__layout__::${ep.layout}`] = (0, viewTemplates_1.renderLayoutTemplate)(ep.layout);
280
- seenLayouts.add(ep.layout);
281
- }
282
- }
283
- return result;
284
- }
285
- /**
286
- * Generate templates for a module (handles both single routes object and array)
287
- */
288
- generateForModule(moduleConfig, moduleDir) {
289
- const result = {};
290
- if (!moduleConfig.routes || !moduleConfig.models || moduleConfig.models.length === 0) {
291
- return result;
292
- }
293
- const seenLayouts = new Set();
294
- // Support both single routes object and array (Option D)
295
- const routesArray = Array.isArray(moduleConfig.routes)
296
- ? moduleConfig.routes
297
- : [moduleConfig.routes];
298
- for (const routesConfig of routesArray) {
299
- const templates = this.generateForRoutesConfig(routesConfig, moduleConfig, seenLayouts);
300
- Object.assign(result, templates);
301
- }
443
+ else if (page.path.includes('edit')) {
444
+ // Find corresponding POST endpoint for onSuccess/onError
445
+ const postEndpoint = resourceConfig.pages.find(p => p.path === page.path && p.method === 'POST');
446
+ result[page.view] = this.renderEditTemplate(resourceName, page.view, fields, basePath, postEndpoint === null || postEndpoint === void 0 ? void 0 : postEndpoint.onSuccess, postEndpoint === null || postEndpoint === void 0 ? void 0 : postEndpoint.onError, enumValuesMap);
447
+ }
448
+ });
449
+ });
302
450
  return result;
303
451
  }
304
452
  generateFromYamlFile(yamlFilePath) {
305
- const raw = fs.readFileSync(yamlFilePath, 'utf8');
306
- const config = (0, yaml_1.parse)(raw);
307
- const results = {};
308
- const addModule = (mod, moduleDir) => {
309
- const templates = this.generateForModule(mod, moduleDir);
310
- const viewsDir = path.join(moduleDir, 'views');
311
- for (const [name, contents] of Object.entries(templates)) {
312
- if (name.startsWith('__layout__::')) {
313
- const layoutName = name.substring('__layout__::'.length);
314
- const file = path.join(viewsDir, (0, viewTemplates_1.toFileNameFromTemplateName)(layoutName));
315
- results[`layout:${layoutName}`] = { file, contents };
316
- }
317
- else {
318
- const file = path.join(viewsDir, (0, viewTemplates_1.toFileNameFromTemplateName)(name));
319
- results[name] = { file, contents };
320
- }
321
- }
322
- };
323
- if (config.modules) {
324
- const modules = config.modules;
325
- for (const [key, mod] of Object.entries(modules)) {
326
- const moduleDir = path.dirname(yamlFilePath); // each entry is per-module yaml
327
- addModule(mod, moduleDir);
328
- }
329
- }
330
- else {
331
- const moduleDir = path.dirname(yamlFilePath);
332
- addModule(config, moduleDir);
453
+ const yamlContent = fs.readFileSync(yamlFilePath, 'utf8');
454
+ const config = (0, yaml_1.parse)(yamlContent);
455
+ if (!(0, configTypes_1.isValidModuleConfig)(config)) {
456
+ throw new Error('Configuration does not match new module format. Expected domain/useCases/web structure.');
333
457
  }
334
- return results;
458
+ return this.generateFromConfig(config);
335
459
  }
336
- async generateAndSaveFiles(yamlFilePath, _outputDir, opts) {
337
- const toWrite = this.generateFromYamlFile(yamlFilePath);
338
- for (const { file, contents } of Object.values(toWrite)) {
339
- const dir = path.dirname(file);
340
- fs.mkdirSync(dir, { recursive: true });
460
+ async generateAndSaveFiles(yamlFilePath, moduleDir, opts) {
461
+ const templatesByName = this.generateFromYamlFile(yamlFilePath);
462
+ const viewsDir = path.join(moduleDir, 'views');
463
+ fs.mkdirSync(viewsDir, { recursive: true });
464
+ for (const [name, content] of Object.entries(templatesByName)) {
465
+ const filePath = path.join(viewsDir, `${name}.html`);
341
466
  // eslint-disable-next-line no-await-in-loop
342
- await (0, generationRegistry_1.writeGeneratedFile)(file, contents, { force: !!(opts === null || opts === void 0 ? void 0 : opts.force), skipOnConflict: !!(opts === null || opts === void 0 ? void 0 : opts.skipOnConflict) });
467
+ await (0, generationRegistry_1.writeGeneratedFile)(filePath, content, { force: !!(opts === null || opts === void 0 ? void 0 : opts.force), skipOnConflict: !!(opts === null || opts === void 0 ? void 0 : opts.skipOnConflict) });
343
468
  }
344
469
  // eslint-disable-next-line no-console
345
470
  console.log('\n' + colors_1.colors.green('Template files generated successfully!') + '\n');