@appxdigital/appx-core 0.1.122 → 0.1.124
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 +50 -1
- package/README.md +35 -3
- package/dist/appx-core.module.d.ts +1 -0
- package/dist/appx-core.module.d.ts.map +1 -1
- package/dist/appx-core.module.js +1 -0
- package/dist/common/bigint-serializer.d.ts +1 -0
- package/dist/common/bigint-serializer.d.ts.map +1 -0
- package/dist/common/bigint-serializer.js +18 -0
- package/dist/common/decorators/scalar-field.decorator.d.ts +22 -0
- package/dist/common/decorators/scalar-field.decorator.d.ts.map +1 -0
- package/dist/common/decorators/scalar-field.decorator.js +103 -0
- package/dist/config/generate-all.js +22 -55
- package/dist/config/generate-controllers.d.ts +2 -1
- package/dist/config/generate-controllers.d.ts.map +1 -1
- package/dist/config/generate-controllers.js +7 -22
- package/dist/config/generate-dtos.d.ts +14 -1
- package/dist/config/generate-dtos.d.ts.map +1 -1
- package/dist/config/generate-dtos.js +85 -28
- package/dist/config/generate-models.d.ts +3 -0
- package/dist/config/generate-models.d.ts.map +1 -0
- package/dist/config/generate-models.js +107 -0
- package/dist/config/generate-modules.d.ts +30 -1
- package/dist/config/generate-modules.d.ts.map +1 -1
- package/dist/config/generate-modules.js +145 -86
- package/dist/config/generate-resolvers.d.ts +7 -1
- package/dist/config/generate-resolvers.d.ts.map +1 -1
- package/dist/config/generate-resolvers.js +10 -17
- package/dist/config/generate-services.d.ts +2 -1
- package/dist/config/generate-services.d.ts.map +1 -1
- package/dist/config/generate-services.js +7 -16
- package/dist/config/utils.d.ts +53 -1
- package/dist/config/utils.d.ts.map +1 -1
- package/dist/config/utils.js +124 -11
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/docs/README.md +1 -0
- package/docs/dtos.md +13 -1
- package/docs/generate.md +57 -0
- package/package.json +4 -2
|
@@ -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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
342
|
+
exports.scaffoldDtoSubclass = scaffoldDtoSubclass;
|
|
@@ -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 utils_1 = require("./utils");
|
|
26
|
+
const generate_dtos_1 = require("./generate-dtos");
|
|
27
|
+
const generate_modules_1 = require("./generate-modules");
|
|
28
|
+
const generate_services_1 = require("./generate-services");
|
|
29
|
+
const generate_controllers_1 = require("./generate-controllers");
|
|
30
|
+
const generate_resolvers_1 = require("./generate-resolvers");
|
|
31
|
+
// inquirer is ESM-only; this file compiles to CommonJS, where tsc would
|
|
32
|
+
// down-level a normal `import('inquirer')` into `require()` (ERR_REQUIRE_ESM at
|
|
33
|
+
// runtime). Wrapping in `new Function` keeps a genuine dynamic `import()` that
|
|
34
|
+
// Node resolves as ESM. Only used on the interactive path.
|
|
35
|
+
const dynamicImport = new Function('m', 'return import(m)');
|
|
36
|
+
async function main() {
|
|
37
|
+
// 1) Deploy-safe pass first — refresh client + GraphQL artifacts + DTO bases.
|
|
38
|
+
console.log('Running Prisma Generate (client + GraphQL artifacts)...');
|
|
39
|
+
// Project-local prisma: the CLI is usually global, so PATH has no project .bin.
|
|
40
|
+
(0, utils_1.runProjectBin)('prisma', 'generate');
|
|
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
|
-
|
|
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
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
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(
|
|
74
|
-
|
|
75
|
-
|
|
152
|
+
while ((match = importRegex.exec(content)) !== null)
|
|
153
|
+
imported.add(match[1]);
|
|
154
|
+
const added = [];
|
|
76
155
|
const newImports = [];
|
|
77
|
-
const
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
132
|
-
if (
|
|
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
|
-
|
|
204
|
+
fs.writeFileSync(appModulePath, content);
|
|
205
|
+
console.log(`AppModule updated: registered ${added.join(', ')}.`);
|
|
206
|
+
}
|
|
207
|
+
exports.registerModulesInAppModule = registerModulesInAppModule;
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
|
|
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"}
|