@appxdigital/appx-core 0.1.121 → 0.1.123

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 (40) hide show
  1. package/CHANGELOG.md +48 -5
  2. package/README.md +24 -2
  3. package/dist/appx-core.module.d.ts +1 -0
  4. package/dist/appx-core.module.d.ts.map +1 -1
  5. package/dist/appx-core.module.js +1 -0
  6. package/dist/common/bigint-serializer.d.ts +1 -0
  7. package/dist/common/bigint-serializer.d.ts.map +1 -0
  8. package/dist/common/bigint-serializer.js +18 -0
  9. package/dist/common/decorators/scalar-field.decorator.d.ts +22 -0
  10. package/dist/common/decorators/scalar-field.decorator.d.ts.map +1 -0
  11. package/dist/common/decorators/scalar-field.decorator.js +103 -0
  12. package/dist/config/generate-all.js +21 -54
  13. package/dist/config/generate-controllers.d.ts +2 -1
  14. package/dist/config/generate-controllers.d.ts.map +1 -1
  15. package/dist/config/generate-controllers.js +7 -22
  16. package/dist/config/generate-dtos.d.ts +14 -1
  17. package/dist/config/generate-dtos.d.ts.map +1 -1
  18. package/dist/config/generate-dtos.js +85 -28
  19. package/dist/config/generate-models.d.ts +3 -0
  20. package/dist/config/generate-models.d.ts.map +1 -0
  21. package/dist/config/generate-models.js +107 -0
  22. package/dist/config/generate-modules.d.ts +30 -1
  23. package/dist/config/generate-modules.d.ts.map +1 -1
  24. package/dist/config/generate-modules.js +145 -86
  25. package/dist/config/generate-resolvers.d.ts +7 -1
  26. package/dist/config/generate-resolvers.d.ts.map +1 -1
  27. package/dist/config/generate-resolvers.js +10 -17
  28. package/dist/config/generate-services.d.ts +2 -1
  29. package/dist/config/generate-services.d.ts.map +1 -1
  30. package/dist/config/generate-services.js +7 -16
  31. package/dist/config/utils.d.ts +38 -1
  32. package/dist/config/utils.d.ts.map +1 -1
  33. package/dist/config/utils.js +76 -11
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -0
  37. package/docs/README.md +1 -0
  38. package/docs/dtos.md +13 -1
  39. package/docs/generate.md +57 -0
  40. package/package.json +3 -1
@@ -23,6 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
23
23
  return result;
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.scaffoldDtoSubclass = exports.generateDtoBases = void 0;
26
27
  const path = __importStar(require("path"));
27
28
  const utils_1 = require("./utils");
28
29
  /**
@@ -55,15 +56,13 @@ const utils_1 = require("./utils");
55
56
  * enforces the matching ABAC rule for each (`create` rule / `connect` rule).
56
57
  */
57
58
  const cwd = process.cwd();
58
- // Read the consumer's generated Prisma client for the DMMF (authoritative
59
- // field metadata incl. `documentation`, which carries /// @Role / @NoWrite).
60
- // eslint-disable-next-line @typescript-eslint/no-var-requires
61
- const { Prisma } = require(path.join(cwd, 'node_modules', '@prisma/client'));
62
59
  const generatedDtoRoot = path.join(cwd, 'src/generated/dto');
63
60
  const modulesRoot = path.join(cwd, 'src/modules');
