@neohm/nh-cli 1.0.1 → 1.1.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 CHANGED
@@ -12,9 +12,10 @@ Every generated artifact matches the conventions of the `nestjs-boilerplate`
12
12
  reference project:
13
13
 
14
14
  - **Filenames** are `dot.separated.lowercase` (no hyphens) — e.g.
15
- `business.client.entity.ts`, `add.business.client.data.dto.ts`.
16
- - **Class names** are `PascalCase` — e.g. `BusinessClientEntity`,
17
- `AddBusinessClientDataDto`.
15
+ `shape.entity.ts`, and a multi-word `RoundShape` becomes
16
+ `round.shape.entity.ts`.
17
+ - **Class names** are `PascalCase` — e.g. `ShapeEntity`,
18
+ `AddShapeDataDto`.
18
19
 
19
20
  ## Installation
20
21
 
@@ -48,36 +49,54 @@ The project root is auto-detected by walking up from the current directory for a
48
49
 
49
50
  ```text
50
51
  $ nh gen
51
- ? Module name (e.g. business): business
52
- ? Entity name (e.g. BusinessClient): BusinessClient
53
- ? Table name (e.g. nh_user_details): nh_user_details
54
- ? Create controller (BusinessClientController)? Yes
55
- ? Create DTO (AddBusinessClientDataDto)? Yes
56
- ? Create data processor (BusinessClientDataProcessor)? Yes
57
- ? Create migration (M-AddBusinessClientTable)? Yes
52
+ ? Module name (e.g. shape): shape
53
+ ? Entity name (e.g. Shape): Shape
54
+ ? Table name (e.g. nh_shapes): nh_shapes
55
+ ? Create controller (ShapeController)? Yes
56
+ ? Create DTO (AddShapeDataDto)? Yes
57
+ ? Create data processor (ShapeDataProcessor)? Yes
58
+ ? Create listing processor (ShapeListProcessor)? Yes
59
+ ? Create migration (M-AddShapeTable)? Yes
58
60
  ```
59
61
 
60
- Produces, under `src/business/`:
62
+ Produces, under `src/shape/`:
61
63
 
62
64
  ```text
63
- business/
64
- business.module.ts # BusinessModule (imports CommonModule, QueueModule, ...)
65
+ shape/
66
+ shape.module.ts # ShapeModule (imports CommonModule, QueueModule, ...)
65
67
  es6.classes.ts # barrel of controllers/services/jobs/subscribers
66
- controllers/business.client.controller.ts
67
- dtos/add.business.client.data.dto.ts
68
- entities/business.client.entity.ts
69
- libraries/business.client.data.processor.ts
68
+ controllers/shape.controller.ts
69
+ dtos/add.shape.data.dto.ts
70
+ entities/shape.entity.ts
71
+ libraries/shape.data.processor.ts
72
+ libraries/shape.list.processor.ts
70
73
  enums/ jobs/ services/ subscribers/
71
74
  ```
72
75
 
73
- plus `src/database/migrations/<timestamp>M-AddBusinessClientTable.ts`, and it
74
- registers `BusinessModule` in `src/app.module.ts`.
76
+ plus `src/database/migrations/<timestamp>M-AddShapeTable.ts`, and it
77
+ registers `ShapeModule` in `src/app.module.ts`.
78
+
79
+ It also registers the entity and its job in the project constant registries —
80
+ `shapeJob: 'shape.job'` in `src/constants/job.constants.ts` (which the
81
+ generated job references as `JobConstants.shapeJob`) and `shape: 'shape'` in
82
+ `src/constants/entity.constants.ts` — and mirrors them in the
83
+ identifier → class map registries: `[JobConstants.shapeJob]: ShapeJob` in
84
+ `src/constants/job.maps.ts` and `[EntityConstants.shape]: ShapeEntity` in
85
+ `src/constants/entity.maps.ts` (imports included). Missing registry files are
86
+ created spreading `baseJobConstants` / `baseEntityConstants` /
87
+ `baseJobMaps` / `baseEntityMaps` from `@neohm/nestend`; existing ones
88
+ (whatever their export is named) just get the entry appended.
75
89
 
76
90
  Generated artifacts extend the `@neohm/nestend` base classes:
77
91
 
