@currentjs/gen 0.3.2 → 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.
- package/CHANGELOG.md +10 -611
- package/README.md +623 -427
- package/dist/cli.js +2 -1
- package/dist/commands/commit.js +25 -42
- package/dist/commands/createApp.js +1 -0
- package/dist/commands/createModule.js +151 -45
- package/dist/commands/diff.js +27 -40
- package/dist/commands/generateAll.js +141 -291
- package/dist/commands/migrateCommit.js +6 -18
- package/dist/commands/migratePush.d.ts +1 -0
- package/dist/commands/migratePush.js +135 -0
- package/dist/commands/migrateUpdate.d.ts +1 -0
- package/dist/commands/migrateUpdate.js +147 -0
- package/dist/commands/newGenerateAll.d.ts +4 -0
- package/dist/commands/newGenerateAll.js +336 -0
- package/dist/generators/controllerGenerator.d.ts +43 -19
- package/dist/generators/controllerGenerator.js +547 -329
- package/dist/generators/domainLayerGenerator.d.ts +21 -0
- package/dist/generators/domainLayerGenerator.js +276 -0
- package/dist/generators/dtoGenerator.d.ts +21 -0
- package/dist/generators/dtoGenerator.js +518 -0
- package/dist/generators/newControllerGenerator.d.ts +55 -0
- package/dist/generators/newControllerGenerator.js +644 -0
- package/dist/generators/newServiceGenerator.d.ts +19 -0
- package/dist/generators/newServiceGenerator.js +266 -0
- package/dist/generators/newStoreGenerator.d.ts +39 -0
- package/dist/generators/newStoreGenerator.js +408 -0
- package/dist/generators/newTemplateGenerator.d.ts +29 -0
- package/dist/generators/newTemplateGenerator.js +510 -0
- package/dist/generators/serviceGenerator.d.ts +16 -51
- package/dist/generators/serviceGenerator.js +167 -586
- package/dist/generators/storeGenerator.d.ts +35 -32
- package/dist/generators/storeGenerator.js +291 -238
- package/dist/generators/storeGeneratorV2.d.ts +31 -0
- package/dist/generators/storeGeneratorV2.js +190 -0
- package/dist/generators/templateGenerator.d.ts +21 -21
- package/dist/generators/templateGenerator.js +393 -268
- package/dist/generators/templates/appTemplates.d.ts +3 -1
- package/dist/generators/templates/appTemplates.js +15 -10
- package/dist/generators/templates/data/appYamlTemplate +5 -2
- package/dist/generators/templates/data/cursorRulesTemplate +315 -221
- package/dist/generators/templates/data/frontendScriptTemplate +45 -11
- package/dist/generators/templates/data/mainViewTemplate +1 -1
- package/dist/generators/templates/data/systemTsTemplate +5 -0
- package/dist/generators/templates/index.d.ts +0 -3
- package/dist/generators/templates/index.js +0 -3
- package/dist/generators/templates/newStoreTemplates.d.ts +5 -0
- package/dist/generators/templates/newStoreTemplates.js +141 -0
- package/dist/generators/templates/storeTemplates.d.ts +1 -5
- package/dist/generators/templates/storeTemplates.js +102 -219
- package/dist/generators/templates/viewTemplates.js +1 -1
- package/dist/generators/useCaseGenerator.d.ts +13 -0
- package/dist/generators/useCaseGenerator.js +188 -0
- package/dist/types/configTypes.d.ts +148 -0
- package/dist/types/configTypes.js +10 -0
- package/dist/utils/childEntityUtils.d.ts +18 -0
- package/dist/utils/childEntityUtils.js +78 -0
- package/dist/utils/commandUtils.d.ts +43 -0
- package/dist/utils/commandUtils.js +124 -0
- package/dist/utils/commitUtils.d.ts +4 -1
- package/dist/utils/constants.d.ts +10 -0
- package/dist/utils/constants.js +13 -1
- package/dist/utils/diResolver.d.ts +32 -0
- package/dist/utils/diResolver.js +204 -0
- package/dist/utils/new_parts_of_migrationUtils.d.ts +0 -0
- package/dist/utils/new_parts_of_migrationUtils.js +164 -0
- package/dist/utils/typeUtils.d.ts +19 -0
- package/dist/utils/typeUtils.js +70 -0
- 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
|
-
*
|
|
50
|
+
* Convert a route prefix like "/invoice/:invoiceId/items" into a
|
|
51
|
+
* template-ready path like "/invoice/{{ invoiceId }}/items".
|
|
46
52
|
*/
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
*
|
|
57
|
+
* Replace a single path param in prefix with a template expression (e.g. for list: item.id, for detail: id).
|
|
68
58
|
*/
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
'
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
401
|
+
this.valueObjects = config.domain.valueObjects || {};
|
|
402
|
+
if (!config.web) {
|
|
182
403
|
return result;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
306
|
-
const config = (0, yaml_1.parse)(
|
|
307
|
-
|
|
308
|
-
|
|
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
|
|
458
|
+
return this.generateFromConfig(config);
|
|
335
459
|
}
|
|
336
|
-
async generateAndSaveFiles(yamlFilePath,
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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)(
|
|
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');
|