@neohm/nh-cli 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -13
- 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 +12 -2
- package/src/commands/gen.js +6 -2
- package/src/commands/migration.js +2 -0
- package/src/commands/seed.js +2 -0
- package/src/index.js +22 -14
- package/src/lib/flavour.js +106 -0
- package/src/lib/templates.js +187 -63
package/README.md
CHANGED
|
@@ -1,15 +1,37 @@
|
|
|
1
|
-
# @neohm/nh-cli — `nh`
|
|
1
|
+
# @neohm/nh-cli — `nh` and `nhx`
|
|
2
2
|
|
|
3
3
|
A scaffolding CLI that accelerates development on **neohm. NestJS projects**
|
|
4
|
-
built on top of the shared [`@neohm/nestend`](../nestend) library
|
|
4
|
+
built on top of the shared [`@neohm/nestend`](../nestend) library — or its B2C
|
|
5
|
+
sibling [`@neohm/nestendx`](../nestendx).
|
|
5
6
|
|
|
6
7
|
`nh` generates fully wired modules, entities, controllers, services, DTOs, data
|
|
7
8
|
processors, migrations and seeds — and keeps your import barrels
|
|
8
9
|
(`es6.classes.ts`) and `app.module.ts` registration in sync, so the project
|
|
9
10
|
builds without any manual import wiring.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
## Two flavours, one CLI
|
|
13
|
+
|
|
14
|
+
`nh` writes code against **`@neohm/nestend`**, `nhx` against
|
|
15
|
+
**`@neohm/nestendx`**. Same commands, same conventions, same wiring — they
|
|
16
|
+
differ only where the two packages do:
|
|
17
|
+
|
|
18
|
+
| | `nh` (nestend) | `nhx` (nestendx) |
|
|
19
|
+
| ------------------------ | ------------------------------------------ | ----------------------------------------- |
|
|
20
|
+
| Import specifier | `@neohm/nestend` | `@neohm/nestendx` |
|
|
21
|
+
| What a row belongs to | a tenant (`tenant_id` + `TenantEntity`) | the user who created it (`created_by`) |
|
|
22
|
+
| Access check | `validateAccess(scope, role)` | `validateAccess(scope)` — there are no roles |
|
|
23
|
+
| Generated migration | FK + index on `tenant_id` | no tenant column to key |
|
|
24
|
+
|
|
25
|
+
Everything that does not write package-specific code — `sync`, `check`, `list`,
|
|
26
|
+
`remove` — behaves identically under either name.
|
|
27
|
+
|
|
28
|
+
Running the wrong one is refused rather than half-generated: both read the
|
|
29
|
+
target project's `package.json`, and scaffolding tenanted code into a nestendx
|
|
30
|
+
project (or the reverse) stops with a message naming the command to use instead.
|
|
31
|
+
A project that depends on neither is taken at its word.
|
|
32
|
+
|
|
33
|
+
Every generated artifact matches the conventions of the reference project for
|
|
34
|
+
that flavour — `nestjs-boilerplate` for `nh`, `nestendx-boilerplate` for `nhx`:
|
|
13
35
|
|
|
14
36
|
- **Filenames** are `dot.separated.lowercase` (no hyphens) — e.g.
|
|
15
37
|
`shape.entity.ts`, and a multi-word `RoundShape` becomes
|
|
@@ -22,13 +44,15 @@ reference project:
|
|
|
22
44
|
```bash
|
|
23
45
|
# from the neohm-cli directory
|
|
24
46
|
npm install
|
|
25
|
-
npm link # exposes `nh` globally
|
|
47
|
+
npm link # exposes both `nh` and `nhx` globally
|
|
26
48
|
|
|
27
49
|
# or run without linking
|
|
28
50
|
npx nh --help
|
|
51
|
+
npx nhx --help
|
|
29
52
|
```
|
|
30
53
|
|
|
31
|
-
Run `nh` from **inside** a target NestJS project (e.g. `nestjs-boilerplate`)
|
|
54
|
+
Run `nh` from **inside** a target NestJS project (e.g. `nestjs-boilerplate`),
|
|
55
|
+
or `nhx` from inside a nestendx one (e.g. `nestendx-boilerplate`).
|
|
32
56
|
The project root is auto-detected by walking up from the current directory for a
|
|
33
57
|
`package.json` next to a `src/` folder.
|
|
34
58
|
|
|
@@ -46,6 +70,9 @@ The project root is auto-detected by walking up from the current directory for a
|
|
|
46
70
|
| `nh remove <name>` | Safely remove a scaffolded module and de-register it (migrations are preserved). |
|
|
47
71
|
| `nh --help` / `nh -v` | Help and version. |
|
|
48
72
|
|
|
73
|
+
Read `nhx` for `nh` throughout the table above when generating against
|
|
74
|
+
`@neohm/nestendx`.
|
|
75
|
+
|
|
49
76
|
### `nh gen`
|
|
50
77
|
|
|
51
78
|
```text
|
|
@@ -93,15 +120,21 @@ created spreading `baseJobConstants` / `baseEntityConstants` /
|
|
|
93
120
|
|
|
94
121
|
Generated artifacts extend the `@neohm/nestend` base classes:
|
|
95
122
|
|
|
96
|
-
- Entity → `
|
|
97
|
-
relation,
|
|
123
|
+
- Entity → `CommonTenantEntity` under `nh` (it carries `tenant_id` and the
|
|
124
|
+
`TenantEntity` relation, and `TenantScopeSubscriber` stamps and guards it),
|
|
125
|
+
`CommonEntity` under `nhx`; both with an empty `attributes` jsonb column
|
|
98
126
|
- DTO → `CommonDataDto`
|
|
99
127
|
- Data processor → `CommonDataProcessor` (`process()` → `validate()` → `set()`),
|
|
100
|
-
reached through the service's `upsert()
|
|
128
|
+
reached through the service's `upsert()`. It takes the caller and, on edit,
|
|
129
|
+
loads the row scoped to them — another owner's `id` is a field error, never an
|
|
130
|
+
update.
|
|
101
131
|
- Listing processor → `CommonListProcessor` (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.
|
|
102
132
|
|
|
103
133
|
### Controller → service → processor
|
|
104
134
|
|
|
135
|
+
Generated controllers run under `@UsePipes(StrictValidationPipe)`, take ids
|
|
136
|
+
with `ParseUUIDPipe`, and describe every route with `@ApiOperation`.
|
|
137
|
+
|
|
105
138
|
Controllers hold no business logic. They authenticate with
|
|
106
139
|
`AuthService.validateAccess()`, then delegate to the module's service — so the
|
|
107
140
|
same operation is reusable from a job, a subscriber or another service, and the
|
|
@@ -114,7 +147,7 @@ answers you already gave:
|
|
|
114
147
|
| Service method | Generated when | Controller endpoint |
|
|
115
148
|
| --- | --- | --- |
|
|
116
149
|
| `get(user, id)` | always | `GET /api/v1/shape/:id` |
|
|
117
|
-
| `upsert(data)` | data processor: Yes | `POST /api/v1/shape` |
|
|
150
|
+
| `upsert(user, data)` | data processor: Yes | `POST /api/v1/shape` |
|
|
118
151
|
| `list(user, data)` | listing processor: Yes | `POST /api/v1/shape/list` |
|
|
119
152
|
|
|
120
153
|
`get()` is the one operation that needs no processor, so a module that declines
|
|
@@ -188,9 +221,9 @@ Only one target flag per run; the module is created if it does not exist yet.
|
|
|
188
221
|
|
|
189
222
|
External files take an `external.` filename prefix and an `External` class
|
|
190
223
|
prefix. An external controller differs from an internal one in three ways, all
|
|
191
|
-
load-bearing: it mounts at `api/e/v1/<path>`, it
|
|
192
|
-
with `@
|
|
193
|
-
|
|
224
|
+
load-bearing: it mounts at `api/e/v1/<path>`, it documents the key header to
|
|
225
|
+
swagger with `@ApiHeader`, and it authenticates through `ApiKeyService` rather
|
|
226
|
+
than `AuthService`.
|
|
194
227
|
|
|
195
228
|
`-x` does not apply to `-e` or `-j`: a table and a queue worker have no
|
|
196
229
|
inward/outward face, so there is nothing to mirror. Both refuse the flag rather
|
package/bin/nh.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
// Generates @neohm/nestend code — the tenanted model. `nhx` is the same CLI
|
|
5
|
+
// against @neohm/nestendx; the flavour is set before anything else loads,
|
|
6
|
+
// because the templates read it while they are being defined.
|
|
7
|
+
require('../src/lib/flavour').setFlavour('nestend');
|
|
8
|
+
|
|
4
9
|
const { program } = require('../src/index');
|
|
5
10
|
|
|
6
11
|
program.parseAsync(process.argv);
|
package/bin/nhx.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// The nestendx flavour of `nh`: same commands, same conventions, code written
|
|
5
|
+
// against @neohm/nestendx — users hold scopes rather than roles, and a row
|
|
6
|
+
// belongs to whoever created it rather than to a tenant.
|
|
7
|
+
require('../src/lib/flavour').setFlavour('nestendx');
|
|
8
|
+
|
|
9
|
+
const { program } = require('../src/index');
|
|
10
|
+
|
|
11
|
+
program.parseAsync(process.argv);
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neohm/nh-cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "nh — Command Line Interface for neohm. NestJS projects",
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"description": "nh / nhx — Command Line Interface for neohm. NestJS projects",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"nh": "bin/nh.js"
|
|
7
|
+
"nh": "bin/nh.js",
|
|
8
|
+
"nhx": "bin/nhx.js"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
11
|
"start": "node bin/nh.js",
|
|
11
|
-
"prepublishOnly": "find bin src -name '*.js' | xargs node --check"
|
|
12
|
+
"prepublishOnly": "find bin src -name '*.js' | xargs node --check",
|
|
13
|
+
"start:x": "node bin/nhx.js"
|
|
12
14
|
},
|
|
13
15
|
"packageManager": "yarn@1.22.22",
|
|
14
16
|
"files": [
|
|
@@ -20,7 +22,8 @@
|
|
|
20
22
|
"cli",
|
|
21
23
|
"scaffold",
|
|
22
24
|
"neohm",
|
|
23
|
-
"nestend"
|
|
25
|
+
"nestend",
|
|
26
|
+
"nestendx"
|
|
24
27
|
],
|
|
25
28
|
"author": "Ashim Baral",
|
|
26
29
|
"license": "MIT",
|
package/src/commands/check.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const chalk = require('chalk');
|
|
5
|
+
const { cli } = require('../lib/flavour');
|
|
5
6
|
|
|
6
7
|
const { projectPaths } = require('../lib/paths');
|
|
7
8
|
const engine = require('../lib/sync.engine');
|
|
@@ -47,9 +48,13 @@ async function check() {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
if (changes) {
|
|
50
|
-
console.log(
|
|
51
|
+
console.log(
|
|
52
|
+
chalk.yellow(
|
|
53
|
+
`\n${changes} file(s) would change. Run \`${cli()} sync\` to apply.`,
|
|
54
|
+
),
|
|
55
|
+
);
|
|
51
56
|
} else {
|
|
52
|
-
log.done(
|
|
57
|
+
log.done(`In sync — \`${cli()} sync\` would make no changes.`);
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
60
|
|
package/src/commands/gen.flag.js
CHANGED
|
@@ -6,6 +6,7 @@ const inquirer = require('inquirer');
|
|
|
6
6
|
const chalk = require('chalk');
|
|
7
7
|
|
|
8
8
|
const { projectPaths, MODULE_SUBFOLDERS } = require('../lib/paths');
|
|
9
|
+
const { assertFlavourMatchesProject, cli } = require('../lib/flavour');
|
|
9
10
|
const naming = require('../lib/naming');
|
|
10
11
|
const tpl = require('../lib/templates');
|
|
11
12
|
const {
|
|
@@ -170,6 +171,7 @@ async function genFlag(target, { external = false } = {}) {
|
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
const paths = projectPaths();
|
|
174
|
+
assertFlavourMatchesProject(paths.root);
|
|
173
175
|
log.info(`Project: ${paths.root}\n`);
|
|
174
176
|
|
|
175
177
|
const touched = [];
|
|
@@ -281,7 +283,13 @@ async function genFlag(target, { external = false } = {}) {
|
|
|
281
283
|
break;
|
|
282
284
|
|
|
283
285
|
case 'dataProcessor':
|
|
284
|
-
write(
|
|
286
|
+
write(
|
|
287
|
+
dest,
|
|
288
|
+
tpl.processor(rawName, {
|
|
289
|
+
external,
|
|
290
|
+
withEntity: fs.existsSync(path.join(moduleDir, 'entities', n.entityFile)),
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
285
293
|
// The processor's payload DTO lives in the module's dtos/ either way —
|
|
286
294
|
// created here only if it is not already there.
|
|
287
295
|
write(path.join(moduleDir, 'dtos', n.dtoFile), tpl.dto(rawName));
|
|
@@ -346,7 +354,9 @@ async function genFlag(target, { external = false } = {}) {
|
|
|
346
354
|
const made = artifacts.map((a) => a.className).join(', ');
|
|
347
355
|
log.done(`${made} generated in "${moduleDot}".`);
|
|
348
356
|
console.log(
|
|
349
|
-
chalk.dim(
|
|
357
|
+
chalk.dim(
|
|
358
|
+
`Run \`${cli()} sync\` anytime to re-wire imports after manual edits.`,
|
|
359
|
+
),
|
|
350
360
|
);
|
|
351
361
|
}
|
|
352
362
|
|
package/src/commands/gen.js
CHANGED
|
@@ -6,6 +6,7 @@ const inquirer = require("inquirer");
|
|
|
6
6
|
const chalk = require("chalk");
|
|
7
7
|
|
|
8
8
|
const { projectPaths, MODULE_SUBFOLDERS } = require("../lib/paths");
|
|
9
|
+
const { assertFlavourMatchesProject, cli } = require("../lib/flavour");
|
|
9
10
|
const naming = require("../lib/naming");
|
|
10
11
|
const tpl = require("../lib/templates");
|
|
11
12
|
const { writeFile, report: reportFile, log, ensureDir } = require("../lib/fs.utils");
|
|
@@ -15,6 +16,7 @@ const { registerJob, registerEntity } = require("../lib/registry");
|
|
|
15
16
|
|
|
16
17
|
async function gen() {
|
|
17
18
|
const paths = projectPaths();
|
|
19
|
+
assertFlavourMatchesProject(paths.root);
|
|
18
20
|
log.info(`Project: ${paths.root}\n`);
|
|
19
21
|
|
|
20
22
|
// Every file this run writes or rewrites, formatted in one pass at the end.
|
|
@@ -133,7 +135,7 @@ async function gen() {
|
|
|
133
135
|
report(writeFile(dtoPath, tpl.dto(entityName)), dtoPath, paths.root);
|
|
134
136
|
|
|
135
137
|
const procPath = path.join(moduleDir, "libraries", n.processorFile);
|
|
136
|
-
report(writeFile(procPath, tpl.processor(entityName)), procPath, paths.root);
|
|
138
|
+
report(writeFile(procPath, tpl.processor(entityName, { withEntity: true })), procPath, paths.root);
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
// 6. Optional: listing processor (takes CommonListFilterDto directly) ------
|
|
@@ -215,7 +217,9 @@ async function gen() {
|
|
|
215
217
|
|
|
216
218
|
log.done(`Module "${moduleDot}" scaffolded.`);
|
|
217
219
|
console.log(
|
|
218
|
-
chalk.dim(
|
|
220
|
+
chalk.dim(
|
|
221
|
+
`Run \`${cli()} sync\` anytime to re-wire imports after manual edits.`,
|
|
222
|
+
),
|
|
219
223
|
);
|
|
220
224
|
}
|
|
221
225
|
|
|
@@ -4,12 +4,14 @@ const path = require("path");
|
|
|
4
4
|
const inquirer = require("inquirer");
|
|
5
5
|
|
|
6
6
|
const { projectPaths } = require("../lib/paths");
|
|
7
|
+
const { assertFlavourMatchesProject } = require("../lib/flavour");
|
|
7
8
|
const naming = require("../lib/naming");
|
|
8
9
|
const tpl = require("../lib/templates");
|
|
9
10
|
const { writeFile, report, log } = require("../lib/fs.utils");
|
|
10
11
|
|
|
11
12
|
async function migration() {
|
|
12
13
|
const paths = projectPaths();
|
|
14
|
+
assertFlavourMatchesProject(paths.root);
|
|
13
15
|
|
|
14
16
|
const { tableName, migrationName } = await inquirer.prompt([
|
|
15
17
|
{
|
package/src/commands/seed.js
CHANGED
|
@@ -4,12 +4,14 @@ const path = require("path");
|
|
|
4
4
|
const inquirer = require("inquirer");
|
|
5
5
|
|
|
6
6
|
const { projectPaths } = require("../lib/paths");
|
|
7
|
+
const { assertFlavourMatchesProject } = require("../lib/flavour");
|
|
7
8
|
const naming = require("../lib/naming");
|
|
8
9
|
const tpl = require("../lib/templates");
|
|
9
10
|
const { writeFile, report, log } = require("../lib/fs.utils");
|
|
10
11
|
|
|
11
12
|
async function seed() {
|
|
12
13
|
const paths = projectPaths();
|
|
14
|
+
assertFlavourMatchesProject(paths.root);
|
|
13
15
|
|
|
14
16
|
const { tableName, seedName } = await inquirer.prompt([
|
|
15
17
|
{
|
package/src/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { Command } = require('commander');
|
|
4
4
|
const chalk = require('chalk');
|
|
5
5
|
const pkg = require('../package.json');
|
|
6
|
+
const flavour = require('./lib/flavour');
|
|
6
7
|
|
|
7
8
|
const program = new Command();
|
|
8
9
|
|
|
@@ -17,14 +18,21 @@ function run(action) {
|
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
// `nh` and `nhx` are the same program against different packages: the bin
|
|
22
|
+
// entrypoint picks the flavour before this module is required, and everything
|
|
23
|
+
// that generates code reads it from there.
|
|
24
|
+
const { cli, package: packageName } = flavour.current();
|
|
25
|
+
|
|
20
26
|
program
|
|
21
|
-
.name(
|
|
22
|
-
.description(
|
|
27
|
+
.name(cli)
|
|
28
|
+
.description(
|
|
29
|
+
`${cli} — scaffolding CLI for neohm. NestJS projects (built on ${packageName})`,
|
|
30
|
+
)
|
|
23
31
|
.version(pkg.version, '-v, --version', 'output the current version');
|
|
24
32
|
|
|
25
|
-
// `
|
|
26
|
-
//
|
|
27
|
-
//
|
|
33
|
+
// `gen` with no target flag runs the full wizard; a target flag generates just
|
|
34
|
+
// that one artifact. -x mirrors a target onto the module's external (API-key)
|
|
35
|
+
// surface and applies to -c, -s, -dp and -lp only.
|
|
28
36
|
const GEN_TARGETS = [
|
|
29
37
|
['controller', 'controller'],
|
|
30
38
|
['service', 'service'],
|
|
@@ -50,15 +58,15 @@ program
|
|
|
50
58
|
'after',
|
|
51
59
|
`
|
|
52
60
|
Examples:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
${cli} gen full wizard: module, entity, controller, service, ...
|
|
62
|
+
${cli} gen -c controller only (asks module, name, route path)
|
|
63
|
+
${cli} gen -s bare injectable service
|
|
64
|
+
${cli} gen -dp data processor (+ its Add<Name>DataDto if missing)
|
|
65
|
+
${cli} gen -lp listing processor
|
|
66
|
+
${cli} gen -e entity + migration, registered in the constants/maps
|
|
67
|
+
${cli} gen -j job, registered in job.constants/job.maps
|
|
68
|
+
${cli} gen -x -c external controller under <module>/external/controllers
|
|
69
|
+
${cli} gen -x -dp external data processor under <module>/external/libraries`,
|
|
62
70
|
)
|
|
63
71
|
.action(
|
|
64
72
|
run((opts) => {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Which package the generated code is written against.
|
|
8
|
+
*
|
|
9
|
+
* The two share every convention this CLI encodes — dot.separated filenames,
|
|
10
|
+
* the processor lifecycle, es6.classes barrels, the migration utility — and
|
|
11
|
+
* differ in what a record belongs to. `nestend` is the tenanted model: rows
|
|
12
|
+
* carry a `tenant_id`, access is a scope *and* a role. `nestendx` is the B2C
|
|
13
|
+
* one: users register themselves and hold scopes alone, so a row belongs to the
|
|
14
|
+
* user who created it and access is a scope.
|
|
15
|
+
*
|
|
16
|
+
* `nh` generates the first, `nhx` the second. Everything that does not generate
|
|
17
|
+
* code — sync, check, list, remove — is the same command either way.
|
|
18
|
+
*/
|
|
19
|
+
const FLAVOURS = {
|
|
20
|
+
nestend: {
|
|
21
|
+
key: 'nestend',
|
|
22
|
+
cli: 'nh',
|
|
23
|
+
package: '@neohm/nestend',
|
|
24
|
+
/** Rows belong to a tenant; the scope check carries a role beside it. */
|
|
25
|
+
tenantScoped: true,
|
|
26
|
+
},
|
|
27
|
+
nestendx: {
|
|
28
|
+
key: 'nestendx',
|
|
29
|
+
cli: 'nhx',
|
|
30
|
+
package: '@neohm/nestendx',
|
|
31
|
+
tenantScoped: false,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let active = FLAVOURS.nestend;
|
|
36
|
+
|
|
37
|
+
/** Set by the bin entrypoint before anything else is required. */
|
|
38
|
+
function setFlavour(key) {
|
|
39
|
+
const flavour = FLAVOURS[key];
|
|
40
|
+
if (!flavour) throw new Error(`Unknown flavour "${key}".`);
|
|
41
|
+
active = flavour;
|
|
42
|
+
return active;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function current() {
|
|
46
|
+
return active;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The package specifier generated imports are written against. */
|
|
50
|
+
function pkg() {
|
|
51
|
+
return active.package;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The command the person actually typed, for help text and hints. */
|
|
55
|
+
function cli() {
|
|
56
|
+
return active.cli;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Which package a project depends on, read from its package.json. Returns null
|
|
61
|
+
* when it depends on neither — a project that has not installed one yet is not
|
|
62
|
+
* an error, it is simply not evidence either way.
|
|
63
|
+
*/
|
|
64
|
+
function detectFlavour(projectRoot) {
|
|
65
|
+
const manifest = path.join(projectRoot, 'package.json');
|
|
66
|
+
let deps;
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(fs.readFileSync(manifest, 'utf8'));
|
|
69
|
+
deps = { ...parsed.dependencies, ...parsed.devDependencies };
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// nestendx first: "@neohm/nestend" is a prefix of it, and a project can only
|
|
75
|
+
// sensibly depend on one.
|
|
76
|
+
if (deps[FLAVOURS.nestendx.package]) return FLAVOURS.nestendx.key;
|
|
77
|
+
if (deps[FLAVOURS.nestend.package]) return FLAVOURS.nestend.key;
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Refuse to scaffold against the wrong package. Generating tenanted code into a
|
|
83
|
+
* nestendx project produces files that import a package that is not there and
|
|
84
|
+
* columns no table has — a failure the person would meet later, in a compiler
|
|
85
|
+
* error, with nothing pointing back here.
|
|
86
|
+
*/
|
|
87
|
+
function assertFlavourMatchesProject(projectRoot) {
|
|
88
|
+
const detected = detectFlavour(projectRoot);
|
|
89
|
+
if (!detected || detected === active.key) return;
|
|
90
|
+
|
|
91
|
+
const wanted = FLAVOURS[detected];
|
|
92
|
+
throw new Error(
|
|
93
|
+
`This project depends on ${wanted.package}, but you ran ${active.cli}, ` +
|
|
94
|
+
`which generates ${active.package} code.\n Use \`${wanted.cli}\` here instead.`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
FLAVOURS,
|
|
100
|
+
setFlavour,
|
|
101
|
+
current,
|
|
102
|
+
pkg,
|
|
103
|
+
cli,
|
|
104
|
+
detectFlavour,
|
|
105
|
+
assertFlavourMatchesProject,
|
|
106
|
+
};
|
package/src/lib/templates.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const { pkg, current } = require('./flavour');
|
|
3
4
|
const {
|
|
4
5
|
toWords,
|
|
5
6
|
toDotCase,
|
|
@@ -11,10 +12,17 @@ const {
|
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Code templates. All generated code imports shared base classes from the
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* 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.
|
|
18
26
|
*/
|
|
19
27
|
|
|
20
28
|
/**
|
|
@@ -75,26 +83,40 @@ function names(base) {
|
|
|
75
83
|
|
|
76
84
|
function entity(base, tableName) {
|
|
77
85
|
const n = names(base);
|
|
78
|
-
|
|
79
|
-
|
|
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()}';
|
|
80
92
|
import { ${n.attributesDtoClass} } from '../dtos/${n.dot}.attributes.dto';
|
|
81
93
|
|
|
82
94
|
@Entity({ name: '${tableName}' })
|
|
83
95
|
export class ${n.entityClass} extends CommonEntity {
|
|
84
|
-
// Tenant scope — the service and the list processor both filter on it.
|
|
85
|
-
@Column()
|
|
86
|
-
tenant_id: string;
|
|
87
|
-
|
|
88
96
|
// TODO: declare the columns ${n.pascal} owns.
|
|
89
97
|
|
|
90
98
|
@Column({ type: 'jsonb' })
|
|
91
99
|
attributes: ${n.attributesDtoClass};
|
|
92
100
|
|
|
93
101
|
// Relations and methods
|
|
102
|
+
}
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// CommonTenantEntity adds tenant_id + the tenant relation; TenantScopeSubscriber
|
|
107
|
+
// stamps it from the session and refuses cross-tenant writes.
|
|
108
|
+
return `import { Column, Entity } from 'typeorm';
|
|
109
|
+
import { CommonTenantEntity } from '${pkg()}';
|
|
110
|
+
import { ${n.attributesDtoClass} } from '../dtos/${n.dot}.attributes.dto';
|
|
111
|
+
|
|
112
|
+
@Entity({ name: '${tableName}' })
|
|
113
|
+
export class ${n.entityClass} extends CommonTenantEntity {
|
|
114
|
+
// TODO: declare the columns ${n.pascal} owns.
|
|
115
|
+
|
|
116
|
+
@Column({ type: 'jsonb' })
|
|
117
|
+
attributes: ${n.attributesDtoClass};
|
|
94
118
|
|
|
95
|
-
|
|
96
|
-
@JoinColumn({ name: 'tenant_id' })
|
|
97
|
-
tenant: TenantEntity;
|
|
119
|
+
// Relations and methods
|
|
98
120
|
}
|
|
99
121
|
`;
|
|
100
122
|
}
|
|
@@ -118,13 +140,13 @@ function controller(base, { processor = false, listProcessor = false } = {}) {
|
|
|
118
140
|
const hasPost = processor || listProcessor;
|
|
119
141
|
|
|
120
142
|
// @nestjs/common members, alphabetised the way prettier leaves them.
|
|
121
|
-
const nestCommon = ['Controller', 'Get', 'Param'];
|
|
143
|
+
const nestCommon = ['Controller', 'Get', 'Param', 'ParseUUIDPipe', 'UsePipes'];
|
|
122
144
|
if (hasPost) nestCommon.push('Body', 'Post');
|
|
123
145
|
nestCommon.sort();
|
|
124
146
|
|
|
125
147
|
const nestendImports = ['AuthService'];
|
|
126
148
|
if (listProcessor) nestendImports.push('CommonListFilterDto');
|
|
127
|
-
nestendImports.push('
|
|
149
|
+
nestendImports.push('StrictValidationPipe');
|
|
128
150
|
|
|
129
151
|
const imports = [
|
|
130
152
|
namedImport(nestCommon, '@nestjs/common'),
|
|
@@ -132,8 +154,8 @@ function controller(base, { processor = false, listProcessor = false } = {}) {
|
|
|
132
154
|
// DocumentBuilder call only registers the scheme (under its default name,
|
|
133
155
|
// 'bearer') and draws the Authorize button — without the decorator
|
|
134
156
|
// swagger-ui shows no padlock and never sends the Authorization header.
|
|
135
|
-
`import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';`,
|
|
136
|
-
namedImport(nestendImports,
|
|
157
|
+
`import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';`,
|
|
158
|
+
namedImport(nestendImports, pkg()),
|
|
137
159
|
`import { ${n.serviceClass} } from '../services/${n.dot}.service';`,
|
|
138
160
|
];
|
|
139
161
|
if (processor) {
|
|
@@ -142,43 +164,54 @@ function controller(base, { processor = false, listProcessor = false } = {}) {
|
|
|
142
164
|
);
|
|
143
165
|
}
|
|
144
166
|
|
|
167
|
+
// Tenanted access takes a scope and a role; the B2C model has no roles at
|
|
168
|
+
// all, so the same call carries the scope alone.
|
|
169
|
+
const tenanted = current().tenantScoped;
|
|
170
|
+
const gate = (indent) =>
|
|
171
|
+
tenanted
|
|
172
|
+
? `await this.authService.validateAccess(\n${indent} 'add-scope',\n${indent} 'add-role',\n${indent})`
|
|
173
|
+
: `await this.authService.validateAccess('add-scope')`;
|
|
174
|
+
|
|
145
175
|
const methods = [
|
|
146
176
|
` @Get(':id')
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
'add-role',
|
|
151
|
-
);
|
|
177
|
+
@ApiOperation({ summary: 'One ${n.kebab} by id.' })
|
|
178
|
+
async get${n.pascal}(@Param('id', ParseUUIDPipe) id: string) {
|
|
179
|
+
const { user } = ${gate(' ')};
|
|
152
180
|
|
|
153
|
-
return this.${svcProp}.get(user,
|
|
181
|
+
return this.${svcProp}.get(user, id);
|
|
154
182
|
}`,
|
|
155
183
|
];
|
|
156
184
|
|
|
157
185
|
if (processor) {
|
|
158
186
|
methods.push(` @Post()
|
|
187
|
+
@ApiOperation({ summary: 'Create a ${n.kebab}, or update it when an id is sent.' })
|
|
159
188
|
async upsert${n.pascal}(@Body() body: ${n.dtoClass}) {
|
|
160
|
-
|
|
189
|
+
const { user } = ${gate(' ')};
|
|
161
190
|
|
|
162
|
-
return this.${svcProp}.upsert(body);
|
|
191
|
+
return this.${svcProp}.upsert(user, body);
|
|
163
192
|
}`);
|
|
164
193
|
}
|
|
165
194
|
|
|
166
195
|
if (listProcessor) {
|
|
167
196
|
methods.push(` @Post('list')
|
|
197
|
+
@ApiOperation({ summary: 'List, search and filter ${n.kebab}.' })
|
|
168
198
|
async list(@Body() body: CommonListFilterDto) {
|
|
169
|
-
const { user } =
|
|
170
|
-
|
|
171
|
-
'add-role',
|
|
172
|
-
);
|
|
199
|
+
const { user } = ${gate(' ')};
|
|
200
|
+
|
|
173
201
|
return this.${svcProp}.list(user, body);
|
|
174
202
|
}`);
|
|
175
203
|
}
|
|
176
204
|
|
|
205
|
+
const gateTodo = tenanted
|
|
206
|
+
? `// TODO: replace 'add-scope' / 'add-role' with this module's scope and role.`
|
|
207
|
+
: `// 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.`;
|
|
208
|
+
|
|
177
209
|
return `${imports.join('\n')}
|
|
178
210
|
|
|
179
|
-
|
|
211
|
+
${gateTodo}
|
|
180
212
|
@ApiTags('${n.kebab}')
|
|
181
213
|
@ApiBearerAuth()
|
|
214
|
+
@UsePipes(StrictValidationPipe)
|
|
182
215
|
@Controller('api/v1/${n.kebab}')
|
|
183
216
|
export class ${n.controllerClass} {
|
|
184
217
|
constructor(
|
|
@@ -210,7 +243,7 @@ function service(base, { processor = false, listProcessor = false } = {}) {
|
|
|
210
243
|
nestendImports.push('NotFoundException', 'UserEntity');
|
|
211
244
|
|
|
212
245
|
const imports = [
|
|
213
|
-
namedImport(nestendImports,
|
|
246
|
+
namedImport(nestendImports, pkg()),
|
|
214
247
|
`import { Injectable } from '@nestjs/common';`,
|
|
215
248
|
];
|
|
216
249
|
if (listProcessor) {
|
|
@@ -235,11 +268,15 @@ function service(base, { processor = false, listProcessor = false } = {}) {
|
|
|
235
268
|
);
|
|
236
269
|
}
|
|
237
270
|
|
|
238
|
-
//
|
|
239
|
-
//
|
|
271
|
+
// Ownership is part of the lookup, not a check after it: a row belonging to
|
|
272
|
+
// someone else must read as "not found", never as "forbidden". Tenanted rows
|
|
273
|
+
// carry the organization; without tenants it is the user who created it.
|
|
274
|
+
const owner = current().tenantScoped
|
|
275
|
+
? 'tenant_id: user.organization_id'
|
|
276
|
+
: 'created_by: user.id';
|
|
240
277
|
members.push(` async get(user: UserEntity, id: string) {
|
|
241
278
|
const ${n.camel} = await ${n.entityClass}.findOne({
|
|
242
|
-
where: { id,
|
|
279
|
+
where: { id, ${owner} },
|
|
243
280
|
});
|
|
244
281
|
if (!${n.camel}) throw new NotFoundException('Invalid ${label} id');
|
|
245
282
|
|
|
@@ -249,8 +286,8 @@ function service(base, { processor = false, listProcessor = false } = {}) {
|
|
|
249
286
|
if (processor) {
|
|
250
287
|
// Insert or update: CommonDataDto carries an optional `id`, and the data
|
|
251
288
|
// processor decides which of the two the payload asks for.
|
|
252
|
-
members.push(` async upsert(data: ${n.dtoClass}) {
|
|
253
|
-
return new ${n.processorClass}().process(data);
|
|
289
|
+
members.push(` async upsert(user: UserEntity, data: ${n.dtoClass}) {
|
|
290
|
+
return new ${n.processorClass}(user).process(data);
|
|
254
291
|
}`);
|
|
255
292
|
}
|
|
256
293
|
|
|
@@ -290,12 +327,13 @@ ${members.join('\n\n')}
|
|
|
290
327
|
*/
|
|
291
328
|
function bareController(base, routePath) {
|
|
292
329
|
const n = names(base);
|
|
293
|
-
return `import { Controller, Get } from '@nestjs/common';
|
|
330
|
+
return `import { Controller, Get, UsePipes } from '@nestjs/common';
|
|
294
331
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
|
295
|
-
import { AuthService } from '
|
|
332
|
+
import { AuthService, StrictValidationPipe } from '${pkg()}';
|
|
296
333
|
|
|
297
334
|
@ApiTags('${routePath}')
|
|
298
335
|
@ApiBearerAuth()
|
|
336
|
+
@UsePipes(StrictValidationPipe)
|
|
299
337
|
@Controller('api/v1/${routePath}')
|
|
300
338
|
export class ${n.controllerClass} {
|
|
301
339
|
constructor(private readonly authService: AuthService) {}
|
|
@@ -314,12 +352,13 @@ export class ${n.controllerClass} {
|
|
|
314
352
|
*/
|
|
315
353
|
function bareExternalController(base, routePath) {
|
|
316
354
|
const n = names(base);
|
|
317
|
-
return `import { Controller, Get } from '@nestjs/common';
|
|
318
|
-
import {
|
|
319
|
-
import { ApiKeyService } from '
|
|
355
|
+
return `import { Controller, Get, UsePipes } from '@nestjs/common';
|
|
356
|
+
import { ApiHeader, ApiTags } from '@nestjs/swagger';
|
|
357
|
+
import { ApiKeyService, StrictValidationPipe } from '${pkg()}';
|
|
320
358
|
|
|
321
359
|
@ApiTags('external-${routePath} (Api key)')
|
|
322
|
-
@
|
|
360
|
+
@ApiHeader({ name: 'x-api-key', required: true })
|
|
361
|
+
@UsePipes(StrictValidationPipe)
|
|
323
362
|
@Controller('api/e/v1/${routePath}')
|
|
324
363
|
export class ${n.externalControllerClass} {
|
|
325
364
|
constructor(private readonly apiKeyService: ApiKeyService) {}
|
|
@@ -348,7 +387,7 @@ export class ${cls} {}
|
|
|
348
387
|
*/
|
|
349
388
|
function additionalJob(base) {
|
|
350
389
|
const n = names(base);
|
|
351
|
-
return `import { CommonJob, QueueService } from '
|
|
390
|
+
return `import { CommonJob, QueueService } from '${pkg()}';
|
|
352
391
|
import { Injectable } from '@nestjs/common';
|
|
353
392
|
import { JobConstants } from '../../constants/job.constants';
|
|
354
393
|
|
|
@@ -367,7 +406,7 @@ export class ${n.jobClass} extends CommonJob {
|
|
|
367
406
|
|
|
368
407
|
function dto(base) {
|
|
369
408
|
const n = names(base);
|
|
370
|
-
return `import { CommonDataDto } from '
|
|
409
|
+
return `import { CommonDataDto } from '${pkg()}';
|
|
371
410
|
|
|
372
411
|
export class ${n.dtoClass} extends CommonDataDto {
|
|
373
412
|
// TODO: declare validated payload fields for ${n.pascal}.
|
|
@@ -375,13 +414,14 @@ export class ${n.dtoClass} extends CommonDataDto {
|
|
|
375
414
|
`;
|
|
376
415
|
}
|
|
377
416
|
|
|
378
|
-
function processor(base, { external = false } = {}) {
|
|
417
|
+
function processor(base, { external = false, withEntity = false } = {}) {
|
|
379
418
|
const n = names(base);
|
|
380
419
|
// An external processor sits one level deeper (external/libraries/), so its
|
|
381
420
|
// reach back into the module's dtos/ gains a segment.
|
|
382
421
|
const up = external ? '../..' : '..';
|
|
383
422
|
const cls = external ? n.externalProcessorClass : n.processorClass;
|
|
384
|
-
|
|
423
|
+
if (external) {
|
|
424
|
+
return `import { CommonDataProcessor } from '${pkg()}';
|
|
385
425
|
import { ${n.dtoClass} } from '${up}/dtos/add.${n.dot}.data.dto';
|
|
386
426
|
|
|
387
427
|
export class ${cls} extends CommonDataProcessor {
|
|
@@ -404,6 +444,80 @@ export class ${cls} extends CommonDataProcessor {
|
|
|
404
444
|
// TODO: persist ${n.pascal} using this.payload.
|
|
405
445
|
}
|
|
406
446
|
}
|
|
447
|
+
`;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Edits load the caller's own row: another owner's id reads as not found.
|
|
451
|
+
const owner = current().tenantScoped
|
|
452
|
+
? 'tenant_id: this.user.organization_id'
|
|
453
|
+
: 'created_by: this.user.id';
|
|
454
|
+
const label = toWords(base).join(' ');
|
|
455
|
+
const entityImport = withEntity
|
|
456
|
+
? `\nimport { ${n.entityClass} } from '${up}/entities/${n.dot}.entity';`
|
|
457
|
+
: '';
|
|
458
|
+
const existingField = withEntity
|
|
459
|
+
? `\n private existing: ${n.entityClass} | null = null;`
|
|
460
|
+
: '';
|
|
461
|
+
const load = withEntity
|
|
462
|
+
? ` if (this.payload.id) {
|
|
463
|
+
this.existing = await ${n.entityClass}.findOne({
|
|
464
|
+
where: { id: this.payload.id, ${owner} },
|
|
465
|
+
});
|
|
466
|
+
if (!this.existing) this.setColumnError('id', 'Invalid ${label} id');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
`
|
|
470
|
+
: ` // TODO: on edit (payload.id), load the row scoped to the caller
|
|
471
|
+
// ({ id, ${owner} }) and record an 'id' error when it is not found.
|
|
472
|
+
|
|
473
|
+
`;
|
|
474
|
+
const set = withEntity
|
|
475
|
+
? ` private set(): Promise<${n.entityClass}> {
|
|
476
|
+
const ${n.camel} = this.existing ?? ${n.entityClass}.create();
|
|
477
|
+
// TODO: assign fields from this.payload explicitly — never spread the DTO.
|
|
478
|
+
|
|
479
|
+
return ${n.camel}.save();
|
|
480
|
+
}`
|
|
481
|
+
: ` private set(): void {
|
|
482
|
+
// TODO: persist ${n.pascal} from this.payload, assigning fields explicitly.
|
|
483
|
+
}`;
|
|
484
|
+
return `import { CommonDataProcessor, UserEntity } from '${pkg()}';
|
|
485
|
+
import { ${n.dtoClass} } from '${up}/dtos/add.${n.dot}.data.dto';${entityImport}
|
|
486
|
+
|
|
487
|
+
export class ${cls} extends CommonDataProcessor {
|
|
488
|
+
protected payload: ${n.dtoClass};${existingField}
|
|
489
|
+
|
|
490
|
+
constructor(private readonly user: UserEntity) {
|
|
491
|
+
super();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
${
|
|
495
|
+
withEntity
|
|
496
|
+
? ` async process(data: ${n.dtoClass}) {
|
|
497
|
+
this.payload = data;
|
|
498
|
+
|
|
499
|
+
await this.validate();
|
|
500
|
+
|
|
501
|
+
return this.set();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private async validate() {`
|
|
505
|
+
: ` process(data: ${n.dtoClass}) {
|
|
506
|
+
this.payload = data;
|
|
507
|
+
|
|
508
|
+
this.validate();
|
|
509
|
+
|
|
510
|
+
return this.set();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private validate(): void {`
|
|
514
|
+
}
|
|
515
|
+
${load} // TODO: collect validation errors via this.addErrors({ column: 'message' }).
|
|
516
|
+
this.raiseExceptionOnValidationFailure();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
${set}
|
|
520
|
+
}
|
|
407
521
|
`;
|
|
408
522
|
}
|
|
409
523
|
|
|
@@ -418,7 +532,7 @@ function listProcessor(base, tableName, { external = false } = {}) {
|
|
|
418
532
|
ListFilterService,
|
|
419
533
|
SortDirection,
|
|
420
534
|
UserEntity,
|
|
421
|
-
} from '
|
|
535
|
+
} from '${pkg()}';
|
|
422
536
|
|
|
423
537
|
/**
|
|
424
538
|
* Lists / filters ${n.pascal}.
|
|
@@ -474,8 +588,13 @@ export class ${cls} extends CommonListProcessor {
|
|
|
474
588
|
}
|
|
475
589
|
|
|
476
590
|
protected applyCustomPredicates() {
|
|
477
|
-
|
|
478
|
-
|
|
591
|
+
${
|
|
592
|
+
current().tenantScoped
|
|
593
|
+
? ` // Filter for the caller's tenant/organization
|
|
594
|
+
this.predicates.add('a.tenant_id = :tenant_id', this.user.organization_id);`
|
|
595
|
+
: ` // Filter for the caller: rows belong to whoever created them.
|
|
596
|
+
this.predicates.add('a.created_by = :created_by', this.user.id);`
|
|
597
|
+
}
|
|
479
598
|
}
|
|
480
599
|
}
|
|
481
600
|
`;
|
|
@@ -483,7 +602,7 @@ export class ${cls} extends CommonListProcessor {
|
|
|
483
602
|
|
|
484
603
|
function subscriber(base) {
|
|
485
604
|
const n = names(base);
|
|
486
|
-
return `import { CommonSubscriber } from '
|
|
605
|
+
return `import { CommonSubscriber } from '${pkg()}';
|
|
487
606
|
import { EventSubscriber } from 'typeorm';
|
|
488
607
|
import { ${n.entityClass} } from '../entities/${n.dot}.entity';
|
|
489
608
|
import { DataSource } from 'typeorm';
|
|
@@ -499,6 +618,9 @@ export class ${n.subscriberClass} extends CommonSubscriber<${n.entityClass}> {
|
|
|
499
618
|
datasource.subscribers.push(this);
|
|
500
619
|
}
|
|
501
620
|
|
|
621
|
+
// Jobs receive only \`id\` unless the columns they need are listed here.
|
|
622
|
+
protected readonly snapshotColumns: (keyof ${n.entityClass} & string)[] = [];
|
|
623
|
+
|
|
502
624
|
listenTo() {
|
|
503
625
|
return ${n.entityClass};
|
|
504
626
|
}
|
|
@@ -508,7 +630,7 @@ export class ${n.subscriberClass} extends CommonSubscriber<${n.entityClass}> {
|
|
|
508
630
|
|
|
509
631
|
function job(base) {
|
|
510
632
|
const n = names(base);
|
|
511
|
-
return `import { CommonJob, QueueService, DatabaseEventDto } from '
|
|
633
|
+
return `import { CommonJob, QueueService, DatabaseEventDto } from '${pkg()}';
|
|
512
634
|
import { Injectable } from '@nestjs/common';
|
|
513
635
|
import { JobConstants } from '../../constants/job.constants';
|
|
514
636
|
import { ${n.entityClass} } from '../entities/${n.dot}.entity';
|
|
@@ -534,7 +656,7 @@ export class ${n.jobClass} extends CommonJob {
|
|
|
534
656
|
function genMigration(base, tableName, timestamp) {
|
|
535
657
|
const n = names(base);
|
|
536
658
|
const className = migrationClassName(n.migrationName, timestamp);
|
|
537
|
-
return `import { MigrationUtility } from '
|
|
659
|
+
return `import { MigrationUtility } from '${pkg()}';
|
|
538
660
|
|
|
539
661
|
export class ${className} extends MigrationUtility {
|
|
540
662
|
constructor() {
|
|
@@ -545,8 +667,11 @@ export class ${className} extends MigrationUtility {
|
|
|
545
667
|
process() {
|
|
546
668
|
this.rootPrimary();
|
|
547
669
|
|
|
548
|
-
|
|
549
|
-
|
|
670
|
+
${
|
|
671
|
+
current().tenantScoped
|
|
672
|
+
? ` this.foreign({ name: 'tenant_id', foreignTable: 'sys_tenants' });\n`
|
|
673
|
+
: ''
|
|
674
|
+
} this.foreign({ name: 'created_by', foreignTable: 'sys_users' });
|
|
550
675
|
this.foreign({ name: 'updated_by', foreignTable: 'sys_users' });
|
|
551
676
|
|
|
552
677
|
// TODO: add columns matching ${n.entityClass}.
|
|
@@ -554,8 +679,7 @@ export class ${className} extends MigrationUtility {
|
|
|
554
679
|
this.json('attributes');
|
|
555
680
|
this.timestamps();
|
|
556
681
|
|
|
557
|
-
this.index('tenant_id');
|
|
558
|
-
this.index('created_by');
|
|
682
|
+
${current().tenantScoped ? ` this.index('tenant_id');\n` : ''} this.index('created_by');
|
|
559
683
|
this.index('updated_by');
|
|
560
684
|
}
|
|
561
685
|
}
|
|
@@ -565,7 +689,7 @@ export class ${className} extends MigrationUtility {
|
|
|
565
689
|
/** Standalone migration (`nh migration`). */
|
|
566
690
|
function migration(migrationName, tableName, timestamp) {
|
|
567
691
|
const className = migrationClassName(migrationName, timestamp);
|
|
568
|
-
return `import { MigrationUtility } from '
|
|
692
|
+
return `import { MigrationUtility } from '${pkg()}';
|
|
569
693
|
|
|
570
694
|
export class ${className} extends MigrationUtility {
|
|
571
695
|
constructor() {
|
|
@@ -585,7 +709,7 @@ export class ${className} extends MigrationUtility {
|
|
|
585
709
|
/** Seed file (`nh seed`). */
|
|
586
710
|
function seed(seedName, tableName, timestamp) {
|
|
587
711
|
const className = `${toPascalCase(seedName)}${timestamp}`;
|
|
588
|
-
return `import { SeederUtility } from '
|
|
712
|
+
return `import { SeederUtility } from '${pkg()}';
|
|
589
713
|
|
|
590
714
|
export class ${className} extends SeederUtility {
|
|
591
715
|
constructor() {
|
|
@@ -604,7 +728,7 @@ export class ${className} extends SeederUtility {
|
|
|
604
728
|
/** Fresh src/constants/job.constants.ts (first entry included). */
|
|
605
729
|
function jobConstantsFile(base) {
|
|
606
730
|
const n = names(base);
|
|
607
|
-
return `import { baseJobConstants } from '
|
|
731
|
+
return `import { baseJobConstants } from '${pkg()}';
|
|
608
732
|
|
|
609
733
|
export const JobConstants = {
|
|
610
734
|
...baseJobConstants,
|
|
@@ -616,7 +740,7 @@ export const JobConstants = {
|
|
|
616
740
|
/** Fresh src/constants/entity.constants.ts (first entry included). */
|
|
617
741
|
function entityConstantsFile(base) {
|
|
618
742
|
const n = names(base);
|
|
619
|
-
return `import { baseEntityConstants } from '
|
|
743
|
+
return `import { baseEntityConstants } from '${pkg()}';
|
|
620
744
|
|
|
621
745
|
export const EntityConstants = {
|
|
622
746
|
...baseEntityConstants,
|
|
@@ -628,7 +752,7 @@ export const EntityConstants = {
|
|
|
628
752
|
/** Fresh src/constants/job.maps.ts (first entry included). */
|
|
629
753
|
function jobMapsFile(base, moduleDot, constants) {
|
|
630
754
|
const n = names(base);
|
|
631
|
-
return `import { baseJobMaps } from '
|
|
755
|
+
return `import { baseJobMaps } from '${pkg()}';
|
|
632
756
|
import { ${n.jobClass} } from '../${moduleDot}/jobs/${n.dot}.job';
|
|
633
757
|
import { ${constants.exportName} } from '${constants.importPath}';
|
|
634
758
|
|
|
@@ -642,7 +766,7 @@ export const JobMaps = {
|
|
|
642
766
|
/** Fresh src/constants/entity.maps.ts (first entry included). */
|
|
643
767
|
function entityMapsFile(base, moduleDot, constants) {
|
|
644
768
|
const n = names(base);
|
|
645
|
-
return `import { baseEntityMaps } from '
|
|
769
|
+
return `import { baseEntityMaps } from '${pkg()}';
|
|
646
770
|
import { ${n.entityClass} } from '../${moduleDot}/entities/${n.dot}.entity';
|
|
647
771
|
import { ${constants.exportName} } from '${constants.importPath}';
|
|
648
772
|
|
|
@@ -674,7 +798,7 @@ import {
|
|
|
674
798
|
QueueModule,
|
|
675
799
|
AuthModule,
|
|
676
800
|
SystemPropertyModule,
|
|
677
|
-
} from '
|
|
801
|
+
} from '${pkg()}';
|
|
678
802
|
import { es6Classes } from './es6.classes';
|
|
679
803
|
|
|
680
804
|
@Module({
|