64
- const modelsByName = {};
65
- for (const m of Prisma.dmmf.datamodel.models)
66
- modelsByName[m.name] = m;
61
+ // Populated by generateDtoBases() from the DMMF models it is handed (authoritative
62
+ // field metadata incl. `documentation`, which carries /// @Role / @NoWrite).
63
+ // A module-level lookup so the relation-nested-write renderer can resolve a
64
+ // related model by name without threading it through every call.
65
+ let modelsByName = {};
67
66
  function isNoWrite(doc) {
68
67
  if (!doc)
69
68
  return false;
@@ -84,7 +83,12 @@ function isServerManagedTimestamp(field) {
84
83
  const def = field.default;
85
84
  return !!def && typeof def === 'object' && def.name === 'now';
86
85
  }
87
- /** Decision for a scalar/enum field: base validator, TS type, optional enum arg. */
86
+ /** Decision for a scalar/enum field: base validator, TS type, optional enum arg,
87
+ * optional class-transformer `@Type(() => <transform>)` (needed when the wire
88
+ * value must be coerced to a non-primitive before validation, e.g. ISO → Date).
89
+ * `fieldDecorator` marks a framework decorator (imported from
90
+ * `@appxdigital/appx-core`, emitted as `@Name()`) that does its own coercion +
91
+ * validation (Decimal/BigInt); `needsPrisma` requests a `Prisma` type import. */
88
92
  function mapField(field) {
89
93
  if (field.kind === 'enum') {
90
94
  return { validator: 'IsEnum', tsType: field.type, enumArg: field.type };
@@ -95,13 +99,25 @@ function mapField(field) {
95
99
  case 'Boolean':
96
100
  return { validator: 'IsBoolean', tsType: 'boolean' };
97
101
  case 'Int':
98
- case 'BigInt':
99
102
  return { validator: 'IsInt', tsType: 'number' };
103
+ case 'BigInt':
104
+ // Prisma types BigInt columns as `bigint`; JSON can't carry one, so
105
+ // `@BigIntField()` accepts a string or number and coerces it.
106
+ return { validator: 'BigIntField', tsType: 'bigint', fieldDecorator: true };
100
107
  case 'Float':
101
- case 'Decimal':
102
108
  return { validator: 'IsNumber', tsType: 'number' };
109
+ case 'Decimal':
110
+ // Prisma types Decimal columns as `Prisma.Decimal`; `@DecimalField()`
111
+ // accepts a string or number and coerces it to a real Decimal so the
112
+ // DTO matches the model type and Prisma gets the right value.
113
+ return { validator: 'DecimalField', tsType: 'Prisma.Decimal', fieldDecorator: true, needsPrisma: true };
103
114
  case 'DateTime':
104
- return { validator: 'IsDateString', tsType: 'string' };
115
+ // Prisma types DateTime columns as `Date`; the DTO must match so a
116
+ // controller override (`data: UpdateXDto` vs base `Partial<X>`)
117
+ // type-checks. `@Type(() => Date)` still accepts an ISO string over
118
+ // the wire and coerces it (ValidationPipe runs with transform:true),
119
+ // so there is no API change and Prisma receives a real Date.
120
+ return { validator: 'IsDate', tsType: 'Date', transform: 'Date' };
105
121
  default:
106
122
  // Json, Bytes, and anything unrecognised: keep the field (so it isn't
107
123
  // stripped by whitelist) without a strict type validator.
@@ -133,13 +149,18 @@ function backFkColumns(relatedModel, relationName) {
133
149
  return inverse ? inverse.relationFromFields || [] : [];
134
150
  }
135
151
  /** Renders class-body lines for a set of scalar/enum fields; collects imports. */
136
- function renderScalarLines(fields, mode, validatorImports, enumImports) {
152
+ function renderScalarLines(fields, mode, validatorImports, enumImports, transformerImports, frameworkImports) {
137
153
  const lines = [];
138
154
  for (const field of fields) {
139
155
  const m = mapField(field);
140
- validatorImports.add(m.validator);
156
+ if (m.fieldDecorator)
157
+ frameworkImports.add(m.validator);
158
+ else
159
+ validatorImports.add(m.validator);
141
160
  if (m.enumArg)
142
161
  enumImports.add(m.enumArg);
162
+ if (m.needsPrisma)
163
+ enumImports.add('Prisma');
143
164
  // update / connect: everything optional. create: optional if not required or defaulted.
144
165
  const optional = mode !== 'create' || !field.isRequired || field.hasDefaultValue;
145
166
  const isList = field.isList;
@@ -148,6 +169,10 @@ function renderScalarLines(fields, mode, validatorImports, enumImports) {
148
169
  if (m.validator === 'Allow') {
149
170
  validatorDecorator = '@Allow()';
150
171
  }
172
+ else if (m.fieldDecorator) {
173
+ // Framework decorator handles list coercion internally — no `each`.
174
+ validatorDecorator = `@${m.validator}()`;
175
+ }
151
176
  else {
152
177
  const args = [];
153
178
  if (m.enumArg)
@@ -165,6 +190,10 @@ function renderScalarLines(fields, mode, validatorImports, enumImports) {
165
190
  validatorImports.add('IsArray');
166
191
  decoratorLines.push(' @IsArray()');
167
192
  }
193
+ if (m.transform) {
194
+ transformerImports.add('Type');
195
+ decoratorLines.push(` @Type(() => ${m.transform})`);
196
+ }
168
197
  decoratorLines.push(` ${validatorDecorator}`);
169
198
  lines.push(decoratorLines.join('\n'));
170
199
  lines.push(` ${field.name}${optional ? '?' : '!'}: ${tsType};`);
@@ -180,7 +209,7 @@ function renderClass(className, bodyLines) {
180
209
  * plus the property lines to add to the parent class. Nested create classes are
181
210
  * scalars-only (no deeper relations) so there are no circular imports.
182
211
  */
183
- function renderRelationNestedWrites(model, validatorImports, enumImports, transformerImports) {
212
+ function renderRelationNestedWrites(model, validatorImports, enumImports, transformerImports, frameworkImports) {
184
213
  const classes = [];
185
214
  const parentLines = [];
186
215
  const relations = model.fields.filter((f) => f.kind === 'object');
@@ -194,10 +223,10 @@ function renderRelationNestedWrites(model, validatorImports, enumImports, transf
194
223
  const writeCls = `${model.name}${relCap}NestedWriteDto`;
195
224
  // create: related writable scalars minus the back-reference FK.
196
225
  const omit = backFkColumns(related, rel.relationName);
197
- classes.push(renderClass(createCls, renderScalarLines(writableFields(related, omit), 'create', validatorImports, enumImports)));
226
+ classes.push(renderClass(createCls, renderScalarLines(writableFields(related, omit), 'create', validatorImports, enumImports, transformerImports, frameworkImports)));
198
227
  // connect: related unique fields (all optional).
199
228
  const conn = connectFields(related);
200
- classes.push(renderClass(connectCls, renderScalarLines(conn, 'connect', validatorImports, enumImports)));
229
+ classes.push(renderClass(connectCls, renderScalarLines(conn, 'connect', validatorImports, enumImports, transformerImports, frameworkImports)));
201
230
  // nested-write: create? / connect? only.
202
231
  transformerImports.add('Type');
203
232
  validatorImports.add('IsOptional');
@@ -231,10 +260,11 @@ function renderBaseDto(className, model, mode) {
231
260
  const validatorImports = new Set();
232
261
  const enumImports = new Set();
233
262
  const transformerImports = new Set();
234
- const scalarLines = renderScalarLines(writableFields(model), mode, validatorImports, enumImports);
263
+ const frameworkImports = new Set();
264
+ const scalarLines = renderScalarLines(writableFields(model), mode, validatorImports, enumImports, transformerImports, frameworkImports);
235
265
  let nested = { classes: [], parentLines: [] };
236
266
  if (mode === 'create') {
237
- nested = renderRelationNestedWrites(model, validatorImports, enumImports, transformerImports);
267
+ nested = renderRelationNestedWrites(model, validatorImports, enumImports, transformerImports, frameworkImports);
238
268
  }
239
269
  const parentClass = renderClass(className, [...scalarLines, ...nested.parentLines]);
240
270
  const importLines = [];
@@ -247,6 +277,9 @@ function renderBaseDto(className, model, mode) {
247
277
  if (enumImports.size > 0) {
248
278
  importLines.push(`import { ${[...enumImports].sort().join(', ')} } from '@prisma/client';`);
249
279
  }
280
+ if (frameworkImports.size > 0) {
281
+ importLines.push(`import { ${[...frameworkImports].sort().join(', ')} } from '@appxdigital/appx-core';`);
282
+ }
250
283
  return (utils_1.GENERATED_BANNER +
251
284
  '\n' +
252
285
  (importLines.length ? importLines.join('\n') + '\n\n' : '') +
@@ -264,22 +297,46 @@ function renderSubclassDto(subclass, baseClass, importPath) {
264
297
  ` */\n` +
265
298
  `export class ${subclass} extends ${baseClass} {}\n`);
266
299
  }
267
- for (const model of Prisma.dmmf.datamodel.models) {
268
- const modelName = model.name; // Pascal, e.g. ProjectMember
269
- const folder = (0, utils_1.pascalToKebabCase)(modelName); // kebab, e.g. project-member
270
- if (utils_1.IGNORE_FOLDERS.includes(folder))
271
- continue;
300
+ /**
301
+ * Deploy-safe pass: (re)generate the DTO **base** classes under the gitignored
302
+ * `src/generated/dto/`, one per non-framework model. Overwrite-only — never
303
+ * touches `src/modules/`. `allModels` is the full DMMF list (framework models
304
+ * included) so relation-nested-write members can resolve their target model;
305
+ * bases are only emitted for non-framework models.
306
+ */
307
+ function generateDtoBases(allModels) {
308
+ modelsByName = {};
309
+ for (const m of allModels)
310
+ modelsByName[m.name] = m;
311
+ for (const model of allModels) {
312
+ const modelName = model.name; // Pascal, e.g. ProjectMember
313
+ if (utils_1.FRAMEWORK_MODELS.includes(modelName))
314
+ continue;
315
+ const folder = (0, utils_1.pascalToKebabCase)(modelName); // kebab, e.g. project-member
316
+ for (const mode of ['create', 'update']) {
317
+ const capMode = mode.charAt(0).toUpperCase() + mode.slice(1);
318
+ const baseClass = `${capMode}${modelName}GeneratedDto`;
319
+ const basePath = path.join(generatedDtoRoot, folder, `${mode}-${folder}.generated.dto.ts`);
320
+ (0, utils_1.writeGeneratedFile)(basePath, renderBaseDto(baseClass, model, mode));
321
+ }
322
+ }
323
+ console.log('DTO base classes generated.');
324
+ }
325
+ exports.generateDtoBases = generateDtoBases;
326
+ /**
327
+ * Wizard/scaffold pass: create the hand-owned DTO **subclass** files (create +
328
+ * update) for a single model under `src/modules/<model>/dto/`. Generated once,
329
+ * never overwritten — this is where the developer adds custom validation.
330
+ */
331
+ function scaffoldDtoSubclass(modelName) {
332
+ const folder = (0, utils_1.pascalToKebabCase)(modelName);
272
333
  for (const mode of ['create', 'update']) {
273
334
  const capMode = mode.charAt(0).toUpperCase() + mode.slice(1);
274
335
  const baseClass = `${capMode}${modelName}GeneratedDto`;
275
336
  const subclass = `${capMode}${modelName}Dto`;
276
- // Base (overwritten, gitignored).
277
- const basePath = path.join(generatedDtoRoot, folder, `${mode}-${folder}.generated.dto.ts`);
278
- (0, utils_1.writeGeneratedFile)(basePath, renderBaseDto(baseClass, model, mode));
279
- // Subclass (once, hand-owned, committed).
280
337
  const subPath = path.join(modulesRoot, folder, 'dto', `${mode}-${folder}.dto.ts`);
281
338
  const importPath = `../../../generated/dto/${folder}/${mode}-${folder}.generated.dto`;
282
339
  (0, utils_1.createFileIfNotExists)(subPath, renderSubclassDto(subclass, baseClass, importPath));
283
340
  }
284
341
  }
285
- console.log('DTOs generated.');
342
+ exports.scaffoldDtoSubclass = scaffoldDtoSubclass;
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=generate-models.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-models.d.ts","sourceRoot":"","sources":["../../src/config/generate-models.ts"],"names":[],"mappings":""}
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ /**
5
+ * `appx generate models` — the module WIZARD (bucket B + C).
6
+ *
7
+ * The only generator that writes hand-owned files under `src/modules/**` and
8
+ * mutates `src/app.module.ts`. For each selected model it scaffolds the module,
9
+ * service, controller, resolver and DTO subclass (each once, never overwritten),
10
+ * then registers the module in AppModule.
11
+ *
12
+ * Model discovery is DMMF-driven (schema is the source of truth), independent of
13
+ * whatever folders the GraphQL plugin emitted. Only models WITHOUT an existing
14
+ * module are offered; framework-owned models (User/Session/UserRefreshToken) are
15
+ * never listed.
16
+ *
17
+ * Usage:
18
+ * appx generate models interactive picker
19
+ * appx generate models Habit Log scaffold the named models (Pascal or kebab)
20
+ * appx generate models --all scaffold every generatable model (legacy)
21
+ *
22
+ * It first runs the deploy-safe pass (prisma generate + DTO bases), so the
23
+ * scaffolded resolver/controller imports resolve.
24
+ */
25
+ const child_process_1 = require("child_process");
26
+ const utils_1 = require("./utils");
27
+ const generate_dtos_1 = require("./generate-dtos");
28
+ const generate_modules_1 = require("./generate-modules");
29
+ const generate_services_1 = require("./generate-services");
30
+ const generate_controllers_1 = require("./generate-controllers");
31
+ const generate_resolvers_1 = require("./generate-resolvers");
32
+ // inquirer is ESM-only; this file compiles to CommonJS, where tsc would
33
+ // down-level a normal `import('inquirer')` into `require()` (ERR_REQUIRE_ESM at
34
+ // runtime). Wrapping in `new Function` keeps a genuine dynamic `import()` that
35
+ // Node resolves as ESM. Only used on the interactive path.
36
+ const dynamicImport = new Function('m', 'return import(m)');
37
+ async function main() {
38
+ // 1) Deploy-safe pass first — refresh client + GraphQL artifacts + DTO bases.
39
+ console.log('Running Prisma Generate (client + GraphQL artifacts)...');
40
+ (0, child_process_1.execSync)('prisma generate', { stdio: 'inherit' });
41
+ (0, generate_dtos_1.generateDtoBases)((0, utils_1.loadAllModels)());
42
+ // 2) Candidates = non-framework models with no module yet. "Has a module"
43
+ // means either the canonical folder exists OR some existing module already
44
+ // serves the model via CoreController/CoreService<Model> — the latter
45
+ // catches hand-written modules under a non-canonical (e.g. pluralised)
46
+ // folder, so we don't scaffold a duplicate.
47
+ const owned = (0, utils_1.ownedModels)();
48
+ const candidates = (0, utils_1.loadModels)().filter((m) => !(0, utils_1.moduleExists)(m.name) && !owned.has(m.name));
49
+ // 3) Resolve the selection from argv, else prompt.
50
+ const args = process.argv.slice(2);
51
+ const wantAll = args.includes('--all');
52
+ const requested = args.filter((a) => !a.startsWith('-'));
53
+ let selected;
54
+ if (wantAll) {
55
+ selected = candidates.map((m) => m.name);
56
+ }
57
+ else if (requested.length > 0) {
58
+ // Accept a Pascal model name or a kebab folder; only from candidates.
59
+ const byKey = new Map();
60
+ for (const m of candidates) {
61
+ byKey.set(m.name.toLowerCase(), m.name);
62
+ byKey.set((0, utils_1.modelFolder)(m.name), m.name);
63
+ }
64
+ selected = [];
65
+ for (const r of requested) {
66
+ const hit = byKey.get(r.toLowerCase()) ?? byKey.get(r);
67
+ if (hit)
68
+ selected.push(hit);
69
+ else
70
+ console.warn(`Skipping "${r}": not a generatable model (unknown, framework-owned, or already has a module).`);
71
+ }
72
+ }
73
+ else {
74
+ if (candidates.length === 0) {
75
+ console.log('No modules available to generate.');
76
+ return;
77
+ }
78
+ const inquirer = (await dynamicImport('inquirer')).default;
79
+ const answer = await inquirer.prompt([
80
+ {
81
+ type: 'checkbox',
82
+ name: 'models',
83
+ message: 'Select models to generate CRUD modules for:',
84
+ choices: candidates.map((m) => ({ name: m.name, value: m.name })),
85
+ },
86
+ ]);
87
+ selected = answer.models;
88
+ }
89
+ if (selected.length === 0) {
90
+ console.log('No modules generated.');
91
+ return;
92
+ }
93
+ // 4) Scaffold hand-owned files per model, then register the modules.
94
+ for (const name of selected) {
95
+ (0, generate_modules_1.scaffoldModule)(name);
96
+ (0, generate_services_1.scaffoldService)(name);
97
+ (0, generate_controllers_1.scaffoldController)(name);
98
+ (0, generate_resolvers_1.scaffoldResolver)(name);
99
+ (0, generate_dtos_1.scaffoldDtoSubclass)(name);
100
+ }
101
+ (0, generate_modules_1.registerModulesInAppModule)(selected);
102
+ console.log(`Generated modules: ${selected.join(', ')}.`);
103
+ }
104
+ main().catch((err) => {
105
+ console.error('❌ Module generation failed:', err?.message ?? err);
106
+ process.exit(1);
107
+ });
@@ -1,2 +1,31 @@
1
- export {};
1
+ /** Scaffold the module file for a single model (once; never overwritten). */
2
+ export declare function scaffoldModule(modelName: string): void;
3
+ /**
4
+ * Pure source transform: register the given models' modules in app.module.ts
5
+ * source. Returns the new source and the module names actually added. Extracted
6
+ * from filesystem I/O so it is unit-testable.
7
+ *
8
+ * Correctness requirements this satisfies:
9
+ * - **Registration order is preserved.** New modules are appended at the END of
10
+ * the `@Module` `imports` array, after the existing entries.
11
+ * - **Nested array literals are respected.** The array's closing `]` is found
12
+ * by bracket-matching (skipping strings/comments), so a nested
13
+ * `ThrottlerModule.forRoot([...])` never captures the insertion point.
14
+ * - **No double registration.** A module already present as an entry in the
15
+ * imports array is not added again.
16
+ * - **No double import.** An `import { XModule } …` line is added only if one
17
+ * isn't already present.
18
+ */
19
+ export declare function addModulesToAppModuleSource(content: string, modelNames: string[]): {
20
+ content: string;
21
+ added: string[];
22
+ };
23
+ /**
24
+ * Register the given models' modules in `src/app.module.ts` — the ONE code
25
+ * mutation in the generator, performed only by the module wizard (never by the
26
+ * deploy-safe pass). Idempotent (already-imported modules are skipped) and scoped
27
+ * to the models passed in — it does NOT scan `src/modules`, so it never registers
28
+ * something the caller didn't ask for.
29
+ */
30
+ export declare function registerModulesInAppModule(modelNames: string[]): void;
2
31
  //# sourceMappingURL=generate-modules.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"generate-modules.d.ts","sourceRoot":"","sources":["../../src/config/generate-modules.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"generate-modules.d.ts","sourceRoot":"","sources":["../../src/config/generate-modules.ts"],"names":[],"mappings":"AA2BA,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAItD;AAyCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,2BAA2B,CACvC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAAE,GACrB;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAC,CAkDpC;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,CASrE"}
@@ -23,10 +23,10 @@ var __importStar = (this && this.__importStar) || function (mod) {
23
23
  return result;
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.registerModulesInAppModule = exports.addModulesToAppModuleSource = exports.scaffoldModule = void 0;
26
27
  const fs = __importStar(require("fs"));
27
28
  const path = __importStar(require("path"));
28
29
  const utils_1 = require("./utils");
29
- const modelsPath = path.join(process.cwd(), 'src/generated');
30
30
  const modulesOutputPath = path.join(process.cwd(), 'src/modules');
31
31
  const appModulePath = path.join(process.cwd(), 'src/app.module.ts');
32
32
  /**
@@ -48,101 +48,160 @@ import { ${model}Resolver } from './${folder}.resolver';
48
48
  })
49
49
  export class ${model}Module {}
50
50
  `;
51
+ /** Scaffold the module file for a single model (once; never overwritten). */
52
+ function scaffoldModule(modelName) {
53
+ const folder = (0, utils_1.modelFolder)(modelName);
54
+ const modulePath = path.join(modulesOutputPath, folder, `${folder}.module.ts`);
55
+ (0, utils_1.createFileIfNotExists)(modulePath, moduleTemplate(modelName, folder));
56
+ }
57
+ exports.scaffoldModule = scaffoldModule;
51
58
  /**
52
- * Create or update the module file for a given model
53
- * @param model
54
- * @param modulePath
55
- * @param folder
59
+ * Given the index of an opening `[`, return the index of its matching `]`,
60
+ * counting bracket depth while skipping string literals and comments so that
61
+ * neither a nested array literal (`ThrottlerModule.forRoot([{ ttl }])`) nor a
62
+ * bracket inside a string/comment throws off the match.
56
63
  */
57
- const createModuleFile = (model, modulePath, folder) => {
58
- const moduleContent = moduleTemplate(model, folder);
59
- fs.writeFileSync(modulePath, moduleContent);
60
- console.log(`Module for ${model} created or updated.`);
61
- };
64
+ function matchingBracket(src, openIdx) {
65
+ let depth = 0;
66
+ let inString = null;
67
+ let inLineComment = false;
68
+ let inBlockComment = false;
69
+ for (let i = openIdx; i < src.length; i++) {
70
+ const c = src[i];
71
+ const next = src[i + 1];
72
+ if (inLineComment) {
73
+ if (c === '\n')
74
+ inLineComment = false;
75
+ continue;
76
+ }
77
+ if (inBlockComment) {
78
+ if (c === '*' && next === '/') {
79
+ inBlockComment = false;
80
+ i++;
81
+ }
82
+ continue;
83
+ }
84
+ if (inString) {
85
+ if (c === '\\') {
86
+ i++;
87
+ continue;
88
+ } // skip escaped char
89
+ if (c === inString)
90
+ inString = null;
91
+ continue;
92
+ }
93
+ if (c === '/' && next === '/') {
94
+ inLineComment = true;
95
+ i++;
96
+ continue;
97
+ }
98
+ if (c === '/' && next === '*') {
99
+ inBlockComment = true;
100
+ i++;
101
+ continue;
102
+ }
103
+ if (c === '"' || c === "'" || c === '`') {
104
+ inString = c;
105
+ continue;
106
+ }
107
+ if (c === '[')
108
+ depth++;
109
+ else if (c === ']') {
110
+ depth--;
111
+ if (depth === 0)
112
+ return i;
113
+ }
114
+ }
115
+ throw new Error('app.module.ts: unbalanced `imports` array (no matching `]`)');
116
+ }
62
117
  /**
63
- * Update the AppModule with new module imports
118
+ * Pure source transform: register the given models' modules in app.module.ts
119
+ * source. Returns the new source and the module names actually added. Extracted
120
+ * from filesystem I/O so it is unit-testable.
121
+ *
122
+ * Correctness requirements this satisfies:
123
+ * - **Registration order is preserved.** New modules are appended at the END of
124
+ * the `@Module` `imports` array, after the existing entries.
125
+ * - **Nested array literals are respected.** The array's closing `]` is found
126
+ * by bracket-matching (skipping strings/comments), so a nested
127
+ * `ThrottlerModule.forRoot([...])` never captures the insertion point.
128
+ * - **No double registration.** A module already present as an entry in the
129
+ * imports array is not added again.
130
+ * - **No double import.** An `import { XModule } …` line is added only if one
131
+ * isn't already present.
64
132
  */
65
- const updateAppModule = () => {
66
- let appModuleContent = fs.readFileSync(appModulePath, 'utf8');
67
- /**
68
- * Regex to extract existing module imports from the AppModule
69
- */
70
- const importRegex = /import { (\w+)Module } from '.\/modules\/\w+\/\w+.module';/g;
71
- const existingImports = new Set();
133
+ function addModulesToAppModuleSource(content, modelNames) {
134
+ // Locate the @Module imports array and its true bounds.
135
+ const moduleIdx = content.indexOf('@Module(');
136
+ if (moduleIdx === -1)
137
+ throw new Error('app.module.ts: @Module decorator not found');
138
+ const importsKeyIdx = content.indexOf('imports:', moduleIdx);
139
+ if (importsKeyIdx === -1)
140
+ throw new Error('app.module.ts: `imports:` not found in @Module');
141
+ const openIdx = content.indexOf('[', importsKeyIdx);
142
+ if (openIdx === -1)
143
+ throw new Error('app.module.ts: `imports` is not an array literal');
144
+ const closeIdx = matchingBracket(content, openIdx);
145
+ const inner = content.slice(openIdx + 1, closeIdx);
146
+ // Modules already registered in the imports array (any `XxxModule` token).
147
+ const registered = new Set((inner.match(/\b\w+Module\b/g) ?? []));
148
+ // Modules already imported (by `import { XModule } from './modules/…'`).
149
+ const imported = new Set();
150
+ const importRegex = /import\s*{\s*(\w+Module)\s*}\s*from\s*'\.\/modules\/[\w-]+\/[\w-]+\.module'/g;
72
151
  let match;
73
- while ((match = importRegex.exec(appModuleContent)) !== null) {
74
- existingImports.add(match[1]);
75
- }
152
+ while ((match = importRegex.exec(content)) !== null)
153
+ imported.add(match[1]);
154
+ const added = [];
76
155
  const newImports = [];
77
- const newModules = [];
78
- /**
79
- * Regex to extract the imports array from the AppModule to prevent duplicates
80
- */
81
- const importsArrayRegex = /(imports:\s*\[)([^]*?)(\])/;
82
- const importsArrayMatch = appModuleContent.match(importsArrayRegex);
83
- const existingModuleNames = new Set();
84
- if (importsArrayMatch) {
85
- const currentImportsArray = importsArrayMatch[2]
86
- .split(',')
87
- .map((m) => m.trim().replace(/\s/g, ''))
88
- .filter((m) => m);
89
- currentImportsArray.forEach((moduleName) => {
90
- existingModuleNames.add(moduleName);
91
- });
92
- }
93
- /**
94
- * Iterate over the modules directory and add new imports to the AppModule
95
- */
96
- fs.readdirSync(modulesOutputPath).forEach((folder) => {
97
- if (utils_1.IGNORE_FOLDERS.includes(folder))
98
- return;
99
- const modelName = (0, utils_1.kebabToPascalCase)(folder);
156
+ for (const modelName of modelNames) {
100
157
  const moduleName = `${modelName}Module`;
101
- // Ensure no duplicate imports and no duplicate entries in the imports array
102
- if (!existingImports.has(moduleName) && !existingModuleNames.has(moduleName)) {
103
- newImports.push(`import { ${modelName}Module } from './modules/${folder}/${folder}.module';`);
104
- newModules.push(moduleName);
158
+ if (registered.has(moduleName))
159
+ continue; // already in the imports array
160
+ added.push(moduleName);
161
+ if (!imported.has(moduleName)) {
162
+ const folder = (0, utils_1.modelFolder)(modelName);
163
+ newImports.push(`import { ${moduleName} } from './modules/${folder}/${folder}.module';`);
105
164
  }
106
- });
107
- if (newImports.length === 0) {
108
- console.log('No new modules to add.');
109
- return;
110
165
  }
111
- // Add new imports to AppModule and update the imports array without duplicates
112
- appModuleContent = appModuleContent.replace(/(imports:\s*\[)([^]*?)(\])/, (_, prefix, currentImports, suffix) => {
113
- const currentModules = currentImports
114
- .split(',')
115
- .map((m) => m.trim())
116
- .filter((m) => m);
117
- newModules.forEach((newModule) => {
118
- if (!currentModules.includes(newModule)) {
119
- currentModules.push(newModule);
120
- }
121
- });
122
- return `${prefix}${currentModules.join(', ')}${suffix}`;
123
- });
124
- const finalContent = newImports.join('\n') + '\n' + appModuleContent;
125
- fs.writeFileSync(appModulePath, finalContent);
126
- console.log('AppModule updated successfully.');
127
- };
166
+ if (added.length === 0)
167
+ return { content, added };
168
+ // Append after the last existing entry, before the array's closing `]`
169
+ // (preserving order and the existing trailing whitespace / indentation).
170
+ let lastContentIdx = closeIdx - 1;
171
+ while (lastContentIdx > openIdx && /\s/.test(content[lastContentIdx]))
172
+ lastContentIdx--;
173
+ const arrayIsEmpty = lastContentIdx === openIdx;
174
+ const list = added.join(', ');
175
+ let withArray;
176
+ if (arrayIsEmpty) {
177
+ withArray = content.slice(0, openIdx + 1) + list + content.slice(openIdx + 1);
178
+ }
179
+ else {
180
+ const insertAt = lastContentIdx + 1;
181
+ const needsComma = content[lastContentIdx] !== ',';
182
+ const insertion = `${needsComma ? ',' : ''} ${list}`;
183
+ withArray = content.slice(0, insertAt) + insertion + content.slice(insertAt);
184
+ }
185
+ const withImports = newImports.length ? newImports.join('\n') + '\n' + withArray : withArray;
186
+ return { content: withImports, added };
187
+ }
188
+ exports.addModulesToAppModuleSource = addModulesToAppModuleSource;
128
189
  /**
129
- * Generate modules for each model
190
+ * Register the given models' modules in `src/app.module.ts` — the ONE code
191
+ * mutation in the generator, performed only by the module wizard (never by the
192
+ * deploy-safe pass). Idempotent (already-imported modules are skipped) and scoped
193
+ * to the models passed in — it does NOT scan `src/modules`, so it never registers
194
+ * something the caller didn't ask for.
130
195
  */
131
- fs.readdirSync(modelsPath).forEach((folder) => {
132
- if (utils_1.IGNORE_FOLDERS.includes(folder))
196
+ function registerModulesInAppModule(modelNames) {
197
+ if (modelNames.length === 0)
198
+ return;
199
+ const { content, added } = addModulesToAppModuleSource(fs.readFileSync(appModulePath, 'utf8'), modelNames);
200
+ if (added.length === 0) {
201
+ console.log('No new modules to register in AppModule.');
133
202
  return;
134
- const modelName = (0, utils_1.kebabToPascalCase)(folder);
135
- const modelOutputPath = path.join(modulesOutputPath, folder);
136
- if (!fs.existsSync(modelOutputPath)) {
137
- fs.mkdirSync(modelOutputPath, { recursive: true });
138
- console.log(`Folder for model ${modelName} created.`);
139
- }
140
- const modulePath = path.join(modelOutputPath, `${folder}.module.ts`);
141
- if (!fs.existsSync(modulePath)) {
142
- createModuleFile(modelName, modulePath, folder);
143
- }
144
- else {
145
- console.log(`Module for ${modelName} already exists, skipping.`);
146
203
  }
147
- });
148
- updateAppModule();
204
+ fs.writeFileSync(appModulePath, content);
205
+ console.log(`AppModule updated: registered ${added.join(', ')}.`);
206
+ }
207
+ exports.registerModulesInAppModule = registerModulesInAppModule;
@@ -1,2 +1,8 @@
1
- export {};
1
+ /**
2
+ * Scaffold the (read-only) GraphQL resolver file for a single model (once;
3
+ * never overwritten). Imports the prisma-nestjs-graphql artifacts under
4
+ * `src/generated/<folder>/`, so the deploy-safe pass (`prisma generate`) must
5
+ * have produced them first.
6
+ */
7
+ export declare function scaffoldResolver(modelName: string): void;
2
8
  //# sourceMappingURL=generate-resolvers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"generate-resolvers.d.ts","sourceRoot":"","sources":["../../src/config/generate-resolvers.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"generate-resolvers.d.ts","sourceRoot":"","sources":["../../src/config/generate-resolvers.ts"],"names":[],"mappings":"AA8CA;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAIxD"}