@neohm/nh-cli 1.4.0 → 2.0.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 +29 -28
- package/package.json +1 -1
- package/src/commands/check.js +2 -2
- package/src/commands/gen.flag.js +16 -16
- package/src/commands/gen.js +14 -14
- package/src/commands/sync.js +1 -1
- package/src/index.js +7 -7
- package/src/lib/flavour.js +1 -1
- package/src/lib/sync.engine.js +20 -20
- package/src/lib/templates.js +59 -63
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ sibling [`@neohm/nestendx`](../nestendx).
|
|
|
6
6
|
|
|
7
7
|
`nh` generates fully wired modules, entities, controllers, services, DTOs, data
|
|
8
8
|
processors, migrations and seeds — and keeps your import barrels
|
|
9
|
-
(`
|
|
9
|
+
(`module.registry.ts`) and `app.module.ts` registration in sync, so the project
|
|
10
10
|
builds without any manual import wiring.
|
|
11
11
|
|
|
12
12
|
## Two flavours, one CLI
|
|
@@ -19,7 +19,7 @@ differ only where the two packages do:
|
|
|
19
19
|
| ------------------------ | ------------------------------------------ | ----------------------------------------- |
|
|
20
20
|
| Import specifier | `@neohm/nestend` | `@neohm/nestendx` |
|
|
21
21
|
| What a row belongs to | a tenant (`tenant_id` + `TenantEntity`) | the user who created it (`created_by`) |
|
|
22
|
-
| Access check | `
|
|
22
|
+
| Access check | `validateRole(role)` (scope checked first) | `validateAccess(scope)` — there are no roles |
|
|
23
23
|
| Generated migration | FK + index on `tenant_id` | no tenant column to key |
|
|
24
24
|
|
|
25
25
|
Everything that does not write package-specific code — `sync`, `check`, `list`,
|
|
@@ -64,7 +64,7 @@ The project root is auto-detected by walking up from the current directory for a
|
|
|
64
64
|
| `nh gen <flag>` | Generate one artifact instead of a whole module — see [Flag generation](#flag-generation). |
|
|
65
65
|
| `nh migration` | Generate a standalone migration `<timestamp>M-<Name>.ts`. |
|
|
66
66
|
| `nh seed` | Generate a seed file `<timestamp>S-<Name>.ts`. |
|
|
67
|
-
| `nh sync` | Rebuild every `
|
|
67
|
+
| `nh sync` | Rebuild every `module.registry.ts` barrel and register modules in `app.module.ts`. |
|
|
68
68
|
| `nh check` | Dry-run of `sync` — show what would change without writing. |
|
|
69
69
|
| `nh list` | List scaffolded modules with their entities and providers. |
|
|
70
70
|
| `nh remove <name>` | Safely remove a scaffolded module and de-register it (migrations are preserved). |
|
|
@@ -83,7 +83,7 @@ $ nh gen
|
|
|
83
83
|
? Create controller (ShapeController)? Yes
|
|
84
84
|
? Create DTO (AddShapeDataDto)? Yes
|
|
85
85
|
? Create data processor (ShapeDataProcessor)? Yes
|
|
86
|
-
? Create
|
|
86
|
+
? Create filter processor (ShapeFilterProcessor)? Yes
|
|
87
87
|
? Create migration (M-AddShapeTable)? Yes
|
|
88
88
|
```
|
|
89
89
|
|
|
@@ -92,13 +92,13 @@ Produces, under `src/shape/`:
|
|
|
92
92
|
```text
|
|
93
93
|
shape/
|
|
94
94
|
shape.module.ts # ShapeModule (imports CommonModule, QueueModule, ...)
|
|
95
|
-
|
|
95
|
+
module.registry.ts # barrel of controllers/services/jobs/subscribers
|
|
96
96
|
controllers/shape.controller.ts
|
|
97
97
|
services/shape.service.ts
|
|
98
98
|
dtos/add.shape.data.dto.ts
|
|
99
99
|
entities/shape.entity.ts
|
|
100
100
|
libraries/shape.data.processor.ts
|
|
101
|
-
libraries/shape.
|
|
101
|
+
libraries/shape.filter.processor.ts
|
|
102
102
|
enums/ jobs/ subscribers/
|
|
103
103
|
external/ # optional API-key surface (nh gen -x ...)
|
|
104
104
|
controllers/ libraries/ services/
|
|
@@ -125,11 +125,11 @@ Generated artifacts extend the `@neohm/nestend` base classes:
|
|
|
125
125
|
`TenantEntity` relation, and `TenantScopeSubscriber` stamps and guards it),
|
|
126
126
|
`CommonEntity` under `nhx`; both with an empty `attributes` jsonb column
|
|
127
127
|
- DTO → `CommonDataDto`
|
|
128
|
-
- Data processor → `
|
|
128
|
+
- Data processor → `BaseDataProcessor` (`process()` → `validate()` → `set()`),
|
|
129
129
|
reached through the service's `upsert()`. It takes the caller and, on edit,
|
|
130
130
|
loads the row scoped to them — another owner's `id` is a field error, never an
|
|
131
131
|
update.
|
|
132
|
-
-
|
|
132
|
+
- Filter processor → `BaseFilterProcessor` (column allowlist config, `id` default sort). It takes `CommonListFilterDto` directly — no per-list DTO is generated, since paging, sorting, filters and free-text search all live on the base DTO and the filterable columns are declared in the processor's `columns` config.
|
|
133
133
|
|
|
134
134
|
### Controller → service → processor
|
|
135
135
|
|
|
@@ -137,7 +137,7 @@ Generated controllers run under `@UsePipes(StrictValidationPipe)`, take ids
|
|
|
137
137
|
with `ParseUUIDPipe`, and describe every route with `@ApiOperation`.
|
|
138
138
|
|
|
139
139
|
Controllers hold no business logic. They authenticate with
|
|
140
|
-
`AuthService.validateAccess()
|
|
140
|
+
`AuthService.validateRole()` (`validateAccess()` under nhx), then delegate to the module's service — so the
|
|
141
141
|
same operation is reusable from a job, a subscriber or another service, and the
|
|
142
142
|
HTTP surface stays readable.
|
|
143
143
|
|
|
@@ -150,7 +150,7 @@ answers you already gave:
|
|
|
150
150
|
| `get(user, id)` | always | `GET /api/v1/shape/:id` |
|
|
151
151
|
| `remove(user, id)` | always | `DELETE /api/v1/shape/:id` |
|
|
152
152
|
| `upsert(user, data)` | data processor: Yes | `POST /api/v1/shape` |
|
|
153
|
-
| `list(user, data)` |
|
|
153
|
+
| `list(user, data)` | filter processor: Yes | `POST /api/v1/shape/list` |
|
|
154
154
|
|
|
155
155
|
`get()` and `remove()` keep their logic in the service — no processor: `remove()`
|
|
156
156
|
soft-deletes through the same owner-scoped lookup as `get()`. A module that
|
|
@@ -191,14 +191,15 @@ export enum RoleEnum {
|
|
|
191
191
|
}
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
-
The controller gates each endpoint at the lowest role that should reach it
|
|
195
|
-
|
|
194
|
+
The controller gates each endpoint at the lowest role that should reach it. A role
|
|
195
|
+
carries its scope, so `validateRole()` first checks the organization holds the
|
|
196
|
+
role's scope, then that the caller holds the role or a more senior one:
|
|
196
197
|
|
|
197
198
|
| Endpoint | Gate |
|
|
198
199
|
| --- | --- |
|
|
199
|
-
| `GET :id`, `POST list` | `
|
|
200
|
-
| `POST` (upsert) | `
|
|
201
|
-
| `DELETE :id` | `
|
|
200
|
+
| `GET :id`, `POST list` | `validateRole(RoleEnum.BILLING_READER)` |
|
|
201
|
+
| `POST` (upsert) | `validateRole(RoleEnum.BILLING_MANAGER)` |
|
|
202
|
+
| `DELETE :id` | `validateRole(RoleEnum.BILLING_ADMIN)` |
|
|
202
203
|
|
|
203
204
|
Under `nhx` the gate is `validateAccess(ScopeEnum.BILLING)`. The enums name the
|
|
204
205
|
identifiers only: create the `sys_scopes` row and the three `sys_roles` rows
|
|
@@ -209,14 +210,14 @@ identifiers only: create the `sys_scopes` row and the three `sys_roles` rows
|
|
|
209
210
|
Bare `nh gen` scaffolds a whole module. Once that module exists, most work is
|
|
210
211
|
adding *one* more file to it — which is what the target flags do. Each asks
|
|
211
212
|
only what it cannot infer, refuses to shadow a name already in the project,
|
|
212
|
-
registers the result in `
|
|
213
|
+
registers the result in `module.registry.ts`, and formats what it wrote.
|
|
213
214
|
|
|
214
215
|
| Command | Generates | Asks |
|
|
215
216
|
| --- | --- | --- |
|
|
216
217
|
| `nh gen -c` | `controllers/<name>.controller.ts` | module, name, route path |
|
|
217
218
|
| `nh gen -s` | `services/<name>.service.ts` | module, name |
|
|
218
219
|
| `nh gen -dp` | `libraries/<name>.data.processor.ts` + `dtos/add.<name>.data.dto.ts` | module, name |
|
|
219
|
-
| `nh gen -
|
|
220
|
+
| `nh gen -fp` | `libraries/<name>.filter.processor.ts` | module, name, table name |
|
|
220
221
|
| `nh gen -e` | `entities/<name>.entity.ts` + `dtos/<name>.attributes.dto.ts` + `jobs/<name>.job.ts` + `subscribers/<name>.subscriber.ts` + a migration | module, name, table name |
|
|
221
222
|
| `nh gen -j` | `jobs/<name>.job.ts` | module, name |
|
|
222
223
|
|
|
@@ -229,10 +230,10 @@ entity, `job.constants.ts` / `job.maps.ts` for the job it brought along.
|
|
|
229
230
|
|
|
230
231
|
The two jobs are deliberately different shapes:
|
|
231
232
|
|
|
232
|
-
| | Driven by | `handle()` takes |
|
|
233
|
-
| --- | --- | --- |
|
|
234
|
-
| the job `-e` brings along | the entity's subscriber | `event: DatabaseEventDto<<Name>Entity>` |
|
|
235
|
-
| `nh gen -j` | an explicit `dispatch()` | `data: unknown` |
|
|
233
|
+
| | Extends | Driven by | `handle()` takes |
|
|
234
|
+
| --- | --- | --- | --- |
|
|
235
|
+
| the job `-e` brings along | `BaseEntityJob` | the entity's subscriber | `event: DatabaseEventDto<<Name>Entity>` |
|
|
236
|
+
| `nh gen -j` | `BaseJob` | an explicit `dispatch()` | `data: unknown` |
|
|
236
237
|
|
|
237
238
|
So reach for `-j` when you want a queue worker that is not tied to a table's
|
|
238
239
|
lifecycle.
|
|
@@ -249,7 +250,7 @@ Only one target flag per run; the module is created if it does not exist yet.
|
|
|
249
250
|
| `nh gen -x -c` | `external/controllers/external.<name>.controller.ts` |
|
|
250
251
|
| `nh gen -x -s` | `external/services/external.<name>.service.ts` |
|
|
251
252
|
| `nh gen -x -dp` | `external/libraries/external.<name>.data.processor.ts` |
|
|
252
|
-
| `nh gen -x -
|
|
253
|
+
| `nh gen -x -fp` | `external/libraries/external.<name>.filter.processor.ts` |
|
|
253
254
|
|
|
254
255
|
External files take an `external.` filename prefix and an `External` class
|
|
255
256
|
prefix. An external controller differs from an internal one in three ways, all
|
|
@@ -261,7 +262,7 @@ than `AuthService`.
|
|
|
261
262
|
inward/outward face, so there is nothing to mirror. Both refuse the flag rather
|
|
262
263
|
than quietly ignoring it.
|
|
263
264
|
|
|
264
|
-
External providers register into the *same* `
|
|
265
|
+
External providers register into the *same* `module.registry.ts` categories as
|
|
265
266
|
their internal siblings, listed after them — so `nh sync` and `nh check` see
|
|
266
267
|
`<module>/external/<folder>` as well as `<module>/<folder>`.
|
|
267
268
|
|
|
@@ -296,7 +297,7 @@ this keeps that true for entity names long enough to push a line past 80
|
|
|
296
297
|
columns. A project without prettier installed still gets its files, just
|
|
297
298
|
unformatted.
|
|
298
299
|
|
|
299
|
-
- Migration → `
|
|
300
|
+
- Migration → `MigrationHelper`
|
|
300
301
|
- Seed → `SeederUtility`
|
|
301
302
|
|
|
302
303
|
Re-running `gen` for an existing module reuses it and only adds new files —
|
|
@@ -305,7 +306,7 @@ existing files are never overwritten.
|
|
|
305
306
|
## `nh sync` and `nh check`
|
|
306
307
|
|
|
307
308
|
`sync` scans every module under `src/` — including its `external/` mirror —
|
|
308
|
-
and regenerates its `
|
|
309
|
+
and regenerates its `module.registry.ts` barrel from the files actually present in
|
|
309
310
|
`controllers/`, `services/`, `libraries/`, `dtos/`, `jobs/` and
|
|
310
311
|
`subscribers/`, then ensures each module is imported and listed in
|
|
311
312
|
`app.module.ts`. Files are classified by their **folder** and the presence of an
|
|
@@ -316,9 +317,9 @@ never silently dropped. The operation is idempotent.
|
|
|
316
317
|
|
|
317
318
|
```text
|
|
318
319
|
$ nh check
|
|
319
|
-
|
|
320
|
-
ok src/shape/
|
|
321
|
-
update src/test/
|
|
320
|
+
module registries:
|
|
321
|
+
ok src/shape/module.registry.ts
|
|
322
|
+
update src/test/module.registry.ts
|
|
322
323
|
+ services: TestService
|
|
323
324
|
|
|
324
325
|
app.module.ts:
|
package/package.json
CHANGED
package/src/commands/check.js
CHANGED
|
@@ -21,8 +21,8 @@ async function check() {
|
|
|
21
21
|
const plan = engine.buildPlan(paths, modules);
|
|
22
22
|
let changes = 0;
|
|
23
23
|
|
|
24
|
-
console.log(chalk.bold('\
|
|
25
|
-
for (const p of plan.
|
|
24
|
+
console.log(chalk.bold('\nmodule registries:'));
|
|
25
|
+
for (const p of plan.registries) {
|
|
26
26
|
const rel = path.relative(paths.root, p.path);
|
|
27
27
|
if (p.changed) {
|
|
28
28
|
changes++;
|
package/src/commands/gen.flag.js
CHANGED
|
@@ -21,7 +21,7 @@ const { registerJob, registerEntity } = require('../lib/registry');
|
|
|
21
21
|
const { findConflict, describeConflict } = require('../lib/collision');
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Single-artifact generation: `nh gen -c`, `-s`, `-dp`, `-
|
|
24
|
+
* Single-artifact generation: `nh gen -c`, `-s`, `-dp`, `-fp`, `-e`, `-j`, and
|
|
25
25
|
* the `-x` (external, API-key surface) variants of the first four.
|
|
26
26
|
*
|
|
27
27
|
* Bare `nh gen` still runs the full wizard — this path exists for adding one
|
|
@@ -34,7 +34,7 @@ const LABELS = {
|
|
|
34
34
|
controller: 'controller',
|
|
35
35
|
service: 'service',
|
|
36
36
|
dataProcessor: 'data processor',
|
|
37
|
-
|
|
37
|
+
filterProcessor: 'filter processor',
|
|
38
38
|
entity: 'entity',
|
|
39
39
|
job: 'job',
|
|
40
40
|
};
|
|
@@ -44,7 +44,7 @@ const DESCRIBES = {
|
|
|
44
44
|
controller: 'controller',
|
|
45
45
|
service: 'service',
|
|
46
46
|
dataProcessor: 'data processor (+ its payload DTO)',
|
|
47
|
-
|
|
47
|
+
filterProcessor: 'filter processor',
|
|
48
48
|
entity: 'entity, its job and subscriber, and a migration',
|
|
49
49
|
job: 'job',
|
|
50
50
|
};
|
|
@@ -55,14 +55,14 @@ const EXTERNAL_CAPABLE = new Set([
|
|
|
55
55
|
'controller',
|
|
56
56
|
'service',
|
|
57
57
|
'dataProcessor',
|
|
58
|
-
'
|
|
58
|
+
'filterProcessor',
|
|
59
59
|
]);
|
|
60
60
|
|
|
61
61
|
const FLAG = {
|
|
62
62
|
controller: '-c',
|
|
63
63
|
service: '-s',
|
|
64
64
|
dataProcessor: '-dp',
|
|
65
|
-
|
|
65
|
+
filterProcessor: '-fp',
|
|
66
66
|
entity: '-e',
|
|
67
67
|
job: '-j',
|
|
68
68
|
};
|
|
@@ -103,14 +103,14 @@ function primaryArtifacts(target, moduleDir, n, external) {
|
|
|
103
103
|
return external
|
|
104
104
|
? one(n.externalProcessorClass, 'libraries', n.externalProcessorFile)
|
|
105
105
|
: one(n.processorClass, 'libraries', n.processorFile);
|
|
106
|
-
case '
|
|
106
|
+
case 'filterProcessor':
|
|
107
107
|
return external
|
|
108
108
|
? one(
|
|
109
|
-
n.
|
|
109
|
+
n.externalFilterProcessorClass,
|
|
110
110
|
'libraries',
|
|
111
|
-
n.
|
|
111
|
+
n.externalFilterProcessorFile,
|
|
112
112
|
)
|
|
113
|
-
: one(n.
|
|
113
|
+
: one(n.filterProcessorClass, 'libraries', n.filterProcessorFile);
|
|
114
114
|
case 'entity':
|
|
115
115
|
return [
|
|
116
116
|
{
|
|
@@ -151,11 +151,11 @@ function ensureModule(paths, moduleDot, external, report) {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
const modulePath = path.join(moduleDir, `${moduleDot}.module.ts`);
|
|
154
|
-
const
|
|
154
|
+
const registryPath = path.join(moduleDir, 'module.registry.ts');
|
|
155
155
|
const a = writeFile(modulePath, tpl.moduleFile(moduleDot));
|
|
156
|
-
const b = writeFile(
|
|
156
|
+
const b = writeFile(registryPath, tpl.emptyModuleRegistry());
|
|
157
157
|
if (a !== 'skipped') report(a, modulePath, paths.root);
|
|
158
|
-
if (b !== 'skipped') report(b,
|
|
158
|
+
if (b !== 'skipped') report(b, registryPath, paths.root);
|
|
159
159
|
|
|
160
160
|
if (!existed) log.info(`Created module "${moduleDot}".`);
|
|
161
161
|
return moduleDir;
|
|
@@ -166,7 +166,7 @@ async function genFlag(target, { external = false } = {}) {
|
|
|
166
166
|
if (external && !EXTERNAL_CAPABLE.has(target)) {
|
|
167
167
|
throw new Error(
|
|
168
168
|
`-x is not supported for ${LABELS[target]} — external applies to ` +
|
|
169
|
-
`-c, -s, -dp and -
|
|
169
|
+
`-c, -s, -dp and -fp only. Run \`nh gen ${FLAG[target]}\` without -x.`,
|
|
170
170
|
);
|
|
171
171
|
}
|
|
172
172
|
|
|
@@ -225,7 +225,7 @@ async function genFlag(target, { external = false } = {}) {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
let tableName;
|
|
228
|
-
if (target === '
|
|
228
|
+
if (target === 'filterProcessor' || target === 'entity') {
|
|
229
229
|
({ tableName } = await inquirer.prompt([
|
|
230
230
|
{
|
|
231
231
|
type: 'input',
|
|
@@ -295,8 +295,8 @@ async function genFlag(target, { external = false } = {}) {
|
|
|
295
295
|
write(path.join(moduleDir, 'dtos', n.dtoFile), tpl.dto(rawName));
|
|
296
296
|
break;
|
|
297
297
|
|
|
298
|
-
case '
|
|
299
|
-
write(dest, tpl.
|
|
298
|
+
case 'filterProcessor':
|
|
299
|
+
write(dest, tpl.filterProcessor(rawName, tableName, { external }));
|
|
300
300
|
break;
|
|
301
301
|
|
|
302
302
|
case 'entity': {
|
package/src/commands/gen.js
CHANGED
|
@@ -25,7 +25,7 @@ async function gen() {
|
|
|
25
25
|
|
|
26
26
|
// Every file this run writes or rewrites, formatted in one pass at the end.
|
|
27
27
|
// Each write is announced through report(), so recording paths here catches
|
|
28
|
-
// templates, the constant registries, the
|
|
28
|
+
// templates, the constant registries, the module registry and app.module alike.
|
|
29
29
|
const touched = [];
|
|
30
30
|
const report = (result, filePath, root) => {
|
|
31
31
|
if (result !== "skipped") touched.push(filePath);
|
|
@@ -51,7 +51,7 @@ async function gen() {
|
|
|
51
51
|
log.info(`Creating module "${moduleDot}".`);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
// Always ensure the module skeleton (folders + module file +
|
|
54
|
+
// Always ensure the module skeleton (folders + module file + module registry).
|
|
55
55
|
for (const sub of MODULE_SUBFOLDERS) ensureDir(path.join(moduleDir, sub));
|
|
56
56
|
report(
|
|
57
57
|
writeFile(
|
|
@@ -61,8 +61,8 @@ async function gen() {
|
|
|
61
61
|
path.join(moduleDir, `${moduleDot}.module.ts`),
|
|
62
62
|
paths.root,
|
|
63
63
|
);
|
|
64
|
-
const
|
|
65
|
-
report(writeFile(
|
|
64
|
+
const registryPath = path.join(moduleDir, "module.registry.ts");
|
|
65
|
+
report(writeFile(registryPath, tpl.emptyModuleRegistry()), registryPath, paths.root);
|
|
66
66
|
|
|
67
67
|
// 2. Entity ---------------------------------------------------------------
|
|
68
68
|
const { entityName } = await inquirer.prompt([
|
|
@@ -125,7 +125,7 @@ async function gen() {
|
|
|
125
125
|
// job, subscriber or sibling service goes through, never an opt-in. It is
|
|
126
126
|
// written after the processor prompts, because those answers decide which
|
|
127
127
|
// methods it (and the controller) ships with: `get()` always, `upsert()`
|
|
128
|
-
// with a data processor, `list()` with a
|
|
128
|
+
// with a data processor, `list()` with a filter processor.
|
|
129
129
|
|
|
130
130
|
// 5. Optional: data processor (includes Add*DataDto) ----------------------
|
|
131
131
|
const { processor } = await inquirer.prompt([
|
|
@@ -145,20 +145,20 @@ async function gen() {
|
|
|
145
145
|
report(writeFile(procPath, tpl.processor(entityName, { withEntity: true })), procPath, paths.root);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
// 6. Optional:
|
|
149
|
-
const {
|
|
148
|
+
// 6. Optional: filter processor (takes CommonListFilterDto directly) ------
|
|
149
|
+
const { filterProcessor } = await inquirer.prompt([
|
|
150
150
|
{
|
|
151
151
|
type: "confirm",
|
|
152
|
-
name: "
|
|
153
|
-
message: `Create
|
|
152
|
+
name: "filterProcessor",
|
|
153
|
+
message: `Create filter processor (${n.filterProcessorClass})?`,
|
|
154
154
|
default: true,
|
|
155
155
|
},
|
|
156
156
|
]);
|
|
157
157
|
|
|
158
|
-
if (
|
|
159
|
-
const listProcPath = path.join(moduleDir, "libraries", n.
|
|
158
|
+
if (filterProcessor) {
|
|
159
|
+
const listProcPath = path.join(moduleDir, "libraries", n.filterProcessorFile);
|
|
160
160
|
report(
|
|
161
|
-
writeFile(listProcPath, tpl.
|
|
161
|
+
writeFile(listProcPath, tpl.filterProcessor(entityName, tableName)),
|
|
162
162
|
listProcPath,
|
|
163
163
|
paths.root,
|
|
164
164
|
);
|
|
@@ -166,7 +166,7 @@ async function gen() {
|
|
|
166
166
|
|
|
167
167
|
const servicePath = path.join(moduleDir, "services", n.serviceFile);
|
|
168
168
|
report(
|
|
169
|
-
writeFile(servicePath, tpl.service(entityName, { processor,
|
|
169
|
+
writeFile(servicePath, tpl.service(entityName, { processor, filterProcessor })),
|
|
170
170
|
servicePath,
|
|
171
171
|
paths.root,
|
|
172
172
|
);
|
|
@@ -176,7 +176,7 @@ async function gen() {
|
|
|
176
176
|
report(
|
|
177
177
|
writeFile(
|
|
178
178
|
p,
|
|
179
|
-
tpl.controller(entityName, { processor,
|
|
179
|
+
tpl.controller(entityName, { processor, filterProcessor, enums }),
|
|
180
180
|
),
|
|
181
181
|
p,
|
|
182
182
|
paths.root,
|
package/src/commands/sync.js
CHANGED
|
@@ -19,7 +19,7 @@ async function sync() {
|
|
|
19
19
|
const plan = engine.buildPlan(paths, modules);
|
|
20
20
|
const written = engine.applyPlan(plan);
|
|
21
21
|
|
|
22
|
-
for (const p of plan.
|
|
22
|
+
for (const p of plan.registries) {
|
|
23
23
|
const rel = path.relative(paths.root, p.path);
|
|
24
24
|
if (p.changed) log.overwritten(rel);
|
|
25
25
|
else console.log(` ${chalk.dim('ok')} ${rel}`);
|
package/src/index.js
CHANGED
|
@@ -32,12 +32,12 @@ program
|
|
|
32
32
|
|
|
33
33
|
// `gen` with no target flag runs the full wizard; a target flag generates just
|
|
34
34
|
// that one artifact. -x mirrors a target onto the module's external (API-key)
|
|
35
|
-
// surface and applies to -c, -s, -dp and -
|
|
35
|
+
// surface and applies to -c, -s, -dp and -fp only.
|
|
36
36
|
const GEN_TARGETS = [
|
|
37
37
|
['controller', 'controller'],
|
|
38
38
|
['service', 'service'],
|
|
39
39
|
['dataProcessor', 'dataProcessor'],
|
|
40
|
-
['
|
|
40
|
+
['filterProcessor', 'filterProcessor'],
|
|
41
41
|
['entity', 'entity'],
|
|
42
42
|
['job', 'job'],
|
|
43
43
|
];
|
|
@@ -50,7 +50,7 @@ program
|
|
|
50
50
|
.option('-c, --controller', 'generate only a controller')
|
|
51
51
|
.option('-s, --service', 'generate only a service')
|
|
52
52
|
.option('-dp, --data-processor', 'generate only a data processor')
|
|
53
|
-
.option('-
|
|
53
|
+
.option('-fp, --filter-processor', 'generate only a filter processor')
|
|
54
54
|
.option('-e, --entity', 'generate an entity and its migration')
|
|
55
55
|
.option('-j, --job', 'generate only a job')
|
|
56
56
|
.option('-x, --external', 'put it on the external (api/e/v1) surface')
|
|
@@ -62,7 +62,7 @@ Examples:
|
|
|
62
62
|
${cli} gen -c controller only (asks module, name, route path)
|
|
63
63
|
${cli} gen -s bare injectable service
|
|
64
64
|
${cli} gen -dp data processor (+ its Add<Name>DataDto if missing)
|
|
65
|
-
${cli} gen -
|
|
65
|
+
${cli} gen -fp filter processor
|
|
66
66
|
${cli} gen -e entity + migration, registered in the constants/maps
|
|
67
67
|
${cli} gen -j job, registered in job.constants/job.maps
|
|
68
68
|
${cli} gen -x -c external controller under <module>/external/controllers
|
|
@@ -75,12 +75,12 @@ Examples:
|
|
|
75
75
|
);
|
|
76
76
|
if (chosen.length > 1) {
|
|
77
77
|
throw new Error(
|
|
78
|
-
'Pick one target flag at a time (-c, -s, -dp, -
|
|
78
|
+
'Pick one target flag at a time (-c, -s, -dp, -fp, -e or -j).',
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
81
|
if (!chosen.length) {
|
|
82
82
|
if (opts.external) {
|
|
83
|
-
throw new Error('-x needs a target: -c, -s, -dp or -
|
|
83
|
+
throw new Error('-x needs a target: -c, -s, -dp or -fp.');
|
|
84
84
|
}
|
|
85
85
|
return require('./commands/gen')();
|
|
86
86
|
}
|
|
@@ -102,7 +102,7 @@ program
|
|
|
102
102
|
|
|
103
103
|
program
|
|
104
104
|
.command('sync')
|
|
105
|
-
.description('rebuild
|
|
105
|
+
.description('rebuild module registries and register modules in app.module.ts')
|
|
106
106
|
.action(run(() => require('./commands/sync')()));
|
|
107
107
|
|
|
108
108
|
program
|
package/src/lib/flavour.js
CHANGED
|
@@ -7,7 +7,7 @@ const path = require('path');
|
|
|
7
7
|
* Which package the generated code is written against.
|
|
8
8
|
*
|
|
9
9
|
* The two share every convention this CLI encodes — dot.separated filenames,
|
|
10
|
-
* the processor lifecycle,
|
|
10
|
+
* the processor lifecycle, module registries, the migration utility — and
|
|
11
11
|
* differ in what a record belongs to. `nestend` is the tenanted model: rows
|
|
12
12
|
* carry a `tenant_id`, access is a scope *and* a role. `nestendx` is the B2C
|
|
13
13
|
* one: users register themselves and hold scopes alone, so a row belongs to the
|
package/src/lib/sync.engine.js
CHANGED
|
@@ -8,12 +8,12 @@ const scan = require('./source.scan');
|
|
|
8
8
|
/**
|
|
9
9
|
* The sync engine is the single brain behind `nh sync`, `nh check` and the
|
|
10
10
|
* auto-wiring step of `nh gen`. It scans scaffolded modules, regenerates each
|
|
11
|
-
* module's
|
|
11
|
+
* module's module.registry.ts from the files actually present on disk, and
|
|
12
12
|
* registers every module in app.module.ts — so the project builds without any
|
|
13
13
|
* manual import wiring.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
// Provider categories registered through
|
|
16
|
+
// Provider categories registered through module.registry.ts. Files are classified
|
|
17
17
|
// by the folder they live in (not by a fragile filename suffix) so that
|
|
18
18
|
// non-standard names — e.g. the boilerplate's `test.service..ts` — are still
|
|
19
19
|
// picked up and never silently dropped from the barrel.
|
|
@@ -27,12 +27,12 @@ const CATEGORIES = [
|
|
|
27
27
|
];
|
|
28
28
|
|
|
29
29
|
// Files in a category folder that are not providers themselves.
|
|
30
|
-
const NON_PROVIDER_FILES = new Set(['
|
|
30
|
+
const NON_PROVIDER_FILES = new Set(['module.registry.ts', 'index.ts']);
|
|
31
31
|
|
|
32
32
|
// A module may expose an API-key surface from `<module>/external/<folder>`
|
|
33
33
|
// (e.g. external/controllers/external.test.controller.ts). Those classes are
|
|
34
34
|
// providers of the same categories as their internal siblings and belong in
|
|
35
|
-
// the same
|
|
35
|
+
// the same module.registry arrays — listed after the internal ones — so the
|
|
36
36
|
// scanner has to walk both roots or `nh sync` would strip them from the
|
|
37
37
|
// barrel.
|
|
38
38
|
const EXTERNAL_DIR = 'external';
|
|
@@ -109,7 +109,7 @@ function scanModule(dir, modFile) {
|
|
|
109
109
|
moduleFile: modFile,
|
|
110
110
|
moduleClass: exportedClassName(modFile),
|
|
111
111
|
moduleImportPath: `./${moduleDot}/${path.basename(modFile, '.ts')}`,
|
|
112
|
-
|
|
112
|
+
registryPath: path.join(dir, 'module.registry.ts'),
|
|
113
113
|
providers,
|
|
114
114
|
entities: listTsFiles(path.join(dir, 'entities'), '.entity.ts'),
|
|
115
115
|
dtos: listTsFiles(path.join(dir, 'dtos'), '.dto.ts'),
|
|
@@ -117,7 +117,7 @@ function scanModule(dir, modFile) {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
|
-
* Render the
|
|
120
|
+
* Render the module.registry.ts content for a scanned module.
|
|
121
121
|
*
|
|
122
122
|
* The array literals are wrapped exactly the way prettier's 80-column printer
|
|
123
123
|
* would: inline while they fit, one class per line once they don't. `nh gen`
|
|
@@ -125,7 +125,7 @@ function scanModule(dir, modFile) {
|
|
|
125
125
|
* every later `nh check` would report the barrel as drifted purely because
|
|
126
126
|
* prettier had reflowed it.
|
|
127
127
|
*/
|
|
128
|
-
function
|
|
128
|
+
function buildRegistryContent(mod) {
|
|
129
129
|
const importLines = [];
|
|
130
130
|
const arrayLines = [];
|
|
131
131
|
for (const cat of CATEGORIES) {
|
|
@@ -142,19 +142,19 @@ function buildEs6Content(mod) {
|
|
|
142
142
|
);
|
|
143
143
|
}
|
|
144
144
|
const header = importLines.length ? importLines.join('\n') + '\n\n' : '';
|
|
145
|
-
return `${header}export const
|
|
145
|
+
return `${header}export const moduleRegistry = {\n${arrayLines.join('\n')}\n};\n`;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
/** Plan the
|
|
149
|
-
function
|
|
150
|
-
const next =
|
|
151
|
-
const current = fs.existsSync(mod.
|
|
152
|
-
? fs.readFileSync(mod.
|
|
148
|
+
/** Plan the module.registry.ts rewrite for one module. */
|
|
149
|
+
function planRegistry(mod) {
|
|
150
|
+
const next = buildRegistryContent(mod);
|
|
151
|
+
const current = fs.existsSync(mod.registryPath)
|
|
152
|
+
? fs.readFileSync(mod.registryPath, 'utf8')
|
|
153
153
|
: null;
|
|
154
154
|
return {
|
|
155
|
-
type: '
|
|
155
|
+
type: 'registry',
|
|
156
156
|
moduleDot: mod.moduleDot,
|
|
157
|
-
path: mod.
|
|
157
|
+
path: mod.registryPath,
|
|
158
158
|
current,
|
|
159
159
|
next,
|
|
160
160
|
changed: current !== next,
|
|
@@ -251,18 +251,18 @@ function planAppModule(paths, modules) {
|
|
|
251
251
|
};
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
/** Full plan: every module's
|
|
254
|
+
/** Full plan: every module's module registry + app.module registration. */
|
|
255
255
|
function buildPlan(paths, modules = scanModules(paths)) {
|
|
256
256
|
return {
|
|
257
257
|
modules,
|
|
258
|
-
|
|
258
|
+
registries: modules.map(planRegistry),
|
|
259
259
|
appModule: planAppModule(paths, modules),
|
|
260
260
|
};
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
function applyPlan(plan) {
|
|
264
264
|
const written = [];
|
|
265
|
-
for (const p of plan.
|
|
265
|
+
for (const p of plan.registries) {
|
|
266
266
|
if (p.changed) {
|
|
267
267
|
fs.writeFileSync(p.path, p.next);
|
|
268
268
|
written.push(p.path);
|
|
@@ -280,8 +280,8 @@ module.exports = {
|
|
|
280
280
|
EXTERNAL_DIR,
|
|
281
281
|
scanModules,
|
|
282
282
|
scanModule,
|
|
283
|
-
|
|
284
|
-
|
|
283
|
+
buildRegistryContent,
|
|
284
|
+
planRegistry,
|
|
285
285
|
planAppModule,
|
|
286
286
|
buildPlan,
|
|
287
287
|
applyPlan,
|
package/src/lib/templates.js
CHANGED
|
@@ -15,7 +15,7 @@ const { accessNames } = require('./access');
|
|
|
15
15
|
* Code templates. All generated code imports shared base classes from the
|
|
16
16
|
* package the active flavour names — `@neohm/nestend` under `nh`,
|
|
17
17
|
* `@neohm/nestendx` under `nhx` (verified exports: CommonEntity, CommonDataDto,
|
|
18
|
-
*
|
|
18
|
+
* BaseDataProcessor, MigrationHelper, SeederUtility, etc.) — and matches the
|
|
19
19
|
* dot.separated filename / PascalCase class conventions used across the
|
|
20
20
|
* boilerplates.
|
|
21
21
|
*
|
|
@@ -59,8 +59,8 @@ function names(base) {
|
|
|
59
59
|
dtoFile: `add.${dot}.data.dto.ts`,
|
|
60
60
|
processorClass: `${pascal}DataProcessor`,
|
|
61
61
|
processorFile: `${dot}.data.processor.ts`,
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
filterProcessorClass: `${pascal}FilterProcessor`,
|
|
63
|
+
filterProcessorFile: `${dot}.filter.processor.ts`,
|
|
64
64
|
subscriberClass: `${pascal}Subscriber`,
|
|
65
65
|
subscriberFile: `${dot}.subscriber.ts`,
|
|
66
66
|
jobClass: `${pascal}Job`,
|
|
@@ -74,8 +74,8 @@ function names(base) {
|
|
|
74
74
|
externalServiceFile: `external.${dot}.service.ts`,
|
|
75
75
|
externalProcessorClass: `External${pascal}DataProcessor`,
|
|
76
76
|
externalProcessorFile: `external.${dot}.data.processor.ts`,
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
externalFilterProcessorClass: `External${pascal}FilterProcessor`,
|
|
78
|
+
externalFilterProcessorFile: `external.${dot}.filter.processor.ts`,
|
|
79
79
|
// Fed through naming.migrationFileName/migrationClassName so the migration
|
|
80
80
|
// `nh gen` writes is named exactly like one from `nh migration`.
|
|
81
81
|
migrationName: `Add${pascal}Table`,
|
|
@@ -137,14 +137,14 @@ function attributesDto(base) {
|
|
|
137
137
|
*/
|
|
138
138
|
function controller(
|
|
139
139
|
base,
|
|
140
|
-
{ processor = false,
|
|
140
|
+
{ processor = false, filterProcessor = false, enums = {} } = {},
|
|
141
141
|
) {
|
|
142
142
|
const n = names(base);
|
|
143
143
|
const a = accessNames(base);
|
|
144
144
|
const scopeEnum = enums.scope || 'ScopeEnum';
|
|
145
145
|
const roleEnum = enums.role || 'RoleEnum';
|
|
146
146
|
const svcProp = `${n.camel}Service`;
|
|
147
|
-
const hasPost = processor ||
|
|
147
|
+
const hasPost = processor || filterProcessor;
|
|
148
148
|
|
|
149
149
|
// @nestjs/common members, alphabetised the way prettier leaves them.
|
|
150
150
|
const nestCommon = [
|
|
@@ -159,7 +159,7 @@ function controller(
|
|
|
159
159
|
nestCommon.sort();
|
|
160
160
|
|
|
161
161
|
const nestendImports = ['AuthService'];
|
|
162
|
-
if (
|
|
162
|
+
if (filterProcessor) nestendImports.push('CommonListFilterDto');
|
|
163
163
|
nestendImports.push('StrictValidationPipe');
|
|
164
164
|
|
|
165
165
|
const imports = [
|
|
@@ -178,28 +178,24 @@ function controller(
|
|
|
178
178
|
);
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
// Tenanted access
|
|
182
|
-
//
|
|
183
|
-
//
|
|
181
|
+
// Tenanted access names the lowest role that should reach the endpoint: the
|
|
182
|
+
// role carries its scope, so validateRole() checks the organization holds the
|
|
183
|
+
// scope first, then the role (a senior role satisfies a junior one). The B2C
|
|
184
|
+
// model has no roles, so the check is the scope alone.
|
|
184
185
|
const tenanted = current().tenantScoped;
|
|
185
186
|
imports.push(
|
|
186
|
-
|
|
187
|
+
tenanted
|
|
188
|
+
? `import { ${roleEnum} } from '../../global/enums/role.enum';`
|
|
189
|
+
: `import { ${scopeEnum} } from '../../global/enums/scope.enum';`,
|
|
187
190
|
);
|
|
188
|
-
if (tenanted)
|
|
189
|
-
imports.push(`import { ${roleEnum} } from '../../global/enums/role.enum';`);
|
|
190
|
-
const scopeRef = `${scopeEnum}.${a.scopeKey}`;
|
|
191
191
|
const gate = (indent, level) => {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
: `await this.authService.validateAccess(\n${indent} ${scopeRef},\n${indent})`;
|
|
197
|
-
}
|
|
198
|
-
const roleRef = `${roleEnum}.${a.roles[level].key}`;
|
|
199
|
-
const call = `await this.authService.validateAccess(${scopeRef}, ${roleRef})`;
|
|
192
|
+
const [method, arg] = tenanted
|
|
193
|
+
? ['validateRole', `${roleEnum}.${a.roles[level].key}`]
|
|
194
|
+
: ['validateAccess', `${scopeEnum}.${a.scopeKey}`];
|
|
195
|
+
const call = `await this.authService.${method}(${arg})`;
|
|
200
196
|
return `${indent}const { user } = ${call};`.length <= 80
|
|
201
197
|
? call
|
|
202
|
-
: `await this.authService
|
|
198
|
+
: `await this.authService.${method}(\n${indent} ${arg},\n${indent})`;
|
|
203
199
|
};
|
|
204
200
|
|
|
205
201
|
const methods = [
|
|
@@ -222,7 +218,7 @@ function controller(
|
|
|
222
218
|
}`);
|
|
223
219
|
}
|
|
224
220
|
|
|
225
|
-
if (
|
|
221
|
+
if (filterProcessor) {
|
|
226
222
|
methods.push(` @Post('list')
|
|
227
223
|
@ApiOperation({ summary: 'List, search and filter ${n.kebab}.' })
|
|
228
224
|
async list(@Body() body: CommonListFilterDto) {
|
|
@@ -263,14 +259,14 @@ ${methods.join('\n\n')}
|
|
|
263
259
|
* controller, so nothing downstream has to know how a record is fetched.
|
|
264
260
|
*
|
|
265
261
|
* `get()` is unconditional — it is the one operation that needs no processor.
|
|
266
|
-
* `upsert()` appears with a data processor, `list()` with a
|
|
262
|
+
* `upsert()` appears with a data processor, `list()` with a filter processor.
|
|
267
263
|
*/
|
|
268
|
-
function service(base, { processor = false,
|
|
264
|
+
function service(base, { processor = false, filterProcessor = false } = {}) {
|
|
269
265
|
const n = names(base);
|
|
270
266
|
const label = toWords(base).join(' ');
|
|
271
267
|
|
|
272
268
|
const nestendImports = [];
|
|
273
|
-
if (
|
|
269
|
+
if (filterProcessor) {
|
|
274
270
|
nestendImports.push('CommonListFilterDto', 'ListFilterService');
|
|
275
271
|
}
|
|
276
272
|
nestendImports.push('NotFoundException', 'UserEntity');
|
|
@@ -279,9 +275,9 @@ function service(base, { processor = false, listProcessor = false } = {}) {
|
|
|
279
275
|
namedImport(nestendImports, pkg()),
|
|
280
276
|
`import { Injectable } from '@nestjs/common';`,
|
|
281
277
|
];
|
|
282
|
-
if (
|
|
278
|
+
if (filterProcessor) {
|
|
283
279
|
imports.push(
|
|
284
|
-
`import { ${n.
|
|
280
|
+
`import { ${n.filterProcessorClass} } from '../libraries/${n.dot}.filter.processor';`,
|
|
285
281
|
);
|
|
286
282
|
}
|
|
287
283
|
if (processor) {
|
|
@@ -295,7 +291,7 @@ function service(base, { processor = false, listProcessor = false } = {}) {
|
|
|
295
291
|
);
|
|
296
292
|
|
|
297
293
|
const members = [];
|
|
298
|
-
if (
|
|
294
|
+
if (filterProcessor) {
|
|
299
295
|
members.push(
|
|
300
296
|
` constructor(protected readonly listFilterService: ListFilterService) {}`,
|
|
301
297
|
);
|
|
@@ -332,12 +328,12 @@ function service(base, { processor = false, listProcessor = false } = {}) {
|
|
|
332
328
|
}`);
|
|
333
329
|
}
|
|
334
330
|
|
|
335
|
-
if (
|
|
331
|
+
if (filterProcessor) {
|
|
336
332
|
// Emit the call already wrapped the way prettier's 80-column printer
|
|
337
333
|
// would, so a long entity name doesn't leave the generated file dirty.
|
|
338
334
|
// Prettier's three tiers: keep the fluent one-liner; else break the
|
|
339
335
|
// .process() argument; else break the constructor arguments too.
|
|
340
|
-
const call = `new ${n.
|
|
336
|
+
const call = `new ${n.filterProcessorClass}(this.listFilterService, user)`;
|
|
341
337
|
const oneLine = ` return ${call}.process(data);`;
|
|
342
338
|
const brokenArg = ` return ${call}.process(`;
|
|
343
339
|
let body;
|
|
@@ -346,7 +342,7 @@ function service(base, { processor = false, listProcessor = false } = {}) {
|
|
|
346
342
|
} else if (brokenArg.length <= 80) {
|
|
347
343
|
body = `${brokenArg}\n data,\n );`;
|
|
348
344
|
} else {
|
|
349
|
-
body = ` return new ${n.
|
|
345
|
+
body = ` return new ${n.filterProcessorClass}(\n this.listFilterService,\n user,\n ).process(data);`;
|
|
350
346
|
}
|
|
351
347
|
members.push(` async list(user: UserEntity, data: CommonListFilterDto) {
|
|
352
348
|
${body}
|
|
@@ -428,12 +424,12 @@ export class ${cls} {}
|
|
|
428
424
|
*/
|
|
429
425
|
function additionalJob(base) {
|
|
430
426
|
const n = names(base);
|
|
431
|
-
return `import {
|
|
427
|
+
return `import { BaseJob, QueueService } from '${pkg()}';
|
|
432
428
|
import { Injectable } from '@nestjs/common';
|
|
433
429
|
import { JobConstants } from '../../constants/job.constants';
|
|
434
430
|
|
|
435
431
|
@Injectable()
|
|
436
|
-
export class ${n.jobClass} extends
|
|
432
|
+
export class ${n.jobClass} extends BaseJob {
|
|
437
433
|
constructor(protected readonly queueService: QueueService) {
|
|
438
434
|
super(JobConstants.${n.camel}Job);
|
|
439
435
|
}
|
|
@@ -462,10 +458,10 @@ function processor(base, { external = false, withEntity = false } = {}) {
|
|
|
462
458
|
const up = external ? '../..' : '..';
|
|
463
459
|
const cls = external ? n.externalProcessorClass : n.processorClass;
|
|
464
460
|
if (external) {
|
|
465
|
-
return `import {
|
|
461
|
+
return `import { BaseDataProcessor } from '${pkg()}';
|
|
466
462
|
import { ${n.dtoClass} } from '${up}/dtos/add.${n.dot}.data.dto';
|
|
467
463
|
|
|
468
|
-
export class ${cls} extends
|
|
464
|
+
export class ${cls} extends BaseDataProcessor {
|
|
469
465
|
protected payload: ${n.dtoClass};
|
|
470
466
|
|
|
471
467
|
async process(data: ${n.dtoClass}) {
|
|
@@ -522,10 +518,10 @@ export class ${cls} extends CommonDataProcessor {
|
|
|
522
518
|
: ` private set(): void {
|
|
523
519
|
// TODO: persist ${n.pascal} from this.payload, assigning fields explicitly.
|
|
524
520
|
}`;
|
|
525
|
-
return `import {
|
|
521
|
+
return `import { BaseDataProcessor, UserEntity } from '${pkg()}';
|
|
526
522
|
import { ${n.dtoClass} } from '${up}/dtos/add.${n.dot}.data.dto';${entityImport}
|
|
527
523
|
|
|
528
|
-
export class ${cls} extends
|
|
524
|
+
export class ${cls} extends BaseDataProcessor {
|
|
529
525
|
protected payload: ${n.dtoClass};${existingField}
|
|
530
526
|
|
|
531
527
|
constructor(private readonly user: UserEntity) {
|
|
@@ -562,13 +558,13 @@ ${set}
|
|
|
562
558
|
`;
|
|
563
559
|
}
|
|
564
560
|
|
|
565
|
-
function
|
|
561
|
+
function filterProcessor(base, tableName, { external = false } = {}) {
|
|
566
562
|
const n = names(base);
|
|
567
|
-
const cls = external ? n.
|
|
563
|
+
const cls = external ? n.externalFilterProcessorClass : n.filterProcessorClass;
|
|
568
564
|
return `import {
|
|
569
565
|
CommonListFilterConfig,
|
|
570
566
|
CommonListFilterDto,
|
|
571
|
-
|
|
567
|
+
BaseFilterProcessor,
|
|
572
568
|
FilterValueTypeEnum,
|
|
573
569
|
ListFilterService,
|
|
574
570
|
SortDirection,
|
|
@@ -593,7 +589,7 @@ function listProcessor(base, tableName, { external = false } = {}) {
|
|
|
593
589
|
* \`columns\`. Standing literal clauses go in \`predicates\`; anything
|
|
594
590
|
* carrying a value goes in the method, where add() binds it.
|
|
595
591
|
*/
|
|
596
|
-
export class ${cls} extends
|
|
592
|
+
export class ${cls} extends BaseFilterProcessor {
|
|
597
593
|
protected config: CommonListFilterConfig = {
|
|
598
594
|
query: '${tableName} a',
|
|
599
595
|
columns: [
|
|
@@ -643,14 +639,14 @@ ${
|
|
|
643
639
|
|
|
644
640
|
function subscriber(base) {
|
|
645
641
|
const n = names(base);
|
|
646
|
-
return `import {
|
|
642
|
+
return `import { BaseEntitySubscriber } from '${pkg()}';
|
|
647
643
|
import { EventSubscriber } from 'typeorm';
|
|
648
644
|
import { ${n.entityClass} } from '../entities/${n.dot}.entity';
|
|
649
645
|
import { DataSource } from 'typeorm';
|
|
650
646
|
import { ${n.jobClass} } from '../jobs/${n.dot}.job';
|
|
651
647
|
|
|
652
648
|
@EventSubscriber()
|
|
653
|
-
export class ${n.subscriberClass} extends
|
|
649
|
+
export class ${n.subscriberClass} extends BaseEntitySubscriber<${n.entityClass}> {
|
|
654
650
|
constructor(
|
|
655
651
|
protected readonly datasource: DataSource,
|
|
656
652
|
protected readonly entityJob: ${n.jobClass},
|
|
@@ -671,13 +667,13 @@ export class ${n.subscriberClass} extends CommonSubscriber<${n.entityClass}> {
|
|
|
671
667
|
|
|
672
668
|
function job(base) {
|
|
673
669
|
const n = names(base);
|
|
674
|
-
return `import {
|
|
670
|
+
return `import { BaseEntityJob, QueueService, DatabaseEventDto } from '${pkg()}';
|
|
675
671
|
import { Injectable } from '@nestjs/common';
|
|
676
672
|
import { JobConstants } from '../../constants/job.constants';
|
|
677
673
|
import { ${n.entityClass} } from '../entities/${n.dot}.entity';
|
|
678
674
|
|
|
679
675
|
@Injectable()
|
|
680
|
-
export class ${n.jobClass} extends
|
|
676
|
+
export class ${n.jobClass} extends BaseEntityJob {
|
|
681
677
|
constructor(protected readonly queueService: QueueService) {
|
|
682
678
|
super(JobConstants.${n.camel}Job);
|
|
683
679
|
}
|
|
@@ -697,9 +693,9 @@ export class ${n.jobClass} extends CommonJob {
|
|
|
697
693
|
function genMigration(base, tableName, timestamp) {
|
|
698
694
|
const n = names(base);
|
|
699
695
|
const className = migrationClassName(n.migrationName, timestamp);
|
|
700
|
-
return `import {
|
|
696
|
+
return `import { MigrationHelper } from '${pkg()}';
|
|
701
697
|
|
|
702
|
-
export class ${className} extends
|
|
698
|
+
export class ${className} extends MigrationHelper {
|
|
703
699
|
constructor() {
|
|
704
700
|
super('${tableName}');
|
|
705
701
|
this.process();
|
|
@@ -730,9 +726,9 @@ ${current().tenantScoped ? ` this.index('tenant_id');\n` : ''} this.index(
|
|
|
730
726
|
/** Standalone migration (`nh migration`). */
|
|
731
727
|
function migration(migrationName, tableName, timestamp) {
|
|
732
728
|
const className = migrationClassName(migrationName, timestamp);
|
|
733
|
-
return `import {
|
|
729
|
+
return `import { MigrationHelper } from '${pkg()}';
|
|
734
730
|
|
|
735
|
-
export class ${className} extends
|
|
731
|
+
export class ${className} extends MigrationHelper {
|
|
736
732
|
constructor() {
|
|
737
733
|
super('${tableName}');
|
|
738
734
|
this.process();
|
|
@@ -845,9 +841,9 @@ ${members}
|
|
|
845
841
|
`;
|
|
846
842
|
}
|
|
847
843
|
|
|
848
|
-
/** A freshly scaffolded module before any
|
|
849
|
-
function
|
|
850
|
-
return `export const
|
|
844
|
+
/** A freshly scaffolded module before any module.registry content exists. */
|
|
845
|
+
function emptyModuleRegistry() {
|
|
846
|
+
return `export const moduleRegistry = {
|
|
851
847
|
controllers: [],
|
|
852
848
|
services: [],
|
|
853
849
|
libraries: [],
|
|
@@ -867,16 +863,16 @@ import {
|
|
|
867
863
|
AuthModule,
|
|
868
864
|
SystemPropertyModule,
|
|
869
865
|
} from '${pkg()}';
|
|
870
|
-
import {
|
|
866
|
+
import { moduleRegistry } from './module.registry';
|
|
871
867
|
|
|
872
868
|
@Module({
|
|
873
869
|
imports: [CommonModule, QueueModule, AuthModule, SystemPropertyModule],
|
|
874
|
-
controllers: [...
|
|
870
|
+
controllers: [...moduleRegistry.controllers],
|
|
875
871
|
providers: [
|
|
876
|
-
...
|
|
877
|
-
...
|
|
878
|
-
...
|
|
879
|
-
...
|
|
872
|
+
...moduleRegistry.services,
|
|
873
|
+
...moduleRegistry.libraries,
|
|
874
|
+
...moduleRegistry.jobs,
|
|
875
|
+
...moduleRegistry.subscribers,
|
|
880
876
|
],
|
|
881
877
|
})
|
|
882
878
|
export class ${pascal}Module {}
|
|
@@ -897,7 +893,7 @@ module.exports = {
|
|
|
897
893
|
additionalJob,
|
|
898
894
|
dto,
|
|
899
895
|
processor,
|
|
900
|
-
|
|
896
|
+
filterProcessor,
|
|
901
897
|
subscriber,
|
|
902
898
|
job,
|
|
903
899
|
genMigration,
|
|
@@ -907,6 +903,6 @@ module.exports = {
|
|
|
907
903
|
entityConstantsFile,
|
|
908
904
|
jobMapsFile,
|
|
909
905
|
entityMapsFile,
|
|
910
|
-
|
|
906
|
+
emptyModuleRegistry,
|
|
911
907
|
moduleFile,
|
|
912
908
|
};
|