@neohm/nh-cli 1.2.0 → 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 +33 -6
- 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 +5 -1
- package/src/commands/gen.js +5 -1
- 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 +88 -42
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
|
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
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "nh / nhx \u2014 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 = [];
|
|
@@ -346,7 +348,9 @@ async function genFlag(target, { external = false } = {}) {
|
|
|
346
348
|
const made = artifacts.map((a) => a.className).join(', ');
|
|
347
349
|
log.done(`${made} generated in "${moduleDot}".`);
|
|
348
350
|
console.log(
|
|
349
|
-
chalk.dim(
|
|
351
|
+
chalk.dim(
|
|
352
|
+
`Run \`${cli()} sync\` anytime to re-wire imports after manual edits.`,
|
|
353
|
+
),
|
|
350
354
|
);
|
|
351
355
|
}
|
|
352
356
|
|
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.
|
|
@@ -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,8 +83,28 @@ function names(base) {
|
|
|
75
83
|
|
|
76
84
|
function entity(base, tableName) {
|
|
77
85
|
const n = names(base);
|
|
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
|
+
|
|
78
106
|
return `import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm';
|
|
79
|
-
import { CommonEntity, TenantEntity } from '
|
|
107
|
+
import { CommonEntity, TenantEntity } from '${pkg()}';
|
|
80
108
|
import { ${n.attributesDtoClass} } from '../dtos/${n.dot}.attributes.dto';
|
|
81
109
|
|
|
82
110
|
@Entity({ name: '${tableName}' })
|
|
@@ -133,7 +161,7 @@ function controller(base, { processor = false, listProcessor = false } = {}) {
|
|
|
133
161
|
// 'bearer') and draws the Authorize button — without the decorator
|
|
134
162
|
// swagger-ui shows no padlock and never sends the Authorization header.
|
|
135
163
|
`import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';`,
|
|
136
|
-
namedImport(nestendImports,
|
|
164
|
+
namedImport(nestendImports, pkg()),
|
|
137
165
|
`import { ${n.serviceClass} } from '../services/${n.dot}.service';`,
|
|
138
166
|
];
|
|
139
167
|
if (processor) {
|
|
@@ -142,13 +170,18 @@ function controller(base, { processor = false, listProcessor = false } = {}) {
|
|
|
142
170
|
);
|
|
143
171
|
}
|
|
144
172
|
|
|
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
|
+
|
|
145
181
|
const methods = [
|
|
146
182
|
` @Get(':id')
|
|
147
183
|
async get${n.pascal}(@Param() params: CommonParamsDto) {
|
|
148
|
-
const { user } =
|
|
149
|
-
'add-scope',
|
|
150
|
-
'add-role',
|
|
151
|
-
);
|
|
184
|
+
const { user } = ${gate(' ')};
|
|
152
185
|
|
|
153
186
|
return this.${svcProp}.get(user, params.id);
|
|
154
187
|
}`,
|
|
@@ -157,7 +190,7 @@ function controller(base, { processor = false, listProcessor = false } = {}) {
|
|
|
157
190
|
if (processor) {
|
|
158
191
|
methods.push(` @Post()
|
|
159
192
|
async upsert${n.pascal}(@Body() body: ${n.dtoClass}) {
|
|
160
|
-
await this.authService.validateAccess('add-scope', 'add-role')
|
|
193
|
+
${tenanted ? `await this.authService.validateAccess('add-scope', 'add-role');` : `await this.authService.validateAccess('add-scope');`}
|
|
161
194
|
|
|
162
195
|
return this.${svcProp}.upsert(body);
|
|
163
196
|
}`);
|
|
@@ -166,17 +199,19 @@ function controller(base, { processor = false, listProcessor = false } = {}) {
|
|
|
166
199
|
if (listProcessor) {
|
|
167
200
|
methods.push(` @Post('list')
|
|
168
201
|
async list(@Body() body: CommonListFilterDto) {
|
|
169
|
-
const { user } =
|
|
170
|
-
|
|
171
|
-
'add-role',
|
|
172
|
-
);
|
|
202
|
+
const { user } = ${gate(' ')};
|
|
203
|
+
|
|
173
204
|
return this.${svcProp}.list(user, body);
|
|
174
205
|
}`);
|
|
175
206
|
}
|
|
176
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
|
+
|
|
177
212
|
return `${imports.join('\n')}
|
|
178
213
|
|
|
179
|
-
|
|
214
|
+
${gateTodo}
|
|
180
215
|
@ApiTags('${n.kebab}')
|
|
181
216
|
@ApiBearerAuth()
|
|
182
217
|
@Controller('api/v1/${n.kebab}')
|
|
@@ -210,7 +245,7 @@ function service(base, { processor = false, listProcessor = false } = {}) {
|
|
|
210
245
|
nestendImports.push('NotFoundException', 'UserEntity');
|
|
211
246
|
|
|
212
247
|
const imports = [
|
|
213
|
-
namedImport(nestendImports,
|
|
248
|
+
namedImport(nestendImports, pkg()),
|
|
214
249
|
`import { Injectable } from '@nestjs/common';`,
|
|
215
250
|
];
|
|
216
251
|
if (listProcessor) {
|
|
@@ -235,11 +270,15 @@ function service(base, { processor = false, listProcessor = false } = {}) {
|
|
|
235
270
|
);
|
|
236
271
|
}
|
|
237
272
|
|
|
238
|
-
//
|
|
239
|
-
//
|
|
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';
|
|
240
279
|
members.push(` async get(user: UserEntity, id: string) {
|
|
241
280
|
const ${n.camel} = await ${n.entityClass}.findOne({
|
|
242
|
-
where: { id,
|
|
281
|
+
where: { id, ${owner} },
|
|
243
282
|
});
|
|
244
283
|
if (!${n.camel}) throw new NotFoundException('Invalid ${label} id');
|
|
245
284
|
|
|
@@ -292,7 +331,7 @@ function bareController(base, routePath) {
|
|
|
292
331
|
const n = names(base);
|
|
293
332
|
return `import { Controller, Get } from '@nestjs/common';
|
|
294
333
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
|
295
|
-
import { AuthService } from '
|
|
334
|
+
import { AuthService } from '${pkg()}';
|
|
296
335
|
|
|
297
336
|
@ApiTags('${routePath}')
|
|
298
337
|
@ApiBearerAuth()
|
|
@@ -316,7 +355,7 @@ function bareExternalController(base, routePath) {
|
|
|
316
355
|
const n = names(base);
|
|
317
356
|
return `import { Controller, Get } from '@nestjs/common';
|
|
318
357
|
import { ApiSecurity, ApiTags } from '@nestjs/swagger';
|
|
319
|
-
import { ApiKeyService } from '
|
|
358
|
+
import { ApiKeyService } from '${pkg()}';
|
|
320
359
|
|
|
321
360
|
@ApiTags('external-${routePath} (Api key)')
|
|
322
361
|
@ApiSecurity('api-key')
|
|
@@ -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}.
|
|
@@ -381,7 +420,7 @@ function processor(base, { external = false } = {}) {
|
|
|
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
|
-
return `import { CommonDataProcessor } from '
|
|
423
|
+
return `import { CommonDataProcessor } from '${pkg()}';
|
|
385
424
|
import { ${n.dtoClass} } from '${up}/dtos/add.${n.dot}.data.dto';
|
|
386
425
|
|
|
387
426
|
export class ${cls} extends CommonDataProcessor {
|
|
@@ -418,7 +457,7 @@ function listProcessor(base, tableName, { external = false } = {}) {
|
|
|
418
457
|
ListFilterService,
|
|
419
458
|
SortDirection,
|
|
420
459
|
UserEntity,
|
|
421
|
-
} from '
|
|
460
|
+
} from '${pkg()}';
|
|
422
461
|
|
|
423
462
|
/**
|
|
424
463
|
* Lists / filters ${n.pascal}.
|
|
@@ -474,8 +513,13 @@ export class ${cls} extends CommonListProcessor {
|
|
|
474
513
|
}
|
|
475
514
|
|
|
476
515
|
protected applyCustomPredicates() {
|
|
477
|
-
|
|
478
|
-
|
|
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
|
+
}
|
|
479
523
|
}
|
|
480
524
|
}
|
|
481
525
|
`;
|
|
@@ -483,7 +527,7 @@ export class ${cls} extends CommonListProcessor {
|
|
|
483
527
|
|
|
484
528
|
function subscriber(base) {
|
|
485
529
|
const n = names(base);
|
|
486
|
-
return `import { CommonSubscriber } from '
|
|
530
|
+
return `import { CommonSubscriber } from '${pkg()}';
|
|
487
531
|
import { EventSubscriber } from 'typeorm';
|
|
488
532
|
import { ${n.entityClass} } from '../entities/${n.dot}.entity';
|
|
489
533
|
import { DataSource } from 'typeorm';
|
|
@@ -508,7 +552,7 @@ export class ${n.subscriberClass} extends CommonSubscriber<${n.entityClass}> {
|
|
|
508
552
|
|
|
509
553
|
function job(base) {
|
|
510
554
|
const n = names(base);
|
|
511
|
-
return `import { CommonJob, QueueService, DatabaseEventDto } from '
|
|
555
|
+
return `import { CommonJob, QueueService, DatabaseEventDto } from '${pkg()}';
|
|
512
556
|
import { Injectable } from '@nestjs/common';
|
|
513
557
|
import { JobConstants } from '../../constants/job.constants';
|
|
514
558
|
import { ${n.entityClass} } from '../entities/${n.dot}.entity';
|
|
@@ -534,7 +578,7 @@ export class ${n.jobClass} extends CommonJob {
|
|
|
534
578
|
function genMigration(base, tableName, timestamp) {
|
|
535
579
|
const n = names(base);
|
|
536
580
|
const className = migrationClassName(n.migrationName, timestamp);
|
|
537
|
-
return `import { MigrationUtility } from '
|
|
581
|
+
return `import { MigrationUtility } from '${pkg()}';
|
|
538
582
|
|
|
539
583
|
export class ${className} extends MigrationUtility {
|
|
540
584
|
constructor() {
|
|
@@ -545,8 +589,11 @@ export class ${className} extends MigrationUtility {
|
|
|
545
589
|
process() {
|
|
546
590
|
this.rootPrimary();
|
|
547
591
|
|
|
548
|
-
|
|
549
|
-
|
|
592
|
+
${
|
|
593
|
+
current().tenantScoped
|
|
594
|
+
? ` this.foreign({ name: 'tenant_id', foreignTable: 'sys_tenants' });\n`
|
|
595
|
+
: ''
|
|
596
|
+
} this.foreign({ name: 'created_by', foreignTable: 'sys_users' });
|
|
550
597
|
this.foreign({ name: 'updated_by', foreignTable: 'sys_users' });
|
|
551
598
|
|
|
552
599
|
// TODO: add columns matching ${n.entityClass}.
|
|
@@ -554,8 +601,7 @@ export class ${className} extends MigrationUtility {
|
|
|
554
601
|
this.json('attributes');
|
|
555
602
|
this.timestamps();
|
|
556
603
|
|
|
557
|
-
this.index('tenant_id');
|
|
558
|
-
this.index('created_by');
|
|
604
|
+
${current().tenantScoped ? ` this.index('tenant_id');\n` : ''} this.index('created_by');
|
|
559
605
|
this.index('updated_by');
|
|
560
606
|
}
|
|
561
607
|
}
|
|
@@ -565,7 +611,7 @@ export class ${className} extends MigrationUtility {
|
|
|
565
611
|
/** Standalone migration (`nh migration`). */
|
|
566
612
|
function migration(migrationName, tableName, timestamp) {
|
|
567
613
|
const className = migrationClassName(migrationName, timestamp);
|
|
568
|
-
return `import { MigrationUtility } from '
|
|
614
|
+
return `import { MigrationUtility } from '${pkg()}';
|
|
569
615
|
|
|
570
616
|
export class ${className} extends MigrationUtility {
|
|
571
617
|
constructor() {
|
|
@@ -585,7 +631,7 @@ export class ${className} extends MigrationUtility {
|
|
|
585
631
|
/** Seed file (`nh seed`). */
|
|
586
632
|
function seed(seedName, tableName, timestamp) {
|
|
587
633
|
const className = `${toPascalCase(seedName)}${timestamp}`;
|
|
588
|
-
return `import { SeederUtility } from '
|
|
634
|
+
return `import { SeederUtility } from '${pkg()}';
|
|
589
635
|
|
|
590
636
|
export class ${className} extends SeederUtility {
|
|
591
637
|
constructor() {
|
|
@@ -604,7 +650,7 @@ export class ${className} extends SeederUtility {
|
|
|
604
650
|
/** Fresh src/constants/job.constants.ts (first entry included). */
|
|
605
651
|
function jobConstantsFile(base) {
|
|
606
652
|
const n = names(base);
|
|
607
|
-
return `import { baseJobConstants } from '
|
|
653
|
+
return `import { baseJobConstants } from '${pkg()}';
|
|
608
654
|
|
|
609
655
|
export const JobConstants = {
|
|
610
656
|
...baseJobConstants,
|
|
@@ -616,7 +662,7 @@ export const JobConstants = {
|
|
|
616
662
|
/** Fresh src/constants/entity.constants.ts (first entry included). */
|
|
617
663
|
function entityConstantsFile(base) {
|
|
618
664
|
const n = names(base);
|
|
619
|
-
return `import { baseEntityConstants } from '
|
|
665
|
+
return `import { baseEntityConstants } from '${pkg()}';
|
|
620
666
|
|
|
621
667
|
export const EntityConstants = {
|
|
622
668
|
...baseEntityConstants,
|
|
@@ -628,7 +674,7 @@ export const EntityConstants = {
|
|
|
628
674
|
/** Fresh src/constants/job.maps.ts (first entry included). */
|
|
629
675
|
function jobMapsFile(base, moduleDot, constants) {
|
|
630
676
|
const n = names(base);
|
|
631
|
-
return `import { baseJobMaps } from '
|
|
677
|
+
return `import { baseJobMaps } from '${pkg()}';
|
|
632
678
|
import { ${n.jobClass} } from '../${moduleDot}/jobs/${n.dot}.job';
|
|
633
679
|
import { ${constants.exportName} } from '${constants.importPath}';
|
|
634
680
|
|
|
@@ -642,7 +688,7 @@ export const JobMaps = {
|
|
|
642
688
|
/** Fresh src/constants/entity.maps.ts (first entry included). */
|
|
643
689
|
function entityMapsFile(base, moduleDot, constants) {
|
|
644
690
|
const n = names(base);
|
|
645
|
-
return `import { baseEntityMaps } from '
|
|
691
|
+
return `import { baseEntityMaps } from '${pkg()}';
|
|
646
692
|
import { ${n.entityClass} } from '../${moduleDot}/entities/${n.dot}.entity';
|
|
647
693
|
import { ${constants.exportName} } from '${constants.importPath}';
|
|
648
694
|
|
|
@@ -674,7 +720,7 @@ import {
|
|
|
674
720
|
QueueModule,
|
|
675
721
|
AuthModule,
|
|
676
722
|
SystemPropertyModule,
|
|
677
|
-
} from '
|
|
723
|
+
} from '${pkg()}';
|
|
678
724
|
import { es6Classes } from './es6.classes';
|
|
679
725
|
|
|
680
726
|
@Module({
|