@neohm/nh-cli 1.1.5 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +179 -18
- package/bin/nh.js +5 -0
- package/bin/nhx.js +11 -0
- package/package.json +8 -5
- package/src/commands/check.js +7 -2
- package/src/commands/gen.flag.js +357 -0
- package/src/commands/gen.js +41 -79
- package/src/commands/migration.js +2 -0
- package/src/commands/seed.js +2 -0
- package/src/index.js +67 -4
- package/src/lib/collision.js +82 -0
- package/src/lib/flavour.js +106 -0
- package/src/lib/format.js +54 -0
- package/src/lib/registry.js +120 -0
- package/src/lib/sync.engine.js +34 -7
- package/src/lib/templates.js +373 -61
package/src/lib/templates.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const { pkg, current } = require('./flavour');
|
|
3
4
|
const {
|
|
5
|
+
toWords,
|
|
4
6
|
toDotCase,
|
|
5
7
|
toPascalCase,
|
|
6
8
|
toKebabCase,
|
|
@@ -10,12 +12,30 @@ const {
|
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Code templates. All generated code imports shared base classes from the
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* across
|
|
15
|
+
* package the active flavour names — `@neohm/nestend` under `nh`,
|
|
16
|
+
* `@neohm/nestendx` under `nhx` (verified exports: CommonEntity, CommonDataDto,
|
|
17
|
+
* CommonDataProcessor, MigrationUtility, SeederUtility, etc.) — and matches the
|
|
18
|
+
* dot.separated filename / PascalCase class conventions used across the
|
|
19
|
+
* boilerplates.
|
|
20
|
+
*
|
|
21
|
+
* The two differ in what a record belongs to, and that difference is read from
|
|
22
|
+
* `current().tenantScoped` rather than branched on a package name: tenanted
|
|
23
|
+
* code scopes rows by `tenant_id` and checks a scope with a role beside it,
|
|
24
|
+
* while the B2C model scopes by the user who created the row and checks the
|
|
25
|
+
* scope alone.
|
|
17
26
|
*/
|
|
18
27
|
|
|
28
|
+
/**
|
|
29
|
+
* A named import, wrapped the way prettier's 80-column printer would: one line
|
|
30
|
+
* while it fits, one member per line once it doesn't. Generated files are
|
|
31
|
+
* committed as-is, so they have to come out already formatted.
|
|
32
|
+
*/
|
|
33
|
+
function namedImport(members, from) {
|
|
34
|
+
const oneLine = `import { ${members.join(', ')} } from '${from}';`;
|
|
35
|
+
if (oneLine.length <= 80) return oneLine;
|
|
36
|
+
return `import {\n${members.map((m) => ` ${m},`).join('\n')}\n} from '${from}';`;
|
|
37
|
+
}
|
|
38
|
+
|
|
19
39
|
/** Build the canonical names for a given entity-ish base (e.g. "Shape"). */
|
|
20
40
|
function names(base) {
|
|
21
41
|
const dot = toDotCase(base);
|
|
@@ -32,6 +52,8 @@ function names(base) {
|
|
|
32
52
|
attributesDtoFile: `${dot}.attributes.dto.ts`,
|
|
33
53
|
controllerClass: `${pascal}Controller`,
|
|
34
54
|
controllerFile: `${dot}.controller.ts`,
|
|
55
|
+
serviceClass: `${pascal}Service`,
|
|
56
|
+
serviceFile: `${dot}.service.ts`,
|
|
35
57
|
dtoClass: `Add${pascal}DataDto`,
|
|
36
58
|
dtoFile: `add.${dot}.data.dto.ts`,
|
|
37
59
|
processorClass: `${pascal}DataProcessor`,
|
|
@@ -42,6 +64,17 @@ function names(base) {
|
|
|
42
64
|
subscriberFile: `${dot}.subscriber.ts`,
|
|
43
65
|
jobClass: `${pascal}Job`,
|
|
44
66
|
jobFile: `${dot}.job.ts`,
|
|
67
|
+
// External (API-key) surface. Mirrors the internal names with an
|
|
68
|
+
// `external.` filename prefix and an `External` class prefix, matching
|
|
69
|
+
// external/controllers/external.test.controller.ts in the boilerplate.
|
|
70
|
+
externalControllerClass: `External${pascal}Controller`,
|
|
71
|
+
externalControllerFile: `external.${dot}.controller.ts`,
|
|
72
|
+
externalServiceClass: `External${pascal}Service`,
|
|
73
|
+
externalServiceFile: `external.${dot}.service.ts`,
|
|
74
|
+
externalProcessorClass: `External${pascal}DataProcessor`,
|
|
75
|
+
externalProcessorFile: `external.${dot}.data.processor.ts`,
|
|
76
|
+
externalListProcessorClass: `External${pascal}ListProcessor`,
|
|
77
|
+
externalListProcessorFile: `external.${dot}.list.processor.ts`,
|
|
45
78
|
// Fed through naming.migrationFileName/migrationClassName so the migration
|
|
46
79
|
// `nh gen` writes is named exactly like one from `nh migration`.
|
|
47
80
|
migrationName: `Add${pascal}Table`,
|
|
@@ -50,14 +83,46 @@ function names(base) {
|
|
|
50
83
|
|
|
51
84
|
function entity(base, tableName) {
|
|
52
85
|
const n = names(base);
|
|
53
|
-
|
|
54
|
-
|
|
86
|
+
|
|
87
|
+
// Without tenants there is nothing extra to declare: CommonEntity already
|
|
88
|
+
// carries `created_by` and its relation, which is what a row belongs to.
|
|
89
|
+
if (!current().tenantScoped) {
|
|
90
|
+
return `import { Column, Entity } from 'typeorm';
|
|
91
|
+
import { CommonEntity } from '${pkg()}';
|
|
92
|
+
import { ${n.attributesDtoClass} } from '../dtos/${n.dot}.attributes.dto';
|
|
93
|
+
|
|
94
|
+
@Entity({ name: '${tableName}' })
|
|
95
|
+
export class ${n.entityClass} extends CommonEntity {
|
|
96
|
+
// TODO: declare the columns ${n.pascal} owns.
|
|
97
|
+
|
|
98
|
+
@Column({ type: 'jsonb' })
|
|
99
|
+
attributes: ${n.attributesDtoClass};
|
|
100
|
+
|
|
101
|
+
// Relations and methods
|
|
102
|
+
}
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return `import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm';
|
|
107
|
+
import { CommonEntity, TenantEntity } from '${pkg()}';
|
|
55
108
|
import { ${n.attributesDtoClass} } from '../dtos/${n.dot}.attributes.dto';
|
|
56
109
|
|
|
57
110
|
@Entity({ name: '${tableName}' })
|
|
58
111
|
export class ${n.entityClass} extends CommonEntity {
|
|
112
|
+
// Tenant scope — the service and the list processor both filter on it.
|
|
113
|
+
@Column()
|
|
114
|
+
tenant_id: string;
|
|
115
|
+
|
|
116
|
+
// TODO: declare the columns ${n.pascal} owns.
|
|
117
|
+
|
|
59
118
|
@Column({ type: 'jsonb' })
|
|
60
119
|
attributes: ${n.attributesDtoClass};
|
|
120
|
+
|
|
121
|
+
// Relations and methods
|
|
122
|
+
|
|
123
|
+
@ManyToOne(() => TenantEntity)
|
|
124
|
+
@JoinColumn({ name: 'tenant_id' })
|
|
125
|
+
tenant: TenantEntity;
|
|
61
126
|
}
|
|
62
127
|
`;
|
|
63
128
|
}
|
|
@@ -70,46 +135,270 @@ function attributesDto(base) {
|
|
|
70
135
|
`;
|
|
71
136
|
}
|
|
72
137
|
|
|
73
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Controllers hold no business logic. They authenticate the caller, then
|
|
140
|
+
* delegate to the module's service — so the HTTP surface stays readable and
|
|
141
|
+
* the same operation is reusable from a job, a subscriber or another service.
|
|
142
|
+
*/
|
|
143
|
+
function controller(base, { processor = false, listProcessor = false } = {}) {
|
|
74
144
|
const n = names(base);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
145
|
+
const svcProp = `${n.camel}Service`;
|
|
146
|
+
const hasPost = processor || listProcessor;
|
|
147
|
+
|
|
148
|
+
// @nestjs/common members, alphabetised the way prettier leaves them.
|
|
149
|
+
const nestCommon = ['Controller', 'Get', 'Param'];
|
|
150
|
+
if (hasPost) nestCommon.push('Body', 'Post');
|
|
151
|
+
nestCommon.sort();
|
|
152
|
+
|
|
153
|
+
const nestendImports = ['AuthService'];
|
|
154
|
+
if (listProcessor) nestendImports.push('CommonListFilterDto');
|
|
155
|
+
nestendImports.push('CommonParamsDto');
|
|
156
|
+
|
|
157
|
+
const imports = [
|
|
158
|
+
namedImport(nestCommon, '@nestjs/common'),
|
|
159
|
+
// @ApiBearerAuth() attaches the requirement to these operations. The
|
|
160
|
+
// DocumentBuilder call only registers the scheme (under its default name,
|
|
161
|
+
// 'bearer') and draws the Authorize button — without the decorator
|
|
162
|
+
// swagger-ui shows no padlock and never sends the Authorization header.
|
|
163
|
+
`import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';`,
|
|
164
|
+
namedImport(nestendImports, pkg()),
|
|
165
|
+
`import { ${n.serviceClass} } from '../services/${n.dot}.service';`,
|
|
166
|
+
];
|
|
167
|
+
if (processor) {
|
|
168
|
+
imports.push(
|
|
169
|
+
`import { ${n.dtoClass} } from '../dtos/add.${n.dot}.data.dto';`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
78
172
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
173
|
+
// Tenanted access takes a scope and a role; the B2C model has no roles at
|
|
174
|
+
// all, so the same call carries the scope alone.
|
|
175
|
+
const tenanted = current().tenantScoped;
|
|
176
|
+
const gate = (indent) =>
|
|
177
|
+
tenanted
|
|
178
|
+
? `await this.authService.validateAccess(\n${indent} 'add-scope',\n${indent} 'add-role',\n${indent})`
|
|
179
|
+
: `await this.authService.validateAccess('add-scope')`;
|
|
180
|
+
|
|
181
|
+
const methods = [
|
|
182
|
+
` @Get(':id')
|
|
183
|
+
async get${n.pascal}(@Param() params: CommonParamsDto) {
|
|
184
|
+
const { user } = ${gate(' ')};
|
|
185
|
+
|
|
186
|
+
return this.${svcProp}.get(user, params.id);
|
|
187
|
+
}`,
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
if (processor) {
|
|
191
|
+
methods.push(` @Post()
|
|
192
|
+
async upsert${n.pascal}(@Body() body: ${n.dtoClass}) {
|
|
193
|
+
${tenanted ? `await this.authService.validateAccess('add-scope', 'add-role');` : `await this.authService.validateAccess('add-scope');`}
|
|
194
|
+
|
|
195
|
+
return this.${svcProp}.upsert(body);
|
|
196
|
+
}`);
|
|
85
197
|
}
|
|
86
|
-
return `import { Body, Controller, Post } from '@nestjs/common';
|
|
87
|
-
import { ApiTags } from '@nestjs/swagger';
|
|
88
|
-
import {
|
|
89
|
-
AuthService,
|
|
90
|
-
CommonListFilterDto,
|
|
91
|
-
ListFilterService,
|
|
92
|
-
} from '@neohm/nestend';
|
|
93
|
-
import { ${n.listProcessorClass} } from '../libraries/${n.dot}.list.processor';
|
|
94
198
|
|
|
199
|
+
if (listProcessor) {
|
|
200
|
+
methods.push(` @Post('list')
|
|
201
|
+
async list(@Body() body: CommonListFilterDto) {
|
|
202
|
+
const { user } = ${gate(' ')};
|
|
203
|
+
|
|
204
|
+
return this.${svcProp}.list(user, body);
|
|
205
|
+
}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const gateTodo = tenanted
|
|
209
|
+
? `// TODO: replace 'add-scope' / 'add-role' with this module's scope and role.`
|
|
210
|
+
: `// TODO: replace 'add-scope' with this module's scope — a granted parent\n// scope reaches its children, so name the one the feature is filed under.`;
|
|
211
|
+
|
|
212
|
+
return `${imports.join('\n')}
|
|
213
|
+
|
|
214
|
+
${gateTodo}
|
|
95
215
|
@ApiTags('${n.kebab}')
|
|
216
|
+
@ApiBearerAuth()
|
|
96
217
|
@Controller('api/v1/${n.kebab}')
|
|
97
218
|
export class ${n.controllerClass} {
|
|
98
219
|
constructor(
|
|
99
|
-
private readonly
|
|
220
|
+
private readonly ${svcProp}: ${n.serviceClass},
|
|
100
221
|
private readonly authService: AuthService,
|
|
101
222
|
) {}
|
|
102
223
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
224
|
+
${methods.join('\n\n')}
|
|
225
|
+
}
|
|
226
|
+
`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The module's service layer, always generated: single-record reads live here,
|
|
231
|
+
* and write / list work is handed to the processors. Injected into the
|
|
232
|
+
* controller, so nothing downstream has to know how a record is fetched.
|
|
233
|
+
*
|
|
234
|
+
* `get()` is unconditional — it is the one operation that needs no processor.
|
|
235
|
+
* `upsert()` appears with a data processor, `list()` with a listing processor.
|
|
236
|
+
*/
|
|
237
|
+
function service(base, { processor = false, listProcessor = false } = {}) {
|
|
238
|
+
const n = names(base);
|
|
239
|
+
const label = toWords(base).join(' ');
|
|
240
|
+
|
|
241
|
+
const nestendImports = [];
|
|
242
|
+
if (listProcessor) {
|
|
243
|
+
nestendImports.push('CommonListFilterDto', 'ListFilterService');
|
|
244
|
+
}
|
|
245
|
+
nestendImports.push('NotFoundException', 'UserEntity');
|
|
246
|
+
|
|
247
|
+
const imports = [
|
|
248
|
+
namedImport(nestendImports, pkg()),
|
|
249
|
+
`import { Injectable } from '@nestjs/common';`,
|
|
250
|
+
];
|
|
251
|
+
if (listProcessor) {
|
|
252
|
+
imports.push(
|
|
253
|
+
`import { ${n.listProcessorClass} } from '../libraries/${n.dot}.list.processor';`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (processor) {
|
|
257
|
+
imports.push(
|
|
258
|
+
`import { ${n.dtoClass} } from '../dtos/add.${n.dot}.data.dto';`,
|
|
259
|
+
`import { ${n.processorClass} } from '../libraries/${n.dot}.data.processor';`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
imports.push(
|
|
263
|
+
`import { ${n.entityClass} } from '../entities/${n.dot}.entity';`,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const members = [];
|
|
267
|
+
if (listProcessor) {
|
|
268
|
+
members.push(
|
|
269
|
+
` constructor(protected readonly listFilterService: ListFilterService) {}`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Ownership is part of the lookup, not a check after it: a row belonging to
|
|
274
|
+
// someone else must read as "not found", never as "forbidden". Tenanted rows
|
|
275
|
+
// carry the organization; without tenants it is the user who created it.
|
|
276
|
+
const owner = current().tenantScoped
|
|
277
|
+
? 'tenant_id: user.organization_id'
|
|
278
|
+
: 'created_by: user.id';
|
|
279
|
+
members.push(` async get(user: UserEntity, id: string) {
|
|
280
|
+
const ${n.camel} = await ${n.entityClass}.findOne({
|
|
281
|
+
where: { id, ${owner} },
|
|
282
|
+
});
|
|
283
|
+
if (!${n.camel}) throw new NotFoundException('Invalid ${label} id');
|
|
284
|
+
|
|
285
|
+
return ${n.camel};
|
|
286
|
+
}`);
|
|
287
|
+
|
|
288
|
+
if (processor) {
|
|
289
|
+
// Insert or update: CommonDataDto carries an optional `id`, and the data
|
|
290
|
+
// processor decides which of the two the payload asks for.
|
|
291
|
+
members.push(` async upsert(data: ${n.dtoClass}) {
|
|
292
|
+
return new ${n.processorClass}().process(data);
|
|
293
|
+
}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (listProcessor) {
|
|
297
|
+
// Emit the call already wrapped the way prettier's 80-column printer
|
|
298
|
+
// would, so a long entity name doesn't leave the generated file dirty.
|
|
299
|
+
// Prettier's three tiers: keep the fluent one-liner; else break the
|
|
300
|
+
// .process() argument; else break the constructor arguments too.
|
|
301
|
+
const call = `new ${n.listProcessorClass}(this.listFilterService, user)`;
|
|
302
|
+
const oneLine = ` return ${call}.process(data);`;
|
|
303
|
+
const brokenArg = ` return ${call}.process(`;
|
|
304
|
+
let body;
|
|
305
|
+
if (oneLine.length <= 80) {
|
|
306
|
+
body = oneLine;
|
|
307
|
+
} else if (brokenArg.length <= 80) {
|
|
308
|
+
body = `${brokenArg}\n data,\n );`;
|
|
309
|
+
} else {
|
|
310
|
+
body = ` return new ${n.listProcessorClass}(\n this.listFilterService,\n user,\n ).process(data);`;
|
|
311
|
+
}
|
|
312
|
+
members.push(` async list(user: UserEntity, data: CommonListFilterDto) {
|
|
313
|
+
${body}
|
|
314
|
+
}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return `${imports.join('\n')}
|
|
318
|
+
|
|
319
|
+
@Injectable()
|
|
320
|
+
export class ${n.serviceClass} {
|
|
321
|
+
${members.join('\n\n')}
|
|
322
|
+
}
|
|
323
|
+
`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Bare controller for `nh gen -c`: swagger tag, the caller-supplied route
|
|
328
|
+
* under api/v1, AuthService on hand, and one empty `check()` to build on.
|
|
329
|
+
*/
|
|
330
|
+
function bareController(base, routePath) {
|
|
331
|
+
const n = names(base);
|
|
332
|
+
return `import { Controller, Get } from '@nestjs/common';
|
|
333
|
+
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
|
334
|
+
import { AuthService } from '${pkg()}';
|
|
335
|
+
|
|
336
|
+
@ApiTags('${routePath}')
|
|
337
|
+
@ApiBearerAuth()
|
|
338
|
+
@Controller('api/v1/${routePath}')
|
|
339
|
+
export class ${n.controllerClass} {
|
|
340
|
+
constructor(private readonly authService: AuthService) {}
|
|
341
|
+
|
|
342
|
+
@Get('check')
|
|
343
|
+
async check() {}
|
|
344
|
+
}
|
|
345
|
+
`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Bare external controller for `nh gen -x -c`. The API-key surface differs
|
|
350
|
+
* from the internal one in three ways, all of them load-bearing: the route is
|
|
351
|
+
* api/e/v1, the key is declared to swagger with @ApiSecurity('api-key'), and
|
|
352
|
+
* access is checked through ApiKeyService rather than AuthService.
|
|
353
|
+
*/
|
|
354
|
+
function bareExternalController(base, routePath) {
|
|
355
|
+
const n = names(base);
|
|
356
|
+
return `import { Controller, Get } from '@nestjs/common';
|
|
357
|
+
import { ApiSecurity, ApiTags } from '@nestjs/swagger';
|
|
358
|
+
import { ApiKeyService } from '${pkg()}';
|
|
359
|
+
|
|
360
|
+
@ApiTags('external-${routePath} (Api key)')
|
|
361
|
+
@ApiSecurity('api-key')
|
|
362
|
+
@Controller('api/e/v1/${routePath}')
|
|
363
|
+
export class ${n.externalControllerClass} {
|
|
364
|
+
constructor(private readonly apiKeyService: ApiKeyService) {}
|
|
365
|
+
|
|
366
|
+
@Get('check')
|
|
367
|
+
async check() {}
|
|
368
|
+
}
|
|
369
|
+
`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Bare injectable service for `nh gen -s` / `nh gen -x -s`. */
|
|
373
|
+
function bareService(base, { external = false } = {}) {
|
|
374
|
+
const n = names(base);
|
|
375
|
+
const cls = external ? n.externalServiceClass : n.serviceClass;
|
|
376
|
+
return `import { Injectable } from '@nestjs/common';
|
|
377
|
+
|
|
378
|
+
@Injectable()
|
|
379
|
+
export class ${cls} {}
|
|
380
|
+
`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Standalone job for `nh gen -j`. Unlike the entity job the wizard scaffolds,
|
|
385
|
+
* this one is not driven by a subscriber, so it takes no DatabaseEventDto and
|
|
386
|
+
* references no entity — whatever `dispatch()` was handed arrives as `data`.
|
|
387
|
+
*/
|
|
388
|
+
function additionalJob(base) {
|
|
389
|
+
const n = names(base);
|
|
390
|
+
return `import { CommonJob, QueueService } from '${pkg()}';
|
|
391
|
+
import { Injectable } from '@nestjs/common';
|
|
392
|
+
import { JobConstants } from '../../constants/job.constants';
|
|
393
|
+
|
|
394
|
+
@Injectable()
|
|
395
|
+
export class ${n.jobClass} extends CommonJob {
|
|
396
|
+
constructor(protected readonly queueService: QueueService) {
|
|
397
|
+
super(JobConstants.${n.camel}Job);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async handle(data: unknown): Promise<void> {
|
|
401
|
+
return;
|
|
113
402
|
}
|
|
114
403
|
}
|
|
115
404
|
`;
|
|
@@ -117,7 +406,7 @@ export class ${n.controllerClass} {
|
|
|
117
406
|
|
|
118
407
|
function dto(base) {
|
|
119
408
|
const n = names(base);
|
|
120
|
-
return `import { CommonDataDto } from '
|
|
409
|
+
return `import { CommonDataDto } from '${pkg()}';
|
|
121
410
|
|
|
122
411
|
export class ${n.dtoClass} extends CommonDataDto {
|
|
123
412
|
// TODO: declare validated payload fields for ${n.pascal}.
|
|
@@ -125,12 +414,16 @@ export class ${n.dtoClass} extends CommonDataDto {
|
|
|
125
414
|
`;
|
|
126
415
|
}
|
|
127
416
|
|
|
128
|
-
function processor(base) {
|
|
417
|
+
function processor(base, { external = false } = {}) {
|
|
129
418
|
const n = names(base);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
419
|
+
// An external processor sits one level deeper (external/libraries/), so its
|
|
420
|
+
// reach back into the module's dtos/ gains a segment.
|
|
421
|
+
const up = external ? '../..' : '..';
|
|
422
|
+
const cls = external ? n.externalProcessorClass : n.processorClass;
|
|
423
|
+
return `import { CommonDataProcessor } from '${pkg()}';
|
|
424
|
+
import { ${n.dtoClass} } from '${up}/dtos/add.${n.dot}.data.dto';
|
|
425
|
+
|
|
426
|
+
export class ${cls} extends CommonDataProcessor {
|
|
134
427
|
protected payload: ${n.dtoClass};
|
|
135
428
|
|
|
136
429
|
async process(data: ${n.dtoClass}) {
|
|
@@ -153,8 +446,9 @@ export class ${n.processorClass} extends CommonDataProcessor {
|
|
|
153
446
|
`;
|
|
154
447
|
}
|
|
155
448
|
|
|
156
|
-
function listProcessor(base, tableName) {
|
|
449
|
+
function listProcessor(base, tableName, { external = false } = {}) {
|
|
157
450
|
const n = names(base);
|
|
451
|
+
const cls = external ? n.externalListProcessorClass : n.listProcessorClass;
|
|
158
452
|
return `import {
|
|
159
453
|
CommonListFilterConfig,
|
|
160
454
|
CommonListFilterDto,
|
|
@@ -163,7 +457,7 @@ function listProcessor(base, tableName) {
|
|
|
163
457
|
ListFilterService,
|
|
164
458
|
SortDirection,
|
|
165
459
|
UserEntity,
|
|
166
|
-
} from '
|
|
460
|
+
} from '${pkg()}';
|
|
167
461
|
|
|
168
462
|
/**
|
|
169
463
|
* Lists / filters ${n.pascal}.
|
|
@@ -183,7 +477,7 @@ function listProcessor(base, tableName) {
|
|
|
183
477
|
* \`columns\`. Standing literal clauses go in \`predicates\`; anything
|
|
184
478
|
* carrying a value goes in the method, where add() binds it.
|
|
185
479
|
*/
|
|
186
|
-
export class ${
|
|
480
|
+
export class ${cls} extends CommonListProcessor {
|
|
187
481
|
protected config: CommonListFilterConfig = {
|
|
188
482
|
query: '${tableName} a',
|
|
189
483
|
columns: [
|
|
@@ -219,11 +513,13 @@ export class ${n.listProcessorClass} extends CommonListProcessor {
|
|
|
219
513
|
}
|
|
220
514
|
|
|
221
515
|
protected applyCustomPredicates() {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
516
|
+
${
|
|
517
|
+
current().tenantScoped
|
|
518
|
+
? ` // Filter for the caller's tenant/organization
|
|
519
|
+
this.predicates.add('a.tenant_id = :tenant_id', this.user.organization_id);`
|
|
520
|
+
: ` // Filter for the caller: rows belong to whoever created them.
|
|
521
|
+
this.predicates.add('a.created_by = :created_by', this.user.id);`
|
|
522
|
+
}
|
|
227
523
|
}
|
|
228
524
|
}
|
|
229
525
|
`;
|
|
@@ -231,7 +527,7 @@ export class ${n.listProcessorClass} extends CommonListProcessor {
|
|
|
231
527
|
|
|
232
528
|
function subscriber(base) {
|
|
233
529
|
const n = names(base);
|
|
234
|
-
return `import { CommonSubscriber } from '
|
|
530
|
+
return `import { CommonSubscriber } from '${pkg()}';
|
|
235
531
|
import { EventSubscriber } from 'typeorm';
|
|
236
532
|
import { ${n.entityClass} } from '../entities/${n.dot}.entity';
|
|
237
533
|
import { DataSource } from 'typeorm';
|
|
@@ -256,7 +552,7 @@ export class ${n.subscriberClass} extends CommonSubscriber<${n.entityClass}> {
|
|
|
256
552
|
|
|
257
553
|
function job(base) {
|
|
258
554
|
const n = names(base);
|
|
259
|
-
return `import { CommonJob, QueueService, DatabaseEventDto } from '
|
|
555
|
+
return `import { CommonJob, QueueService, DatabaseEventDto } from '${pkg()}';
|
|
260
556
|
import { Injectable } from '@nestjs/common';
|
|
261
557
|
import { JobConstants } from '../../constants/job.constants';
|
|
262
558
|
import { ${n.entityClass} } from '../entities/${n.dot}.entity';
|
|
@@ -282,7 +578,7 @@ export class ${n.jobClass} extends CommonJob {
|
|
|
282
578
|
function genMigration(base, tableName, timestamp) {
|
|
283
579
|
const n = names(base);
|
|
284
580
|
const className = migrationClassName(n.migrationName, timestamp);
|
|
285
|
-
return `import { MigrationUtility } from '
|
|
581
|
+
return `import { MigrationUtility } from '${pkg()}';
|
|
286
582
|
|
|
287
583
|
export class ${className} extends MigrationUtility {
|
|
288
584
|
constructor() {
|
|
@@ -292,10 +588,21 @@ export class ${className} extends MigrationUtility {
|
|
|
292
588
|
|
|
293
589
|
process() {
|
|
294
590
|
this.rootPrimary();
|
|
295
|
-
|
|
296
|
-
|
|
591
|
+
|
|
592
|
+
${
|
|
593
|
+
current().tenantScoped
|
|
594
|
+
? ` this.foreign({ name: 'tenant_id', foreignTable: 'sys_tenants' });\n`
|
|
595
|
+
: ''
|
|
596
|
+
} this.foreign({ name: 'created_by', foreignTable: 'sys_users' });
|
|
597
|
+
this.foreign({ name: 'updated_by', foreignTable: 'sys_users' });
|
|
598
|
+
|
|
599
|
+
// TODO: add columns matching ${n.entityClass}.
|
|
600
|
+
|
|
297
601
|
this.json('attributes');
|
|
298
602
|
this.timestamps();
|
|
603
|
+
|
|
604
|
+
${current().tenantScoped ? ` this.index('tenant_id');\n` : ''} this.index('created_by');
|
|
605
|
+
this.index('updated_by');
|
|
299
606
|
}
|
|
300
607
|
}
|
|
301
608
|
`;
|
|
@@ -304,7 +611,7 @@ export class ${className} extends MigrationUtility {
|
|
|
304
611
|
/** Standalone migration (`nh migration`). */
|
|
305
612
|
function migration(migrationName, tableName, timestamp) {
|
|
306
613
|
const className = migrationClassName(migrationName, timestamp);
|
|
307
|
-
return `import { MigrationUtility } from '
|
|
614
|
+
return `import { MigrationUtility } from '${pkg()}';
|
|
308
615
|
|
|
309
616
|
export class ${className} extends MigrationUtility {
|
|
310
617
|
constructor() {
|
|
@@ -324,7 +631,7 @@ export class ${className} extends MigrationUtility {
|
|
|
324
631
|
/** Seed file (`nh seed`). */
|
|
325
632
|
function seed(seedName, tableName, timestamp) {
|
|
326
633
|
const className = `${toPascalCase(seedName)}${timestamp}`;
|
|
327
|
-
return `import { SeederUtility } from '
|
|
634
|
+
return `import { SeederUtility } from '${pkg()}';
|
|
328
635
|
|
|
329
636
|
export class ${className} extends SeederUtility {
|
|
330
637
|
constructor() {
|
|
@@ -343,7 +650,7 @@ export class ${className} extends SeederUtility {
|
|
|
343
650
|
/** Fresh src/constants/job.constants.ts (first entry included). */
|
|
344
651
|
function jobConstantsFile(base) {
|
|
345
652
|
const n = names(base);
|
|
346
|
-
return `import { baseJobConstants } from '
|
|
653
|
+
return `import { baseJobConstants } from '${pkg()}';
|
|
347
654
|
|
|
348
655
|
export const JobConstants = {
|
|
349
656
|
...baseJobConstants,
|
|
@@ -355,7 +662,7 @@ export const JobConstants = {
|
|
|
355
662
|
/** Fresh src/constants/entity.constants.ts (first entry included). */
|
|
356
663
|
function entityConstantsFile(base) {
|
|
357
664
|
const n = names(base);
|
|
358
|
-
return `import { baseEntityConstants } from '
|
|
665
|
+
return `import { baseEntityConstants } from '${pkg()}';
|
|
359
666
|
|
|
360
667
|
export const EntityConstants = {
|
|
361
668
|
...baseEntityConstants,
|
|
@@ -367,7 +674,7 @@ export const EntityConstants = {
|
|
|
367
674
|
/** Fresh src/constants/job.maps.ts (first entry included). */
|
|
368
675
|
function jobMapsFile(base, moduleDot, constants) {
|
|
369
676
|
const n = names(base);
|
|
370
|
-
return `import { baseJobMaps } from '
|
|
677
|
+
return `import { baseJobMaps } from '${pkg()}';
|
|
371
678
|
import { ${n.jobClass} } from '../${moduleDot}/jobs/${n.dot}.job';
|
|
372
679
|
import { ${constants.exportName} } from '${constants.importPath}';
|
|
373
680
|
|
|
@@ -381,7 +688,7 @@ export const JobMaps = {
|
|
|
381
688
|
/** Fresh src/constants/entity.maps.ts (first entry included). */
|
|
382
689
|
function entityMapsFile(base, moduleDot, constants) {
|
|
383
690
|
const n = names(base);
|
|
384
|
-
return `import { baseEntityMaps } from '
|
|
691
|
+
return `import { baseEntityMaps } from '${pkg()}';
|
|
385
692
|
import { ${n.entityClass} } from '../${moduleDot}/entities/${n.dot}.entity';
|
|
386
693
|
import { ${constants.exportName} } from '${constants.importPath}';
|
|
387
694
|
|
|
@@ -413,7 +720,7 @@ import {
|
|
|
413
720
|
QueueModule,
|
|
414
721
|
AuthModule,
|
|
415
722
|
SystemPropertyModule,
|
|
416
|
-
} from '
|
|
723
|
+
} from '${pkg()}';
|
|
417
724
|
import { es6Classes } from './es6.classes';
|
|
418
725
|
|
|
419
726
|
@Module({
|
|
@@ -435,6 +742,11 @@ module.exports = {
|
|
|
435
742
|
entity,
|
|
436
743
|
attributesDto,
|
|
437
744
|
controller,
|
|
745
|
+
service,
|
|
746
|
+
bareController,
|
|
747
|
+
bareExternalController,
|
|
748
|
+
bareService,
|
|
749
|
+
additionalJob,
|
|
438
750
|
dto,
|
|
439
751
|
processor,
|
|
440
752
|
listProcessor,
|