78
92
  - Entity → `CommonEntity` (with an empty `attributes` jsonb column)
79
93
  - DTO → `CommonPayloadDto`
80
94
  - Data processor → `CommonDataProcessor` (`process()` → `validate()` → `set()`)
95
+ - 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
+
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).
81
100
  - Migration → `MigrationUtility`
82
101
  - Seed → `SeederUtility`
83
102
 
@@ -98,7 +117,7 @@ never silently dropped. The operation is idempotent.
98
117
  ```text
99
118
  $ nh check
100
119
  es6.classes barrels:
101
- ok src/business/es6.classes.ts
120
+ ok src/shape/es6.classes.ts
102
121
  update src/test/es6.classes.ts
103
122
  + services: TestService
104
123
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neohm/nh-cli",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "nh — Command Line Interface for neohm. NestJS projects",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -10,6 +10,12 @@ const naming = require("../lib/naming");
10
10
  const tpl = require("../lib/templates");
11
11
  const { writeFile, report, log, ensureDir } = require("../lib/fs.utils");
12
12
  const sync = require("../lib/sync.engine");
13
+ const {
14
+ ensureConstantEntry,
15
+ ensureMapEntry,
16
+ detectExportName,
17
+ entityConstantsPath,
18
+ } = require("../lib/constants.engine");
13
19
 
14
20
  async function gen() {
15
21
  const paths = projectPaths();
@@ -86,6 +92,77 @@ async function gen() {
86
92
  const subscriberPath = path.join(moduleDir, "subscribers", n.subscriberFile);
87
93
  report(writeFile(subscriberPath, tpl.subscriber(entityName)), subscriberPath, paths.root);
88
94
 
95
+ // Register the entity + job identifiers in the project constant registries
96
+ // (the generated job references JobConstants.<name>Job, so the entry must exist).
97
+ const jobConstantsPath = path.join(paths.constantsDir, "job.constants.ts");
98
+ const jobEntry = ensureConstantEntry(
99
+ jobConstantsPath,
100
+ tpl.jobConstantsFile(entityName),
101
+ `${n.camel}Job`,
102
+ `${n.dot}.job`,
103
+ );
104
+ if (jobEntry === "failed") {
105
+ log.warn(`Could not update ${path.relative(paths.root, jobConstantsPath)} — add ${n.camel}Job: '${n.dot}.job' manually.`);
106
+ } else {
107
+ report(jobEntry, jobConstantsPath, paths.root);
108
+ }
109
+
110
+ const entityConstants = entityConstantsPath(paths.constantsDir);
111
+ const entityEntry = ensureConstantEntry(
112
+ entityConstants,
113
+ tpl.entityConstantsFile(entityName),
114
+ n.camel,
115
+ n.dot,
116
+ );
117
+ if (entityEntry === "failed") {
118
+ log.warn(`Could not update ${path.relative(paths.root, entityConstants)} — add ${n.camel}: '${n.dot}' manually.`);
119
+ } else {
120
+ report(entityEntry, entityConstants, paths.root);
121
+ }
122
+
123
+ // ...and mirror them in the identifier → class map registries.
124
+ const jobConstantsRef = {
125
+ exportName: detectExportName(jobConstantsPath) || "JobConstants",
126
+ importPath: "./job.constants",
127
+ };
128
+ const jobMapsPath = path.join(paths.constantsDir, "job.maps.ts");
129
+ const jobMapEntry = ensureMapEntry(
130
+ jobMapsPath,
131
+ tpl.jobMapsFile(entityName, moduleDot, jobConstantsRef),
132
+ `[${jobConstantsRef.exportName}.${n.camel}Job]`,
133
+ n.jobClass,
134
+ [
135
+ { className: n.jobClass, importPath: `../${moduleDot}/jobs/${n.dot}.job` },
136
+ { className: jobConstantsRef.exportName, importPath: jobConstantsRef.importPath },
137
+ ],
138
+ );
139
+ if (jobMapEntry === "failed") {
140
+ log.warn(`Could not update ${path.relative(paths.root, jobMapsPath)} — map ${n.jobClass} manually.`);
141
+ } else {
142
+ report(jobMapEntry, jobMapsPath, paths.root);
143
+ }
144
+
145
+ const entityConstantsRef = {
146
+ exportName: detectExportName(entityConstants) || "EntityConstants",
147
+ importPath: `./${path.basename(entityConstants, ".ts")}`,
148
+ };
149
+ const entityMapsPath = path.join(paths.constantsDir, "entity.maps.ts");
150
+ const entityMapEntry = ensureMapEntry(
151
+ entityMapsPath,
152
+ tpl.entityMapsFile(entityName, moduleDot, entityConstantsRef),
153
+ `[${entityConstantsRef.exportName}.${n.camel}]`,
154
+ n.entityClass,
155
+ [
156
+ { className: n.entityClass, importPath: `../${moduleDot}/entities/${n.dot}.entity` },
157
+ { className: entityConstantsRef.exportName, importPath: entityConstantsRef.importPath },
158
+ ],
159
+ );
160
+ if (entityMapEntry === "failed") {
161
+ log.warn(`Could not update ${path.relative(paths.root, entityMapsPath)} — map ${n.entityClass} manually.`);
162
+ } else {
163
+ report(entityMapEntry, entityMapsPath, paths.root);
164
+ }
165
+
89
166
  // 4. Optional: controller -------------------------------------------------
90
167
  const { controller } = await inquirer.prompt([
91
168
  {
@@ -96,10 +173,8 @@ async function gen() {
96
173
  },
97
174
  ]);
98
175
 
99
- if (controller) {
100
- const p = path.join(moduleDir, "controllers", n.controllerFile);
101
- report(writeFile(p, tpl.controller(entityName)), p, paths.root);
102
- }
176
+ // (written after the listing-processor prompt — the answer decides whether
177
+ // the controller ships with a POST list endpoint)
103
178
 
104
179
  // 5. Optional: data processor (includes Add*DataDto) ----------------------
105
180
  const { processor } = await inquirer.prompt([
@@ -119,6 +194,34 @@ async function gen() {
119
194
  report(writeFile(procPath, tpl.processor(entityName)), procPath, paths.root);
120
195
  }
121
196
 
197
+ // 6. Optional: listing processor (takes CommonListFilterDto directly) ------
198
+ const { listProcessor } = await inquirer.prompt([
199
+ {
200
+ type: "confirm",
201
+ name: "listProcessor",
202
+ message: `Create listing processor (${n.listProcessorClass})?`,
203
+ default: true,
204
+ },
205
+ ]);
206
+
207
+ if (listProcessor) {
208
+ const listProcPath = path.join(moduleDir, "libraries", n.listProcessorFile);
209
+ report(
210
+ writeFile(listProcPath, tpl.listProcessor(entityName, tableName)),
211
+ listProcPath,
212
+ paths.root,
213
+ );
214
+ }
215
+
216
+ if (controller) {
217
+ const p = path.join(moduleDir, "controllers", n.controllerFile);
218
+ report(
219
+ writeFile(p, tpl.controller(entityName, { listProcessor })),
220
+ p,
221
+ paths.root,
222
+ );
223
+ }
224
+
122
225
  // Auto-wire: rebuild this module's barrel and register it in app.module.ts.
123
226
  log.info("\nWiring imports...");
124
227
  const mod = sync.scanModule(
@@ -21,7 +21,7 @@ async function migration() {
21
21
  {
22
22
  type: "input",
23
23
  name: "migrationName",
24
- message: "Migration name (e.g. AddBusinessClientTable):",
24
+ message: "Migration name (e.g. AddShapeTable):",
25
25
  validate: (v) => (v && v.trim() ? true : "Migration name is required"),
26
26
  },
27
27
  ]);
@@ -21,7 +21,7 @@ async function seed() {
21
21
  {
22
22
  type: "input",
23
23
  name: "seedName",
24
- message: "Seed name (e.g. SeedBusinessClients):",
24
+ message: "Seed name (e.g. SeedShapes):",
25
25
  validate: (v) => (v && v.trim() ? true : "Seed name is required"),
26
26
  },
27
27
  ]);
@@ -0,0 +1,155 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const { writeFile } = require('./fs.utils');
7
+
8
+ /**
9
+ * Keeps the project-level constant registries (src/constants/job.constants.ts
10
+ * and src/constants/entity.constants.ts) in step with `nh gen`: every
11
+ * generated job references JobConstants.<camel>Job, so the entry has to exist
12
+ * for the project to compile. Missing files are created spreading the
13
+ * corresponding base object from @neohm/nestend; existing files get the new
14
+ * entry appended to their exported object (whatever it is named — older
15
+ * projects use e.g. `entityMap`).
16
+ */
17
+
18
+ /**
19
+ * Ensure `key: 'value',` exists inside the object exported by `filePath`.
20
+ * Creates the file with `freshContent` when absent. Returns a writeFile-style
21
+ * result: 'created' | 'skipped' | 'overwritten' | 'failed'.
22
+ */
23
+ function ensureConstantEntry(filePath, freshContent, key, value) {
24
+ if (!fs.existsSync(filePath)) {
25
+ return writeFile(filePath, freshContent);
26
+ }
27
+
28
+ const content = fs.readFileSync(filePath, 'utf8');
29
+ if (new RegExp(`(^|[^\\w])${escapeRe(key)}\\s*:`).test(content)) {
30
+ return 'skipped';
31
+ }
32
+
33
+ // First exported object literal in the file, e.g. `export const JobConstants = {`.
34
+ const open = content.search(/export\s+const\s+\w+\s*(?::[^=]+)?=\s*{/);
35
+ if (open === -1) return 'failed';
36
+ const braceStart = content.indexOf('{', open);
37
+
38
+ // Walk to the matching closing brace (entries are flat string values, but
39
+ // stay correct if a nested object ever appears).
40
+ let depth = 0;
41
+ let close = -1;
42
+ for (let i = braceStart; i < content.length; i++) {
43
+ if (content[i] === '{') depth++;
44
+ else if (content[i] === '}') {
45
+ depth--;
46
+ if (depth === 0) {
47
+ close = i;
48
+ break;
49
+ }
50
+ }
51
+ }
52
+ if (close === -1) return 'failed';
53
+
54
+ const before = content.slice(0, close).replace(/\s*$/, '');
55
+ const entry = `${before.endsWith(',') || before.endsWith('{') ? '' : ','}\n ${key}: '${value}',\n`;
56
+ const next = before + entry + content.slice(close);
57
+
58
+ fs.writeFileSync(filePath, next);
59
+ return 'overwritten';
60
+ }
61
+
62
+ /**
63
+ * Ensure a map entry (`[Constants.key]: ClassName,`) plus the import lines it
64
+ * needs exist in `filePath`. Creates the file with `freshContent` when absent.
65
+ * `imports` is a list of { className, importPath }. Returns 'created' |
66
+ * 'skipped' | 'overwritten' | 'failed'.
67
+ */
68
+ function ensureMapEntry(filePath, freshContent, entryKey, entryValue, imports) {
69
+ if (!fs.existsSync(filePath)) {
70
+ return writeFile(filePath, freshContent);
71
+ }
72
+
73
+ let content = fs.readFileSync(filePath, 'utf8');
74
+ if (content.includes(entryKey)) return 'skipped';
75
+
76
+ const open = content.search(/export\s+const\s+\w+\s*(?::[^=]+)?=\s*{/);
77
+ if (open === -1) return 'failed';
78
+
79
+ for (const imp of imports) {
80
+ content = ensureImportLine(content, imp.className, imp.importPath);
81
+ }
82
+
83
+ // Re-locate the object after import insertion shifted offsets.
84
+ const braceStart = content.indexOf(
85
+ '{',
86
+ content.search(/export\s+const\s+\w+\s*(?::[^=]+)?=\s*{/),
87
+ );
88
+ let depth = 0;
89
+ let close = -1;
90
+ for (let i = braceStart; i < content.length; i++) {
91
+ if (content[i] === '{') depth++;
92
+ else if (content[i] === '}') {
93
+ depth--;
94
+ if (depth === 0) {
95
+ close = i;
96
+ break;
97
+ }
98
+ }
99
+ }
100
+ if (close === -1) return 'failed';
101
+
102
+ const before = content.slice(0, close).replace(/\s*$/, '');
103
+ const entry = `${before.endsWith(',') || before.endsWith('{') ? '' : ','}\n ${entryKey}: ${entryValue},\n`;
104
+ fs.writeFileSync(filePath, before + entry + content.slice(close));
105
+ return 'overwritten';
106
+ }
107
+
108
+ function ensureImportLine(content, className, importPath) {
109
+ if (new RegExp(`from\\s+['"]${escapeRe(importPath)}['"]`).test(content)) {
110
+ return content;
111
+ }
112
+ const importLine = `import { ${className} } from '${importPath}';`;
113
+ const lines = content.split('\n');
114
+ let lastImport = -1;
115
+ for (let i = 0; i < lines.length; i++) {
116
+ if (/^import\b/.test(lines[i]) || /^\s*}\s+from\s+/.test(lines[i])) {
117
+ lastImport = i;
118
+ }
119
+ }
120
+ if (lastImport === -1) return importLine + '\n' + content;
121
+ lines.splice(lastImport + 1, 0, importLine);
122
+ return lines.join('\n');
123
+ }
124
+
125
+ /** Name of the first `export const X = {...}` in the file (null if unreadable). */
126
+ function detectExportName(filePath) {
127
+ if (!fs.existsSync(filePath)) return null;
128
+ const match = fs
129
+ .readFileSync(filePath, 'utf8')
130
+ .match(/export\s+const\s+(\w+)\s*(?::[^=]+)?=\s*{/);
131
+ return match ? match[1] : null;
132
+ }
133
+
134
+ /**
135
+ * Resolve the entity constants file, tolerating the legacy misspelled
136
+ * `enity.constants.ts` some projects carry — an existing registry is updated
137
+ * rather than shadowed by a second file.
138
+ */
139
+ function entityConstantsPath(constantsDir) {
140
+ const legacy = path.join(constantsDir, 'enity.constants.ts');
141
+ const canonical = path.join(constantsDir, 'entity.constants.ts');
142
+ if (!fs.existsSync(canonical) && fs.existsSync(legacy)) return legacy;
143
+ return canonical;
144
+ }
145
+
146
+ function escapeRe(str) {
147
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
148
+ }
149
+
150
+ module.exports = {
151
+ ensureConstantEntry,
152
+ ensureMapEntry,
153
+ detectExportName,
154
+ entityConstantsPath,
155
+ };
package/src/lib/naming.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  /**
4
4
  * Naming helpers. Every generated filename follows the dot.separated.lowercase
5
- * convention (e.g. business.client.entity.ts) and every class name is PascalCase
6
- * (e.g. BusinessClientEntity). These helpers are the single source of truth for
5
+ * convention (e.g. round.shape.entity.ts) and every class name is PascalCase
6
+ * (e.g. RoundShapeEntity). These helpers are the single source of truth for
7
7
  * turning a raw user input (PascalCase, kebab-case, snake_case, spaced, or
8
8
  * dotted) into the canonical word list both conventions are derived from.
9
9
  */
@@ -19,25 +19,25 @@ function toWords(input) {
19
19
  .filter(Boolean);
20
20
  }
21
21
 
22
- /** business.client */
22
+ /** round.shape */
23
23
  function toDotCase(input) {
24
24
  return toWords(input).join('.');
25
25
  }
26
26
 
27
- /** BusinessClient */
27
+ /** RoundShape */
28
28
  function toPascalCase(input) {
29
29
  return toWords(input)
30
30
  .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
31
31
  .join('');
32
32
  }
33
33
 
34
- /** businessClient */
34
+ /** roundShape */
35
35
  function toCamelCase(input) {
36
36
  const pascal = toPascalCase(input);
37
37
  return pascal.charAt(0).toLowerCase() + pascal.slice(1);
38
38
  }
39
39
 
40
- /** business-client (used for HTTP route segments / swagger tags) */
40
+ /** round-shape (used for HTTP route segments / swagger tags) */
41
41
  function toKebabCase(input) {
42
42
  return toWords(input).join('-');
43
43
  }
package/src/lib/paths.js CHANGED
@@ -40,6 +40,7 @@ function projectPaths(root = requireProjectRoot()) {
40
40
  srcDir,
41
41
  appModule: path.join(srcDir, 'app.module.ts'),
42
42
  migrationsDir: path.join(srcDir, 'database', 'migrations'),
43
+ constantsDir: path.join(srcDir, 'constants'),
43
44
  moduleDir: (moduleDot) => path.join(srcDir, moduleDot),
44
45
  };
45
46
  }
