@currentjs/gen 0.5.0 → 0.5.1
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 -0
- package/dist/cli.js +0 -0
- package/dist/generators/controllerGenerator.d.ts +7 -0
- package/dist/generators/controllerGenerator.js +56 -17
- package/dist/generators/domainLayerGenerator.js +17 -7
- package/dist/generators/dtoGenerator.js +13 -8
- package/dist/generators/serviceGenerator.d.ts +6 -0
- package/dist/generators/serviceGenerator.js +219 -23
- package/dist/generators/storeGenerator.d.ts +4 -0
- package/dist/generators/storeGenerator.js +116 -9
- package/dist/generators/templates/appTemplates.js +1 -1
- package/dist/generators/templates/data/frontendScriptTemplate +11 -4
- package/dist/generators/templates/data/mainViewTemplate +1 -0
- package/dist/generators/templates/storeTemplates.d.ts +1 -1
- package/dist/generators/templates/storeTemplates.js +3 -26
- package/dist/generators/useCaseGenerator.js +6 -3
- package/dist/types/configTypes.d.ts +1 -0
- package/dist/utils/typeUtils.d.ts +4 -0
- package/dist/utils/typeUtils.js +7 -0
- package/package.json +1 -1
- package/dist/commands/migratePush.d.ts +0 -1
- package/dist/commands/migratePush.js +0 -135
- package/dist/commands/migrateUpdate.d.ts +0 -1
- package/dist/commands/migrateUpdate.js +0 -147
- package/dist/commands/newGenerateAll.d.ts +0 -4
- package/dist/commands/newGenerateAll.js +0 -336
- package/dist/generators/domainModelGenerator.d.ts +0 -41
- package/dist/generators/domainModelGenerator.js +0 -242
- package/dist/generators/newControllerGenerator.d.ts +0 -55
- package/dist/generators/newControllerGenerator.js +0 -644
- package/dist/generators/newServiceGenerator.d.ts +0 -19
- package/dist/generators/newServiceGenerator.js +0 -266
- package/dist/generators/newStoreGenerator.d.ts +0 -39
- package/dist/generators/newStoreGenerator.js +0 -408
- package/dist/generators/newTemplateGenerator.d.ts +0 -29
- package/dist/generators/newTemplateGenerator.js +0 -510
- package/dist/generators/storeGeneratorV2.d.ts +0 -31
- package/dist/generators/storeGeneratorV2.js +0 -190
- package/dist/generators/templates/controllerTemplates.d.ts +0 -43
- package/dist/generators/templates/controllerTemplates.js +0 -82
- package/dist/generators/templates/newStoreTemplates.d.ts +0 -5
- package/dist/generators/templates/newStoreTemplates.js +0 -141
- package/dist/generators/templates/serviceTemplates.d.ts +0 -16
- package/dist/generators/templates/serviceTemplates.js +0 -59
- package/dist/generators/templates/validationTemplates.d.ts +0 -25
- package/dist/generators/templates/validationTemplates.js +0 -66
- package/dist/generators/templates/viewTemplates.d.ts +0 -25
- package/dist/generators/templates/viewTemplates.js +0 -491
- package/dist/generators/validationGenerator.d.ts +0 -29
- package/dist/generators/validationGenerator.js +0 -250
- package/dist/utils/new_parts_of_migrationUtils.d.ts +0 -0
- package/dist/utils/new_parts_of_migrationUtils.js +0 -164
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.1] - 2026-02-28
|
|
4
|
+
|
|
5
|
+
- fixes pack:
|
|
6
|
+
- aggregate references
|
|
7
|
+
- layout setting in module config
|
|
8
|
+
- SPA layout mismatch bug (if a page uses other layout, its loaded inside current one)
|
|
9
|
+
- local provider imports bug
|
|
10
|
+
- types in generated services (was any)
|
|
11
|
+
- access control issues
|
|
12
|
+
|
|
3
13
|
## [0.5.0] – 2026-02-26
|
|
4
14
|
|
|
5
15
|
So many changes were made that we skipped version 0.4:
|
package/dist/cli.js
CHANGED
|
File without changes
|
|
@@ -43,6 +43,13 @@ export declare class ControllerGenerator {
|
|
|
43
43
|
* meant for static routes (e.g. /create).
|
|
44
44
|
*/
|
|
45
45
|
private sortRoutesBySpecificity;
|
|
46
|
+
/**
|
|
47
|
+
* Resolve layout from YAML value.
|
|
48
|
+
* - undefined => use fallback (if provided)
|
|
49
|
+
* - "none" or "" => no layout
|
|
50
|
+
* - other values => use explicit layout name
|
|
51
|
+
*/
|
|
52
|
+
private resolveLayout;
|
|
46
53
|
private generateApiController;
|
|
47
54
|
private generateWebController;
|
|
48
55
|
generateFromConfig(config: ModuleConfig): Record<string, string>;
|
|
@@ -244,18 +244,26 @@ class ControllerGenerator {
|
|
|
244
244
|
}
|
|
245
245
|
`;
|
|
246
246
|
}
|
|
247
|
-
generateApiEndpointMethod(endpoint, resourceName, childInfo) {
|
|
247
|
+
generateApiEndpointMethod(endpoint, resourceName, useCasesConfig, childInfo) {
|
|
248
|
+
var _a;
|
|
248
249
|
const { model, action } = this.parseUseCase(endpoint.useCase);
|
|
249
250
|
const methodName = action;
|
|
250
251
|
const decorator = this.getHttpDecorator(endpoint.method);
|
|
251
252
|
const useCaseVar = `${model.toLowerCase()}UseCase`;
|
|
252
253
|
const inputClass = `${model}${(0, typeUtils_1.capitalize)(action)}Input`;
|
|
253
254
|
const outputClass = `${model}${(0, typeUtils_1.capitalize)(action)}Output`;
|
|
255
|
+
const useCaseDef = (_a = useCasesConfig[model]) === null || _a === void 0 ? void 0 : _a[action];
|
|
256
|
+
const isVoidOutput = !(useCaseDef === null || useCaseDef === void 0 ? void 0 : useCaseDef.output) || useCaseDef.output === 'void';
|
|
254
257
|
const dtoImports = new Set();
|
|
258
|
+
const voidOutputDtos = new Set();
|
|
255
259
|
dtoImports.add(`${model}${(0, typeUtils_1.capitalize)(action)}`);
|
|
260
|
+
if (isVoidOutput) {
|
|
261
|
+
voidOutputDtos.add(`${model}${(0, typeUtils_1.capitalize)(action)}`);
|
|
262
|
+
}
|
|
256
263
|
// Generate auth check (pre-fetch)
|
|
257
264
|
const authCheck = this.generateAuthCheck(endpoint.auth);
|
|
258
265
|
const authLine = authCheck ? `\n ${authCheck}\n` : '';
|
|
266
|
+
const hasOwner = this.hasOwnerAuth(endpoint.auth);
|
|
259
267
|
// Build parsing logic
|
|
260
268
|
// For create: root gets ownerId from user, child gets parentId from URL params
|
|
261
269
|
let parseLogic;
|
|
@@ -282,7 +290,6 @@ class ControllerGenerator {
|
|
|
282
290
|
// Generate owner checks:
|
|
283
291
|
// - For mutations (update, delete): PRE-mutation check (before operation)
|
|
284
292
|
// - For reads (get): POST-fetch check (after fetching)
|
|
285
|
-
const hasOwner = this.hasOwnerAuth(endpoint.auth);
|
|
286
293
|
const isMutation = action === 'update' || action === 'delete';
|
|
287
294
|
const isRead = action === 'get';
|
|
288
295
|
// Pre-mutation owner check for write operations
|
|
@@ -295,22 +302,25 @@ class ControllerGenerator {
|
|
|
295
302
|
: '';
|
|
296
303
|
// Generate output transformation based on action
|
|
297
304
|
let outputTransform;
|
|
298
|
-
if (action === '
|
|
299
|
-
outputTransform = `return ${outputClass}.from(result);`;
|
|
300
|
-
}
|
|
301
|
-
else if (action === 'delete') {
|
|
305
|
+
if (isVoidOutput || action === 'delete') {
|
|
302
306
|
outputTransform = `return result;`;
|
|
303
307
|
}
|
|
308
|
+
else if (action === 'list') {
|
|
309
|
+
outputTransform = `return ${outputClass}.from(result);`;
|
|
310
|
+
}
|
|
304
311
|
else {
|
|
305
312
|
outputTransform = `return ${outputClass}.from(result);`;
|
|
306
313
|
}
|
|
314
|
+
const useCaseArgs = (hasOwner && action === 'list')
|
|
315
|
+
? 'input, context.request.user?.id as number'
|
|
316
|
+
: 'input';
|
|
307
317
|
const method = ` @${decorator}('${endpoint.path}')
|
|
308
318
|
async ${methodName}(context: IContext): Promise<any> {${authLine}
|
|
309
319
|
${parseLogic}${preMutationOwnerCheck}
|
|
310
|
-
const result = await this.${useCaseVar}.${action}(
|
|
320
|
+
const result = await this.${useCaseVar}.${action}(${useCaseArgs});${postFetchOwnerCheck}
|
|
311
321
|
${outputTransform}
|
|
312
322
|
}`;
|
|
313
|
-
return { method, dtoImports };
|
|
323
|
+
return { method, dtoImports, voidOutputDtos };
|
|
314
324
|
}
|
|
315
325
|
generateWebPageMethod(page, resourceName, layout, methodIndex, childInfo, withChildChildren) {
|
|
316
326
|
const method = page.method || 'GET';
|
|
@@ -333,12 +343,16 @@ class ControllerGenerator {
|
|
|
333
343
|
const authLine = authCheck ? `\n ${authCheck}\n` : '';
|
|
334
344
|
// For GET requests with views (display pages)
|
|
335
345
|
if (method === 'GET' && page.view) {
|
|
336
|
-
const
|
|
346
|
+
const pageLayout = this.resolveLayout(page.layout, layout);
|
|
347
|
+
const renderDecorator = pageLayout
|
|
348
|
+
? `\n @Render("${page.view}", "${pageLayout}")`
|
|
349
|
+
: `\n @Render("${page.view}")`;
|
|
337
350
|
if (page.useCase) {
|
|
338
351
|
const { model, action } = this.parseUseCase(page.useCase);
|
|
339
352
|
const useCaseVar = `${model.toLowerCase()}UseCase`;
|
|
340
353
|
const inputClass = `${model}${(0, typeUtils_1.capitalize)(action)}Input`;
|
|
341
354
|
dtoImports.add(`${model}${(0, typeUtils_1.capitalize)(action)}`);
|
|
355
|
+
const hasOwner = this.hasOwnerAuth(page.auth);
|
|
342
356
|
let parseLogic;
|
|
343
357
|
if (page.path.includes(':id')) {
|
|
344
358
|
parseLogic = `const input = ${inputClass}.parse({ id: context.request.parameters.id });`;
|
|
@@ -349,8 +363,6 @@ class ControllerGenerator {
|
|
|
349
363
|
else {
|
|
350
364
|
parseLogic = `const input = ${inputClass}.parse({});`;
|
|
351
365
|
}
|
|
352
|
-
// Generate post-fetch owner check for GET pages (reads only)
|
|
353
|
-
const hasOwner = this.hasOwnerAuth(page.auth);
|
|
354
366
|
const isReadAction = action === 'get' || action === 'list';
|
|
355
367
|
const postFetchOwnerCheck = (hasOwner && isReadAction)
|
|
356
368
|
? this.generatePostFetchOwnerCheck(page.auth, 'result', useCaseVar, childInfo)
|
|
@@ -374,12 +386,15 @@ class ControllerGenerator {
|
|
|
374
386
|
else {
|
|
375
387
|
returnExpr = 'result';
|
|
376
388
|
}
|
|
389
|
+
const useCaseArgs = (hasOwner && action === 'list')
|
|
390
|
+
? 'input, context.request.user?.id as number'
|
|
391
|
+
: 'input';
|
|
377
392
|
const loadChildCode = loadChildBlocks.length ? '\n ' + loadChildBlocks.join('\n ') + '\n ' : '';
|
|
378
393
|
const methodCode = `${renderDecorator}
|
|
379
394
|
@${decorator}('${page.path}')
|
|
380
395
|
async ${methodName}(context: IContext): Promise<any> {${authLine}
|
|
381
396
|
${parseLogic}
|
|
382
|
-
const result = await this.${useCaseVar}.${action}(
|
|
397
|
+
const result = await this.${useCaseVar}.${action}(${useCaseArgs});${postFetchOwnerCheck}${loadChildCode}
|
|
383
398
|
return ${returnExpr};
|
|
384
399
|
}`;
|
|
385
400
|
return { method: methodCode, dtoImports };
|
|
@@ -491,26 +506,49 @@ class ControllerGenerator {
|
|
|
491
506
|
return aParamCount - bParamCount;
|
|
492
507
|
});
|
|
493
508
|
}
|
|
494
|
-
|
|
509
|
+
/**
|
|
510
|
+
* Resolve layout from YAML value.
|
|
511
|
+
* - undefined => use fallback (if provided)
|
|
512
|
+
* - "none" or "" => no layout
|
|
513
|
+
* - other values => use explicit layout name
|
|
514
|
+
*/
|
|
515
|
+
resolveLayout(layout, fallback) {
|
|
516
|
+
if (layout === undefined) {
|
|
517
|
+
return fallback;
|
|
518
|
+
}
|
|
519
|
+
const normalized = layout.trim();
|
|
520
|
+
if (!normalized || normalized.toLowerCase() === 'none') {
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
return normalized;
|
|
524
|
+
}
|
|
525
|
+
generateApiController(resourceName, prefix, endpoints, useCasesConfig, childInfo) {
|
|
495
526
|
const controllerName = `${resourceName}ApiController`;
|
|
496
527
|
// Determine which use cases and DTOs are referenced
|
|
497
528
|
const useCaseModels = new Set();
|
|
498
529
|
const allDtoImports = new Set();
|
|
530
|
+
const allVoidOutputDtos = new Set();
|
|
499
531
|
const methods = [];
|
|
500
532
|
const sortedEndpoints = this.sortRoutesBySpecificity(endpoints);
|
|
501
533
|
sortedEndpoints.forEach(endpoint => {
|
|
502
534
|
const { model } = this.parseUseCase(endpoint.useCase);
|
|
503
535
|
useCaseModels.add(model);
|
|
504
|
-
const { method, dtoImports } = this.generateApiEndpointMethod(endpoint, resourceName, childInfo);
|
|
536
|
+
const { method, dtoImports, voidOutputDtos } = this.generateApiEndpointMethod(endpoint, resourceName, useCasesConfig, childInfo);
|
|
505
537
|
methods.push(method);
|
|
506
538
|
dtoImports.forEach(d => allDtoImports.add(d));
|
|
539
|
+
voidOutputDtos.forEach(d => allVoidOutputDtos.add(d));
|
|
507
540
|
});
|
|
508
541
|
// Generate imports
|
|
509
542
|
const useCaseImports = Array.from(useCaseModels)
|
|
510
543
|
.map(model => `import { ${model}UseCase } from '../../application/useCases/${model}UseCase';`)
|
|
511
544
|
.join('\n');
|
|
512
545
|
const dtoImportStatements = Array.from(allDtoImports)
|
|
513
|
-
.map(dto =>
|
|
546
|
+
.map(dto => {
|
|
547
|
+
if (allVoidOutputDtos.has(dto)) {
|
|
548
|
+
return `import { ${dto}Input } from '../../application/dto/${dto}';`;
|
|
549
|
+
}
|
|
550
|
+
return `import { ${dto}Input, ${dto}Output } from '../../application/dto/${dto}';`;
|
|
551
|
+
})
|
|
514
552
|
.join('\n');
|
|
515
553
|
// Generate constructor parameters
|
|
516
554
|
const constructorParams = Array.from(useCaseModels)
|
|
@@ -602,7 +640,7 @@ ${methods.join('\n\n')}
|
|
|
602
640
|
if (config.api) {
|
|
603
641
|
Object.entries(config.api).forEach(([resourceName, resourceConfig]) => {
|
|
604
642
|
const childInfo = childEntityMap.get(resourceName);
|
|
605
|
-
const code = this.generateApiController(resourceName, resourceConfig.prefix, resourceConfig.endpoints, childInfo);
|
|
643
|
+
const code = this.generateApiController(resourceName, resourceConfig.prefix, resourceConfig.endpoints, config.useCases, childInfo);
|
|
606
644
|
result[`${resourceName}Api`] = code;
|
|
607
645
|
});
|
|
608
646
|
}
|
|
@@ -610,7 +648,8 @@ ${methods.join('\n\n')}
|
|
|
610
648
|
if (config.web) {
|
|
611
649
|
Object.entries(config.web).forEach(([resourceName, resourceConfig]) => {
|
|
612
650
|
const childInfo = childEntityMap.get(resourceName);
|
|
613
|
-
const
|
|
651
|
+
const moduleLayout = this.resolveLayout(resourceConfig.layout, 'main_view');
|
|
652
|
+
const code = this.generateWebController(resourceName, resourceConfig.prefix, moduleLayout, resourceConfig.pages, config, childInfo);
|
|
614
653
|
result[`${resourceName}Web`] = code;
|
|
615
654
|
});
|
|
616
655
|
}
|
|
@@ -146,7 +146,14 @@ class DomainLayerGenerator {
|
|
|
146
146
|
})
|
|
147
147
|
.filter((imp, idx, arr) => arr.indexOf(imp) === idx) // dedupe
|
|
148
148
|
.join('\n');
|
|
149
|
-
|
|
149
|
+
// Generate imports for aggregate references in fields (e.g. idea: { type: Idea })
|
|
150
|
+
const aggregateRefImports = fields
|
|
151
|
+
.filter(([, fieldConfig]) => (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates) &&
|
|
152
|
+
fieldConfig.type !== name)
|
|
153
|
+
.map(([, fieldConfig]) => `import { ${fieldConfig.type} } from './${fieldConfig.type}';`)
|
|
154
|
+
.filter((imp, idx, arr) => arr.indexOf(imp) === idx)
|
|
155
|
+
.join('\n');
|
|
156
|
+
const imports = [entityImports, valueObjectImports, aggregateRefImports].filter(Boolean).join('\n');
|
|
150
157
|
// Generate constructor parameters: id, then ownerId (root) or parentId field (child)
|
|
151
158
|
const constructorParams = ['public id: number'];
|
|
152
159
|
if (childInfo) {
|
|
@@ -157,9 +164,12 @@ class DomainLayerGenerator {
|
|
|
157
164
|
}
|
|
158
165
|
// Sort fields: required first, then optional
|
|
159
166
|
// Fields are required by default unless required: false
|
|
167
|
+
// Aggregate references are always treated as optional (store can't populate them from FK alone)
|
|
160
168
|
const sortedFields = fields.sort((a, b) => {
|
|
161
|
-
const
|
|
162
|
-
const
|
|
169
|
+
const aIsAggRef = (0, typeUtils_1.isAggregateReference)(a[1].type, this.availableAggregates);
|
|
170
|
+
const bIsAggRef = (0, typeUtils_1.isAggregateReference)(b[1].type, this.availableAggregates);
|
|
171
|
+
const aRequired = a[1].required !== false && !a[1].auto && !aIsAggRef;
|
|
172
|
+
const bRequired = b[1].required !== false && !b[1].auto && !bIsAggRef;
|
|
163
173
|
if (aRequired === bRequired)
|
|
164
174
|
return 0;
|
|
165
175
|
return aRequired ? -1 : 1;
|
|
@@ -176,9 +186,9 @@ class DomainLayerGenerator {
|
|
|
176
186
|
}
|
|
177
187
|
});
|
|
178
188
|
sortedFields.forEach(([fieldName, fieldConfig]) => {
|
|
189
|
+
const isAggRef = (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates);
|
|
179
190
|
const tsType = enumTypeNames[fieldName] || this.mapType(fieldConfig.type);
|
|
180
|
-
|
|
181
|
-
const isOptional = fieldConfig.required === false;
|
|
191
|
+
const isOptional = fieldConfig.required === false || isAggRef;
|
|
182
192
|
const hasDefault = fieldConfig.auto;
|
|
183
193
|
let param = `public ${fieldName}`;
|
|
184
194
|
if (isOptional && !hasDefault) {
|
|
@@ -195,10 +205,10 @@ class DomainLayerGenerator {
|
|
|
195
205
|
const setterMethods = sortedFields
|
|
196
206
|
.filter(([fieldName, fieldConfig]) => !fieldConfig.auto && fieldName !== 'id')
|
|
197
207
|
.map(([fieldName, fieldConfig]) => {
|
|
208
|
+
const isAggRef = (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates);
|
|
198
209
|
const tsType = enumTypeNames[fieldName] || this.mapType(fieldConfig.type);
|
|
199
210
|
const methodName = `set${(0, typeUtils_1.capitalize)(fieldName)}`;
|
|
200
|
-
|
|
201
|
-
const isOptional = fieldConfig.required === false;
|
|
211
|
+
const isOptional = fieldConfig.required === false || isAggRef;
|
|
202
212
|
return `
|
|
203
213
|
${methodName}(${fieldName}: ${tsType}${isOptional ? ' | undefined' : ''}): void {
|
|
204
214
|
this.${fieldName} = ${fieldName};
|
|
@@ -171,15 +171,17 @@ class DtoGenerator {
|
|
|
171
171
|
fieldsToInclude.forEach(([fieldName, fieldConfig]) => {
|
|
172
172
|
if (fieldName === 'id' || fieldConfig.auto)
|
|
173
173
|
return;
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
const
|
|
174
|
+
const isAggRef = (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates);
|
|
175
|
+
const tsType = isAggRef ? 'number' : this.mapType(fieldConfig.type);
|
|
176
|
+
const effectiveFieldType = isAggRef ? 'number' : fieldConfig.type;
|
|
177
|
+
// Aggregate references are always optional in DTOs; other fields default to required
|
|
178
|
+
const isRequired = !isAggRef && !inputConfig.partial && fieldConfig.required !== false;
|
|
177
179
|
const optional = isRequired ? '' : '?';
|
|
178
180
|
fieldDeclarations.push(` readonly ${fieldName}${optional}: ${tsType};`);
|
|
179
181
|
constructorParams.push(`${fieldName}${optional}: ${tsType}`);
|
|
180
182
|
constructorAssignments.push(` this.${fieldName} = ${fieldName};`);
|
|
181
|
-
validationChecks.push(...this.getValidationCode(fieldName,
|
|
182
|
-
fieldTransforms.push(` ${fieldName}: ${this.getTransformCode(fieldName,
|
|
183
|
+
validationChecks.push(...this.getValidationCode(fieldName, effectiveFieldType, isRequired));
|
|
184
|
+
fieldTransforms.push(` ${fieldName}: ${this.getTransformCode(fieldName, effectiveFieldType)}`);
|
|
183
185
|
});
|
|
184
186
|
}
|
|
185
187
|
// Handle filters
|
|
@@ -267,12 +269,15 @@ ${transformsStr}
|
|
|
267
269
|
fieldsToInclude.forEach(([fieldName, fieldConfig]) => {
|
|
268
270
|
if (fieldName === 'id')
|
|
269
271
|
return;
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
+
const isAggRef = (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates);
|
|
273
|
+
const tsType = isAggRef ? 'number' : this.mapType(fieldConfig.type);
|
|
274
|
+
const isOptional = fieldConfig.required === false || isAggRef;
|
|
272
275
|
const optional = isOptional ? '?' : '';
|
|
273
276
|
fieldDeclarations.push(` readonly ${fieldName}${optional}: ${tsType};`);
|
|
274
277
|
constructorParams.push(`${fieldName}${optional}: ${tsType}`);
|
|
275
|
-
fromMappings.push(
|
|
278
|
+
fromMappings.push(isAggRef
|
|
279
|
+
? ` ${fieldName}: entity.${fieldName}?.id`
|
|
280
|
+
: ` ${fieldName}: entity.${fieldName}`);
|
|
276
281
|
});
|
|
277
282
|
}
|
|
278
283
|
// Handle includes (nested objects)
|
|
@@ -2,6 +2,12 @@ import { ModuleConfig } from '../types/configTypes';
|
|
|
2
2
|
export declare class ServiceGenerator {
|
|
3
3
|
private availableAggregates;
|
|
4
4
|
private mapType;
|
|
5
|
+
private getDefaultHandlerReturnType;
|
|
6
|
+
private buildHandlerContextMap;
|
|
7
|
+
private deriveInputType;
|
|
8
|
+
private deriveCustomHandlerTypes;
|
|
9
|
+
private getInputDtoFields;
|
|
10
|
+
private computeDtoFieldsForHandler;
|
|
5
11
|
private generateListHandler;
|
|
6
12
|
private generateGetHandler;
|
|
7
13
|
private generateCreateHandler;
|
|
@@ -49,13 +49,146 @@ class ServiceGenerator {
|
|
|
49
49
|
mapType(yamlType) {
|
|
50
50
|
return (0, typeUtils_1.mapType)(yamlType, this.availableAggregates);
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
getDefaultHandlerReturnType(actionName, modelName) {
|
|
53
|
+
switch (actionName) {
|
|
54
|
+
case 'create':
|
|
55
|
+
case 'get':
|
|
56
|
+
case 'update':
|
|
57
|
+
return modelName;
|
|
58
|
+
case 'delete':
|
|
59
|
+
return '{ success: boolean; message: string }';
|
|
60
|
+
case 'list':
|
|
61
|
+
return `{ items: ${modelName}[]; total: number; page: number; limit: number }`;
|
|
62
|
+
default:
|
|
63
|
+
return modelName;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
buildHandlerContextMap(modelName, useCases) {
|
|
67
|
+
const contextMap = new Map();
|
|
68
|
+
Object.entries(useCases).forEach(([actionName, useCaseConfig]) => {
|
|
69
|
+
const inputDtoType = `${modelName}${(0, typeUtils_1.capitalize)(actionName)}Input`;
|
|
70
|
+
let useCaseReturnType;
|
|
71
|
+
if (useCaseConfig.output === 'void') {
|
|
72
|
+
useCaseReturnType = '{ success: boolean; message: string }';
|
|
73
|
+
}
|
|
74
|
+
else if (actionName === 'list') {
|
|
75
|
+
useCaseReturnType = `{ items: ${modelName}[]; total: number; page: number; limit: number }`;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
useCaseReturnType = modelName;
|
|
79
|
+
}
|
|
80
|
+
useCaseConfig.handlers.forEach((handler, index) => {
|
|
81
|
+
const isFirst = index === 0;
|
|
82
|
+
const isLast = index === useCaseConfig.handlers.length - 1;
|
|
83
|
+
let prevHandlerReturnType = null;
|
|
84
|
+
if (!isFirst) {
|
|
85
|
+
const prevHandler = useCaseConfig.handlers[index - 1];
|
|
86
|
+
if (prevHandler.startsWith('default:')) {
|
|
87
|
+
prevHandlerReturnType = this.getDefaultHandlerReturnType(prevHandler.replace('default:', ''), modelName);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
prevHandlerReturnType = modelName;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const context = {
|
|
94
|
+
actionName,
|
|
95
|
+
index,
|
|
96
|
+
isFirst,
|
|
97
|
+
isLast,
|
|
98
|
+
prevHandlerReturnType,
|
|
99
|
+
inputDtoType,
|
|
100
|
+
useCaseReturnType,
|
|
101
|
+
inputConfig: useCaseConfig.input
|
|
102
|
+
};
|
|
103
|
+
const existing = contextMap.get(handler) || [];
|
|
104
|
+
existing.push(context);
|
|
105
|
+
contextMap.set(handler, existing);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
return contextMap;
|
|
109
|
+
}
|
|
110
|
+
deriveInputType(contexts) {
|
|
111
|
+
const inputTypes = [...new Set(contexts.map(c => c.inputDtoType))];
|
|
112
|
+
return inputTypes.join(' | ');
|
|
113
|
+
}
|
|
114
|
+
deriveCustomHandlerTypes(contexts, modelName) {
|
|
115
|
+
const inputTypes = [...new Set(contexts.map(c => c.inputDtoType))];
|
|
116
|
+
const resultTypeParts = new Set();
|
|
117
|
+
contexts.forEach(c => {
|
|
118
|
+
if (c.isFirst) {
|
|
119
|
+
resultTypeParts.add('null');
|
|
120
|
+
}
|
|
121
|
+
if (c.prevHandlerReturnType) {
|
|
122
|
+
resultTypeParts.add(c.prevHandlerReturnType);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
const returnTypeParts = new Set();
|
|
126
|
+
contexts.forEach(c => {
|
|
127
|
+
if (c.isLast) {
|
|
128
|
+
returnTypeParts.add(c.useCaseReturnType);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
returnTypeParts.add(modelName);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
inputType: inputTypes.join(' | '),
|
|
136
|
+
resultType: [...resultTypeParts].join(' | '),
|
|
137
|
+
returnType: [...returnTypeParts].join(' | ')
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
getInputDtoFields(inputConfig, aggregateConfig, childInfo) {
|
|
141
|
+
const fields = new Set();
|
|
142
|
+
if (!inputConfig)
|
|
143
|
+
return fields;
|
|
144
|
+
if (!inputConfig.identifier && !inputConfig.partial) {
|
|
145
|
+
fields.add(childInfo ? childInfo.parentIdField : 'ownerId');
|
|
146
|
+
}
|
|
147
|
+
if (inputConfig.from) {
|
|
148
|
+
let fieldNames = Object.keys(aggregateConfig.fields)
|
|
149
|
+
.filter(f => !aggregateConfig.fields[f].auto && f !== 'id');
|
|
150
|
+
if (inputConfig.pick && inputConfig.pick.length > 0) {
|
|
151
|
+
fieldNames = fieldNames.filter(f => inputConfig.pick.includes(f));
|
|
152
|
+
}
|
|
153
|
+
if (inputConfig.omit && inputConfig.omit.length > 0) {
|
|
154
|
+
fieldNames = fieldNames.filter(f => !inputConfig.omit.includes(f));
|
|
155
|
+
}
|
|
156
|
+
fieldNames.forEach(f => fields.add(f));
|
|
157
|
+
}
|
|
158
|
+
if (inputConfig.add) {
|
|
159
|
+
Object.keys(inputConfig.add).forEach(f => fields.add(f));
|
|
160
|
+
}
|
|
161
|
+
return fields;
|
|
162
|
+
}
|
|
163
|
+
computeDtoFieldsForHandler(contexts, aggregateConfig, childInfo) {
|
|
164
|
+
const fieldSets = contexts.map(ctx => this.getInputDtoFields(ctx.inputConfig, aggregateConfig, childInfo));
|
|
165
|
+
if (fieldSets.length === 0) {
|
|
166
|
+
return new Set(Object.keys(aggregateConfig.fields).filter(f => !aggregateConfig.fields[f].auto && f !== 'id'));
|
|
167
|
+
}
|
|
168
|
+
const result = new Set(fieldSets[0]);
|
|
169
|
+
for (let i = 1; i < fieldSets.length; i++) {
|
|
170
|
+
for (const field of result) {
|
|
171
|
+
if (!fieldSets[i].has(field)) {
|
|
172
|
+
result.delete(field);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
generateListHandler(modelName, storeName, hasPagination) {
|
|
179
|
+
const returnType = `{ items: ${modelName}[]; total: number; page: number; limit: number }`;
|
|
180
|
+
if (hasPagination) {
|
|
181
|
+
return ` async list(page: number = 1, limit: number = 20, ownerId?: number): Promise<${returnType}> {
|
|
54
182
|
const [items, total] = await Promise.all([
|
|
55
|
-
this.${storeName}.
|
|
56
|
-
this.${storeName}.count()
|
|
183
|
+
this.${storeName}.getPaginated(page, limit, ownerId),
|
|
184
|
+
this.${storeName}.count(ownerId)
|
|
57
185
|
]);
|
|
58
186
|
return { items, total, page, limit };
|
|
187
|
+
}`;
|
|
188
|
+
}
|
|
189
|
+
return ` async list(ownerId?: number): Promise<${returnType}> {
|
|
190
|
+
const items = await this.${storeName}.getAll(ownerId);
|
|
191
|
+
return { items, total: items.length, page: 1, limit: items.length };
|
|
59
192
|
}`;
|
|
60
193
|
}
|
|
61
194
|
generateGetHandler(modelName, storeName, entityLower) {
|
|
@@ -67,35 +200,60 @@ class ServiceGenerator {
|
|
|
67
200
|
return ${entityLower};
|
|
68
201
|
}`;
|
|
69
202
|
}
|
|
70
|
-
generateCreateHandler(modelName, storeName, entityLower, aggregateConfig, childInfo) {
|
|
203
|
+
generateCreateHandler(modelName, storeName, entityLower, aggregateConfig, childInfo, inputType, dtoFields) {
|
|
71
204
|
const firstArgField = childInfo ? childInfo.parentIdField : 'ownerId';
|
|
72
205
|
const fields = Object.entries(aggregateConfig.fields)
|
|
73
206
|
.filter(([fieldName, fieldConfig]) => !fieldConfig.auto && fieldName !== 'id')
|
|
74
207
|
.sort((a, b) => {
|
|
75
|
-
const
|
|
76
|
-
const
|
|
208
|
+
const aIsAggRef = (0, typeUtils_1.isAggregateReference)(a[1].type, this.availableAggregates);
|
|
209
|
+
const bIsAggRef = (0, typeUtils_1.isAggregateReference)(b[1].type, this.availableAggregates);
|
|
210
|
+
const aRequired = a[1].required !== false && !aIsAggRef;
|
|
211
|
+
const bRequired = b[1].required !== false && !bIsAggRef;
|
|
77
212
|
if (aRequired === bRequired)
|
|
78
213
|
return 0;
|
|
79
214
|
return aRequired ? -1 : 1;
|
|
80
215
|
});
|
|
81
|
-
const fieldArgs = fields.map(([fieldName]) =>
|
|
216
|
+
const fieldArgs = fields.map(([fieldName, fieldConfig]) => {
|
|
217
|
+
if (!dtoFields.has(fieldName)) {
|
|
218
|
+
return 'undefined';
|
|
219
|
+
}
|
|
220
|
+
if ((0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates)) {
|
|
221
|
+
return `input.${fieldName} != null ? ({ id: input.${fieldName} } as unknown as ${fieldConfig.type}) : undefined`;
|
|
222
|
+
}
|
|
223
|
+
if (fieldConfig.type === 'enum' && fieldConfig.values && fieldConfig.values.length > 0) {
|
|
224
|
+
const enumTypeName = `${modelName}${(0, typeUtils_1.capitalize)(fieldName)}`;
|
|
225
|
+
return `input.${fieldName} as ${enumTypeName}`;
|
|
226
|
+
}
|
|
227
|
+
return `input.${fieldName}`;
|
|
228
|
+
}).join(', ');
|
|
82
229
|
const constructorArgs = `input.${firstArgField}, ${fieldArgs}`;
|
|
83
|
-
return ` async create(input:
|
|
230
|
+
return ` async create(input: ${inputType}): Promise<${modelName}> {
|
|
84
231
|
const ${entityLower} = new ${modelName}(0, ${constructorArgs});
|
|
85
232
|
return await this.${storeName}.insert(${entityLower});
|
|
86
233
|
}`;
|
|
87
234
|
}
|
|
88
|
-
generateUpdateHandler(modelName, storeName, aggregateConfig) {
|
|
235
|
+
generateUpdateHandler(modelName, storeName, aggregateConfig, inputType, dtoFields) {
|
|
89
236
|
const setterCalls = Object.entries(aggregateConfig.fields)
|
|
90
|
-
.filter(([fieldName, fieldConfig]) => !fieldConfig.auto && fieldName !== 'id')
|
|
91
|
-
.map(([fieldName]) => {
|
|
237
|
+
.filter(([fieldName, fieldConfig]) => !fieldConfig.auto && fieldName !== 'id' && dtoFields.has(fieldName))
|
|
238
|
+
.map(([fieldName, fieldConfig]) => {
|
|
92
239
|
const methodName = `set${(0, typeUtils_1.capitalize)(fieldName)}`;
|
|
240
|
+
if ((0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates)) {
|
|
241
|
+
return ` if (input.${fieldName} !== undefined) {
|
|
242
|
+
existing${modelName}.${methodName}(input.${fieldName} != null ? ({ id: input.${fieldName} } as unknown as ${fieldConfig.type}) : undefined);
|
|
243
|
+
}`;
|
|
244
|
+
}
|
|
245
|
+
if (fieldConfig.type === 'enum' && fieldConfig.values && fieldConfig.values.length > 0) {
|
|
246
|
+
const enumTypeName = `${modelName}${(0, typeUtils_1.capitalize)(fieldName)}`;
|
|
247
|
+
return ` if (input.${fieldName} !== undefined) {
|
|
248
|
+
existing${modelName}.${methodName}(input.${fieldName} as ${enumTypeName});
|
|
249
|
+
}`;
|
|
250
|
+
}
|
|
93
251
|
return ` if (input.${fieldName} !== undefined) {
|
|
94
252
|
existing${modelName}.${methodName}(input.${fieldName});
|
|
95
253
|
}`;
|
|
96
254
|
})
|
|
97
255
|
.join('\n');
|
|
98
|
-
return ` async update(id: number, input:
|
|
256
|
+
return ` async update(id: number, input: ${inputType}): Promise<${modelName}> {
|
|
99
257
|
const existing${modelName} = await this.${storeName}.getById(id);
|
|
100
258
|
if (!existing${modelName}) {
|
|
101
259
|
throw new Error('${modelName} not found');
|
|
@@ -115,29 +273,30 @@ ${setterCalls}
|
|
|
115
273
|
return { success: true, message: '${modelName} deleted successfully' };
|
|
116
274
|
}`;
|
|
117
275
|
}
|
|
118
|
-
generateDefaultHandlerMethod(modelName, actionName, aggregateConfig, childInfo) {
|
|
276
|
+
generateDefaultHandlerMethod(modelName, actionName, aggregateConfig, childInfo, inputType, dtoFields, listConfig) {
|
|
277
|
+
var _a;
|
|
119
278
|
const entityLower = modelName.toLowerCase();
|
|
120
279
|
const storeName = `${entityLower}Store`;
|
|
121
280
|
switch (actionName) {
|
|
122
281
|
case 'list':
|
|
123
|
-
return this.generateListHandler(modelName, storeName);
|
|
282
|
+
return this.generateListHandler(modelName, storeName, (_a = listConfig === null || listConfig === void 0 ? void 0 : listConfig.hasPagination) !== null && _a !== void 0 ? _a : true);
|
|
124
283
|
case 'get':
|
|
125
284
|
return this.generateGetHandler(modelName, storeName, entityLower);
|
|
126
285
|
case 'create':
|
|
127
|
-
return this.generateCreateHandler(modelName, storeName, entityLower, aggregateConfig, childInfo);
|
|
286
|
+
return this.generateCreateHandler(modelName, storeName, entityLower, aggregateConfig, childInfo, inputType, dtoFields);
|
|
128
287
|
case 'update':
|
|
129
|
-
return this.generateUpdateHandler(modelName, storeName, aggregateConfig);
|
|
288
|
+
return this.generateUpdateHandler(modelName, storeName, aggregateConfig, inputType, dtoFields);
|
|
130
289
|
case 'delete':
|
|
131
290
|
return this.generateDeleteHandler(modelName, storeName);
|
|
132
291
|
default:
|
|
133
|
-
return ` async ${actionName}(input:
|
|
292
|
+
return ` async ${actionName}(input: ${inputType}): Promise<${modelName}> {
|
|
134
293
|
// TODO: Implement default ${actionName} handler
|
|
135
294
|
throw new Error('Not implemented');
|
|
136
295
|
}`;
|
|
137
296
|
}
|
|
138
297
|
}
|
|
139
|
-
generateCustomHandlerMethod(modelName, handlerName) {
|
|
140
|
-
return ` async ${handlerName}(result:
|
|
298
|
+
generateCustomHandlerMethod(modelName, handlerName, resultType, inputType, returnType) {
|
|
299
|
+
return ` async ${handlerName}(result: ${resultType}, input: ${inputType}): Promise<${returnType}> {
|
|
141
300
|
// TODO: Implement custom ${handlerName} handler
|
|
142
301
|
// This method receives the result from the previous handler (or null if first)
|
|
143
302
|
// and the input context
|
|
@@ -177,17 +336,41 @@ ${setterCalls}
|
|
|
177
336
|
const serviceName = `${modelName}Service`;
|
|
178
337
|
const storeName = `${modelName}Store`;
|
|
179
338
|
const storeVar = `${modelName.toLowerCase()}Store`;
|
|
339
|
+
// Build handler-to-context map for type inference
|
|
340
|
+
const handlerContextMap = this.buildHandlerContextMap(modelName, useCases);
|
|
180
341
|
// Collect all unique handlers
|
|
181
342
|
const handlers = this.collectHandlers(useCases);
|
|
343
|
+
// Collect DTO types needed for imports
|
|
344
|
+
const dtoTypes = new Set();
|
|
345
|
+
const enumTypeNames = new Set();
|
|
182
346
|
// Generate methods for each handler
|
|
183
347
|
const methods = [];
|
|
184
348
|
handlers.forEach(handler => {
|
|
349
|
+
var _a, _b;
|
|
350
|
+
const contexts = handlerContextMap.get(handler) || [];
|
|
185
351
|
if (handler.startsWith('default:')) {
|
|
186
352
|
const actionName = handler.replace('default:', '');
|
|
187
|
-
|
|
353
|
+
const inputType = this.deriveInputType(contexts);
|
|
354
|
+
const dtoFields = this.computeDtoFieldsForHandler(contexts, aggregateConfig, childInfo);
|
|
355
|
+
if (actionName !== 'list' && actionName !== 'get' && actionName !== 'delete') {
|
|
356
|
+
contexts.forEach(c => dtoTypes.add(c.inputDtoType));
|
|
357
|
+
}
|
|
358
|
+
if (actionName === 'create' || actionName === 'update') {
|
|
359
|
+
for (const [fieldName, fieldConfig] of Object.entries(aggregateConfig.fields)) {
|
|
360
|
+
if (fieldConfig.type === 'enum' && fieldConfig.values && fieldConfig.values.length > 0 && dtoFields.has(fieldName)) {
|
|
361
|
+
enumTypeNames.add(`${modelName}${(0, typeUtils_1.capitalize)(fieldName)}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const listConfig = actionName === 'list'
|
|
366
|
+
? { hasPagination: !!((_b = (_a = contexts[0]) === null || _a === void 0 ? void 0 : _a.inputConfig) === null || _b === void 0 ? void 0 : _b.pagination) }
|
|
367
|
+
: undefined;
|
|
368
|
+
methods.push(this.generateDefaultHandlerMethod(modelName, actionName, aggregateConfig, childInfo, inputType, dtoFields, listConfig));
|
|
188
369
|
}
|
|
189
370
|
else {
|
|
190
|
-
|
|
371
|
+
const { inputType, resultType, returnType } = this.deriveCustomHandlerTypes(contexts, modelName);
|
|
372
|
+
contexts.forEach(c => dtoTypes.add(c.inputDtoType));
|
|
373
|
+
methods.push(this.generateCustomHandlerMethod(modelName, handler, resultType, inputType, returnType));
|
|
191
374
|
}
|
|
192
375
|
});
|
|
193
376
|
const listByParentMethod = this.generateListByParentMethod(modelName, childInfo);
|
|
@@ -198,8 +381,21 @@ ${setterCalls}
|
|
|
198
381
|
if (getResourceOwnerMethod) {
|
|
199
382
|
methods.push(getResourceOwnerMethod);
|
|
200
383
|
}
|
|
384
|
+
// Collect imports for aggregate reference types used in fields
|
|
385
|
+
const aggRefImports = Object.entries(aggregateConfig.fields)
|
|
386
|
+
.filter(([, fc]) => (0, typeUtils_1.isAggregateReference)(fc.type, this.availableAggregates) && fc.type !== modelName)
|
|
387
|
+
.map(([, fc]) => `import { ${fc.type} } from '../../domain/entities/${fc.type}';`)
|
|
388
|
+
.filter((imp, idx, arr) => arr.indexOf(imp) === idx);
|
|
389
|
+
const aggRefImportStr = aggRefImports.length > 0 ? '\n' + aggRefImports.join('\n') : '';
|
|
390
|
+
// Generate DTO import statements
|
|
391
|
+
const dtoImports = [...dtoTypes].map(dtoType => {
|
|
392
|
+
const fileSuffix = dtoType.replace(modelName, '').replace('Input', '');
|
|
393
|
+
return `import { ${dtoType} } from '../dto/${modelName}${fileSuffix}';`;
|
|
394
|
+
}).join('\n');
|
|
395
|
+
const dtoImportStr = dtoImports ? '\n' + dtoImports : '';
|
|
396
|
+
const entityImports = [modelName, ...enumTypeNames].join(', ');
|
|
201
397
|
return `import { Injectable } from '../../../../system';
|
|
202
|
-
import { ${
|
|
398
|
+
import { ${entityImports} } from '../../domain/entities/${modelName}';${aggRefImportStr}${dtoImportStr}
|
|
203
399
|
import { ${storeName} } from '../../infrastructure/stores/${storeName}';
|
|
204
400
|
|
|
205
401
|
/**
|