@neohm/nh-cli 1.1.5 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +179 -18
- package/bin/nh.js +5 -0
- package/bin/nhx.js +11 -0
- package/package.json +8 -5
- package/src/commands/check.js +7 -2
- package/src/commands/gen.flag.js +357 -0
- package/src/commands/gen.js +41 -79
- package/src/commands/migration.js +2 -0
- package/src/commands/seed.js +2 -0
- package/src/index.js +67 -4
- package/src/lib/collision.js +82 -0
- package/src/lib/flavour.js +106 -0
- package/src/lib/format.js +54 -0
- package/src/lib/registry.js +120 -0
- package/src/lib/sync.engine.js +34 -7
- package/src/lib/templates.js +373 -61
package/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
|
-
`nh` generates fully wired modules, entities, controllers, DTOs, data
|
|
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
|
|
|
@@ -36,7 +60,8 @@ The project root is auto-detected by walking up from the current directory for a
|
|
|
36
60
|
|
|
37
61
|
| Command | Description |
|
|
38
62
|
| --------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
39
|
-
| `nh gen` | Interactive wizard: scaffold a module + entity + optional controller/DTO/processor/migration, then auto-wire it. |
|
|
63
|
+
| `nh gen` | Interactive wizard: scaffold a module + entity + optional controller/service/DTO/processor/migration, then auto-wire it. |
|
|
64
|
+
| `nh gen <flag>` | Generate one artifact instead of a whole module — see [Flag generation](#flag-generation). |
|
|
40
65
|
| `nh migration` | Generate a standalone migration `<timestamp>M-<Name>.ts`. |
|
|
41
66
|
| `nh seed` | Generate a seed file `<timestamp>S-<Name>.ts`. |
|
|
42
67
|
| `nh sync` | Rebuild every `es6.classes.ts` barrel and register modules in `app.module.ts`. |
|
|
@@ -45,6 +70,9 @@ The project root is auto-detected by walking up from the current directory for a
|
|
|
45
70
|
| `nh remove <name>` | Safely remove a scaffolded module and de-register it (migrations are preserved). |
|
|
46
71
|
| `nh --help` / `nh -v` | Help and version. |
|
|
47
72
|
|
|
73
|
+
Read `nhx` for `nh` throughout the table above when generating against
|
|
74
|
+
`@neohm/nestendx`.
|
|
75
|
+
|
|
48
76
|
### `nh gen`
|
|
49
77
|
|
|
50
78
|
```text
|
|
@@ -66,11 +94,14 @@ shape/
|
|
|
66
94
|
shape.module.ts # ShapeModule (imports CommonModule, QueueModule, ...)
|
|
67
95
|
es6.classes.ts # barrel of controllers/services/jobs/subscribers
|
|
68
96
|
controllers/shape.controller.ts
|
|
97
|
+
services/shape.service.ts
|
|
69
98
|
dtos/add.shape.data.dto.ts
|
|
70
99
|
entities/shape.entity.ts
|
|
71
100
|
libraries/shape.data.processor.ts
|
|
72
101
|
libraries/shape.list.processor.ts
|
|
73
|
-
enums/ jobs/
|
|
102
|
+
enums/ jobs/ subscribers/
|
|
103
|
+
external/ # optional API-key surface (nh gen -x ...)
|
|
104
|
+
controllers/ libraries/ services/
|
|
74
105
|
```
|
|
75
106
|
|
|
76
107
|
plus `src/database/migrations/<timestamp>M-AddShapeTable.ts`, and it
|
|
@@ -89,14 +120,143 @@ created spreading `baseJobConstants` / `baseEntityConstants` /
|
|
|
89
120
|
|
|
90
121
|
Generated artifacts extend the `@neohm/nestend` base classes:
|
|
91
122
|
|
|
92
|
-
- Entity → `CommonEntity` (with
|
|
123
|
+
- Entity → `CommonEntity` (a `tenant_id` column with its `TenantEntity`
|
|
124
|
+
relation, plus an empty `attributes` jsonb column)
|
|
93
125
|
- DTO → `CommonDataDto`
|
|
94
|
-
- Data processor → `CommonDataProcessor` (`process()` → `validate()` → `set()`)
|
|
126
|
+
- Data processor → `CommonDataProcessor` (`process()` → `validate()` → `set()`),
|
|
127
|
+
reached through the service's `upsert()`
|
|
95
128
|
- 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.
|
|
96
129
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
130
|
+
### Controller → service → processor
|
|
131
|
+
|
|
132
|
+
Controllers hold no business logic. They authenticate with
|
|
133
|
+
`AuthService.validateAccess()`, then delegate to the module's service — so the
|
|
134
|
+
same operation is reusable from a job, a subscriber or another service, and the
|
|
135
|
+
HTTP surface stays readable.
|
|
136
|
+
|
|
137
|
+
The service owns single-record reads and hands write and listing work to the
|
|
138
|
+
processors. It is **not** a prompt — it is always generated, prefilled from the
|
|
139
|
+
answers you already gave:
|
|
140
|
+
|
|
141
|
+
| Service method | Generated when | Controller endpoint |
|
|
142
|
+
| --- | --- | --- |
|
|
143
|
+
| `get(user, id)` | always | `GET /api/v1/shape/:id` |
|
|
144
|
+
| `upsert(data)` | data processor: Yes | `POST /api/v1/shape` |
|
|
145
|
+
| `list(user, data)` | listing processor: Yes | `POST /api/v1/shape/list` |
|
|
146
|
+
|
|
147
|
+
`get()` is the one operation that needs no processor, so a module that declines
|
|
148
|
+
both still gets a useful service with a single read method (and a controller
|
|
149
|
+
with just the `GET`). Declining a processor drops the matching service method
|
|
150
|
+
*and* its endpoint together, so the generated module always compiles.
|
|
151
|
+
|
|
152
|
+
`upsert()` rather than `create()` because `CommonDataDto` carries an optional
|
|
153
|
+
`id`: the same payload shape inserts when it is absent and updates when it is
|
|
154
|
+
present, and the data processor decides which. That is also why the endpoint is
|
|
155
|
+
a bare `@Post()` on the collection root rather than a `create` segment — one
|
|
156
|
+
route covers both halves.
|
|
157
|
+
|
|
158
|
+
`get()` scopes on the caller's tenant as part of the lookup —
|
|
159
|
+
`where: { id, tenant_id: user.organization_id }` — so a row belonging to
|
|
160
|
+
another tenant reads as *not found* rather than *forbidden*. (`organization_id`
|
|
161
|
+
is what the tenant is called on `UserEntity`; `tenant_id` is what it is called
|
|
162
|
+
on the row — the same value on two sides of one relation.) The listing
|
|
163
|
+
processor applies the same scope as a private predicate.
|
|
164
|
+
|
|
165
|
+
The generated scope and role identifiers are `'add-scope'` / `'add-role'`
|
|
166
|
+
placeholders, flagged with a `TODO` on the controller — replace them with the
|
|
167
|
+
`sys_scopes` identifier and role each endpoint actually sits behind.
|
|
168
|
+
|
|
169
|
+
## Flag generation
|
|
170
|
+
|
|
171
|
+
Bare `nh gen` scaffolds a whole module. Once that module exists, most work is
|
|
172
|
+
adding *one* more file to it — which is what the target flags do. Each asks
|
|
173
|
+
only what it cannot infer, refuses to shadow a name already in the project,
|
|
174
|
+
registers the result in `es6.classes.ts`, and formats what it wrote.
|
|
175
|
+
|
|
176
|
+
| Command | Generates | Asks |
|
|
177
|
+
| --- | --- | --- |
|
|
178
|
+
| `nh gen -c` | `controllers/<name>.controller.ts` | module, name, route path |
|
|
179
|
+
| `nh gen -s` | `services/<name>.service.ts` | module, name |
|
|
180
|
+
| `nh gen -dp` | `libraries/<name>.data.processor.ts` + `dtos/add.<name>.data.dto.ts` | module, name |
|
|
181
|
+
| `nh gen -lp` | `libraries/<name>.list.processor.ts` | module, name, table name |
|
|
182
|
+
| `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 |
|
|
183
|
+
| `nh gen -j` | `jobs/<name>.job.ts` | module, name |
|
|
184
|
+
|
|
185
|
+
`-e` writes the same five files the wizard does, because an entity is not
|
|
186
|
+
useful alone: TypeORM drives its job through its subscriber, so an entity
|
|
187
|
+
without them is a table nothing reacts to. It registers in both sets of
|
|
188
|
+
registries as a result — `entity.constants.ts` / `entity.maps.ts` for the
|
|
189
|
+
entity, `job.constants.ts` / `job.maps.ts` for the job it brought along.
|
|
190
|
+
`-j` registers in the job pair only.
|
|
191
|
+
|
|
192
|
+
The two jobs are deliberately different shapes:
|
|
193
|
+
|
|
194
|
+
| | Driven by | `handle()` takes |
|
|
195
|
+
| --- | --- | --- |
|
|
196
|
+
| the job `-e` brings along | the entity's subscriber | `event: DatabaseEventDto<<Name>Entity>` |
|
|
197
|
+
| `nh gen -j` | an explicit `dispatch()` | `data: unknown` |
|
|
198
|
+
|
|
199
|
+
So reach for `-j` when you want a queue worker that is not tied to a table's
|
|
200
|
+
lifecycle.
|
|
201
|
+
|
|
202
|
+
Only one target flag per run; the module is created if it does not exist yet.
|
|
203
|
+
|
|
204
|
+
### `-x`: the external (API-key) surface
|
|
205
|
+
|
|
206
|
+
`-x` mirrors a target onto the module's API-key surface, under
|
|
207
|
+
`<module>/external/`:
|
|
208
|
+
|
|
209
|
+
| Command | Generates |
|
|
210
|
+
| --- | --- |
|
|
211
|
+
| `nh gen -x -c` | `external/controllers/external.<name>.controller.ts` |
|
|
212
|
+
| `nh gen -x -s` | `external/services/external.<name>.service.ts` |
|
|
213
|
+
| `nh gen -x -dp` | `external/libraries/external.<name>.data.processor.ts` |
|
|
214
|
+
| `nh gen -x -lp` | `external/libraries/external.<name>.list.processor.ts` |
|
|
215
|
+
|
|
216
|
+
External files take an `external.` filename prefix and an `External` class
|
|
217
|
+
prefix. An external controller differs from an internal one in three ways, all
|
|
218
|
+
load-bearing: it mounts at `api/e/v1/<path>`, it declares the key to swagger
|
|
219
|
+
with `@ApiSecurity('api-key')`, and it authenticates through `ApiKeyService`
|
|
220
|
+
rather than `AuthService`.
|
|
221
|
+
|
|
222
|
+
`-x` does not apply to `-e` or `-j`: a table and a queue worker have no
|
|
223
|
+
inward/outward face, so there is nothing to mirror. Both refuse the flag rather
|
|
224
|
+
than quietly ignoring it.
|
|
225
|
+
|
|
226
|
+
External providers register into the *same* `es6.classes.ts` categories as
|
|
227
|
+
their internal siblings, listed after them — so `nh sync` and `nh check` see
|
|
228
|
+
`<module>/external/<folder>` as well as `<module>/<folder>`.
|
|
229
|
+
|
|
230
|
+
### Name collisions
|
|
231
|
+
|
|
232
|
+
A flag generator checks the whole of `src/` for the name it is about to take —
|
|
233
|
+
both the exported class and the filename — and stops before writing if it is
|
|
234
|
+
taken. Every artifact is checked up front, so `-e` cannot write the entity and
|
|
235
|
+
then abort on the job:
|
|
236
|
+
|
|
237
|
+
```text
|
|
238
|
+
$ nh gen -j
|
|
239
|
+
? Module name (existing: billing, inventory, service): billing
|
|
240
|
+
? Job name (PascalCase): Cleanup
|
|
241
|
+
✗ a class named CleanupJob already exists in src/inventory/jobs/cleanup.job.ts.
|
|
242
|
+
Pick a different name, or edit that file instead — nothing was generated.
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The check is project-wide, not module-local, because the registries these
|
|
246
|
+
feed are project-wide: two `CleanupJob` classes cannot both be
|
|
247
|
+
`JobConstants.cleanupJob`, and two identically named providers in one Nest
|
|
248
|
+
container is a bug that surfaces far from its cause. Dependency files are
|
|
249
|
+
exempt — the payload DTO for `-dp`, the attributes DTO for `-e`, and the
|
|
250
|
+
timestamped migration are reused when present and reported as `skip`.
|
|
251
|
+
|
|
252
|
+
## Formatting
|
|
253
|
+
|
|
254
|
+
The boilerplate lints with `prettier/prettier: error`, so `nh gen` runs the
|
|
255
|
+
**target project's** prettier (honouring its `.prettierrc`) over everything it
|
|
256
|
+
writes before finishing. Templates already aim to emit prettier-clean output;
|
|
257
|
+
this keeps that true for entity names long enough to push a line past 80
|
|
258
|
+
columns. A project without prettier installed still gets its files, just
|
|
259
|
+
unformatted.
|
|
100
260
|
|
|
101
261
|
- Migration → `MigrationUtility`
|
|
102
262
|
- Seed → `SeederUtility`
|
|
@@ -104,11 +264,12 @@ ships with a `POST list` endpoint that runs the listing processor (with
|
|
|
104
264
|
Re-running `gen` for an existing module reuses it and only adds new files —
|
|
105
265
|
existing files are never overwritten.
|
|
106
266
|
|
|
107
|
-
|
|
267
|
+
## `nh sync` and `nh check`
|
|
108
268
|
|
|
109
|
-
`sync` scans every module under `src/`
|
|
110
|
-
barrel from the files actually present in
|
|
111
|
-
|
|
269
|
+
`sync` scans every module under `src/` — including its `external/` mirror —
|
|
270
|
+
and regenerates its `es6.classes.ts` barrel from the files actually present in
|
|
271
|
+
`controllers/`, `services/`, `libraries/`, `dtos/`, `jobs/` and
|
|
272
|
+
`subscribers/`, then ensures each module is imported and listed in
|
|
112
273
|
`app.module.ts`. Files are classified by their **folder** and the presence of an
|
|
113
274
|
exported class — not by a fragile filename suffix — so non-standard names are
|
|
114
275
|
never silently dropped. The operation is idempotent.
|
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
|
|