@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 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
- Every generated artifact matches the conventions of the `nestjs-boilerplate`
12
- reference project:
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/ services/ subscribers/
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 an empty `attributes` jsonb column)
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
- When both a controller and a listing processor are generated, the controller
98
- ships with a `POST list` endpoint that runs the listing processor (with
99
- `SqlService` injected via the constructor).
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
- ### `nh sync` and `nh check`
267
+ ## `nh sync` and `nh check`
108
268
 
109
- `sync` scans every module under `src/` and regenerates its `es6.classes.ts`
110
- barrel from the files actually present in `controllers/`, `services/`, `jobs/`
111
- and `subscribers/`, then ensures each module is imported and listed in
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.1.5",
4
- "description": "nh — Command Line Interface for neohm. NestJS projects",
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",
@@ -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(chalk.yellow(`\n${changes} file(s) would change. Run \`nh sync\` to apply.`));
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('In sync — `nh sync` would make no changes.');
57
+ log.done(`In sync — \`${cli()} sync\` would make no changes.`);
53
58
  }
54
59
  }
55
60