@@ -10,7 +10,7 @@ const { toDotCase, toPascalCase, toKebabCase, toCamelCase } = require('./naming'
10
10
  * across nestjs-boilerplate.
11
11
  */
12
12
 
13
- /** Build the canonical names for a given entity-ish base (e.g. "BusinessClient"). */
13
+ /** Build the canonical names for a given entity-ish base (e.g. "Shape"). */
14
14
  function names(base) {
15
15
  const dot = toDotCase(base);
16
16
  const pascal = toPascalCase(base);
@@ -30,6 +30,8 @@ function names(base) {
30
30
  dtoFile: `add.${dot}.data.dto.ts`,
31
31
  processorClass: `${pascal}DataProcessor`,
32
32
  processorFile: `${dot}.data.processor.ts`,
33
+ listProcessorClass: `${pascal}ListProcessor`,
34
+ listProcessorFile: `${dot}.list.processor.ts`,
33
35
  subscriberClass: `${pascal}Subscriber`,
34
36
  subscriberFile: `${dot}.subscriber.ts`,
35
37
  jobClass: `${pascal}Job`,
@@ -59,9 +61,10 @@ function attributesDto(base) {
59
61
  `;
60
62
  }
61
63
 
62
- function controller(base) {
64
+ function controller(base, { listProcessor = false } = {}) {
63
65
  const n = names(base);
64
- return `import { Controller } from '@nestjs/common';
66
+ if (!listProcessor) {
67
+ return `import { Controller } from '@nestjs/common';
65
68
  import { ApiTags } from '@nestjs/swagger';
66
69
 
67
70
  @ApiTags('${n.kebab}')
@@ -69,6 +72,23 @@ import { ApiTags } from '@nestjs/swagger';
69
72
  export class ${n.controllerClass} {
70
73
  constructor() {}
71
74
  }
75
+ `;
76
+ }
77
+ return `import { Body, Controller, Post } from '@nestjs/common';
78
+ import { ApiTags } from '@nestjs/swagger';
79
+ import { CommonListFilterDto, SqlService } from '@neohm/nestend';
80
+ import { ${n.listProcessorClass} } from '../libraries/${n.dot}.list.processor';
81
+
82
+ @ApiTags('${n.kebab}')
83
+ @Controller('api/v1/${n.kebab}')
84
+ export class ${n.controllerClass} {
85
+ constructor(private readonly sqlService: SqlService) {}
86
+
87
+ @Post('list')
88
+ async list(@Body() body: CommonListFilterDto) {
89
+ return new ${n.listProcessorClass}(this.sqlService).process(body);
90
+ }
91
+ }
72
92
  `;
73
93
  }
74
94
 
@@ -99,8 +119,8 @@ export class ${n.processorClass} extends CommonDataProcessor {
99
119
  }
100
120
 
101
121
  private async validate() {
102
- // TODO: collect validation errors via this.addColumnError(...).
103
- this.throwPresentErrors();
122
+ // TODO: collect validation errors via this.addErrors({ column: 'message' }).
123
+ this.throwExceptionOnError();
104
124
  }
105
125
 
106
126
  private async set() {
@@ -110,6 +130,45 @@ export class ${n.processorClass} extends CommonDataProcessor {
110
130
  `;
111
131
  }
112
132
 
133
+ function listProcessor(base, tableName) {
134
+ const n = names(base);
135
+ return `import {
136
+ CommonListFilterConfig,
137
+ CommonListFilterDto,
138
+ CommonListProcessor,
139
+ SortDirection,
140
+ SqlService,
141
+ } from '@neohm/nestend';
142
+
143
+ /**
144
+ * Lists / filters ${n.pascal}. The \`columns\` config below is the allowlist:
145
+ * an identifier must appear here before a caller can filter or sort on it.
146
+ * Mark a column \`searchable: true\` to include it in the free-text \`str\` sweep.
147
+ */
148
+ export class ${n.listProcessorClass} extends CommonListProcessor {
149
+ protected config: CommonListFilterConfig = {
150
+ query: '${tableName} a',
151
+ columns: [
152
+ { key: 'a.id', identifier: 'id' },
153
+ { key: 'a.attributes', identifier: 'attributes' },
154
+ { key: 'a.created_at', identifier: 'created_at' },
155
+ ],
156
+ defaultSort: { field: 'id', direction: SortDirection.DESC },
157
+ };
158
+
159
+ constructor(protected readonly sqlService: SqlService) {
160
+ super();
161
+ }
162
+
163
+ async process(payload: CommonListFilterDto) {
164
+ this.payload = payload;
165
+
166
+ return this.handle();
167
+ }
168
+ }
169
+ `;
170
+ }
171
+
113
172
  function subscriber(base) {
114
173
  const n = names(base);
115
174
  return `import { CommonSubscriber } from '@neohm/nestend';
@@ -139,12 +198,12 @@ function job(base) {
139
198
  const n = names(base);
140
199
  return `import { CommonJob, QueueService } from '@neohm/nestend';
141
200
  import { Injectable } from '@nestjs/common';
142
- import { JobMap } from '../../constants/job.constants';
201
+ import { JobConstants } from '../../constants/job.constants';
143
202
 
144
203
  @Injectable()
145
204
  export class ${n.jobClass} extends CommonJob {
146
205
  constructor(protected readonly queueService: QueueService) {
147
- super(JobMap.${n.camel}Job);
206
+ super(JobConstants.${n.camel}Job);
148
207
  }
149
208
 
150
209
  async handle(event: any): Promise<void> {
@@ -220,6 +279,58 @@ export class ${className} extends SeederUtility {
220
279
  `;
221
280
  }
222
281
 
282
+ /** Fresh src/constants/job.constants.ts (first entry included). */
283
+ function jobConstantsFile(base) {
284
+ const n = names(base);
285
+ return `import { baseJobConstants } from '@neohm/nestend';
286
+
287
+ export const JobConstants = {
288
+ ...baseJobConstants,
289
+ ${n.camel}Job: '${n.dot}.job',
290
+ };
291
+ `;
292
+ }
293
+
294
+ /** Fresh src/constants/entity.constants.ts (first entry included). */
295
+ function entityConstantsFile(base) {
296
+ const n = names(base);
297
+ return `import { baseEntityConstants } from '@neohm/nestend';
298
+
299
+ export const EntityConstants = {
300
+ ...baseEntityConstants,
301
+ ${n.camel}: '${n.dot}',
302
+ };
303
+ `;
304
+ }
305
+
306
+ /** Fresh src/constants/job.maps.ts (first entry included). */
307
+ function jobMapsFile(base, moduleDot, constants) {
308
+ const n = names(base);
309
+ return `import { baseJobMaps } from '@neohm/nestend';
310
+ import { ${n.jobClass} } from '../${moduleDot}/jobs/${n.dot}.job';
311
+ import { ${constants.exportName} } from '${constants.importPath}';
312
+
313
+ export const JobMaps = {
314
+ ...baseJobMaps,
315
+ [${constants.exportName}.${n.camel}Job]: ${n.jobClass},
316
+ };
317
+ `;
318
+ }
319
+
320
+ /** Fresh src/constants/entity.maps.ts (first entry included). */
321
+ function entityMapsFile(base, moduleDot, constants) {
322
+ const n = names(base);
323
+ return `import { baseEntityMaps } from '@neohm/nestend';
324
+ import { ${n.entityClass} } from '../${moduleDot}/entities/${n.dot}.entity';
325
+ import { ${constants.exportName} } from '${constants.importPath}';
326
+
327
+ export const EntityMaps = {
328
+ ...baseEntityMaps,
329
+ [${constants.exportName}.${n.camel}]: ${n.entityClass},
330
+ };
331
+ `;
332
+ }
333
+
223
334
  /** A freshly scaffolded module before any es6.classes content exists. */
224
335
  function emptyEs6Classes() {
225
336
  return `export const es6Classes = {
@@ -265,11 +376,16 @@ module.exports = {
265
376
  controller,
266
377
  dto,
267
378
  processor,
379
+ listProcessor,
268
380
  subscriber,
269
381
  job,
270
382
  genMigration,
271
383
  migration,
272
384
  seed,
385
+ jobConstantsFile,
386
+ entityConstantsFile,
387
+ jobMapsFile,
388
+ entityMapsFile,
273
389
  emptyEs6Classes,
274
390
  moduleFile,
275
391
  };