@neohm/nh-cli 1.0.1 → 1.1.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
@@ -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,56 @@ 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
+ dtos/shape.list.filter.dto.ts
71
+ entities/shape.entity.ts
72
+ libraries/shape.data.processor.ts
73
+ libraries/shape.list.processor.ts
70
74
  enums/ jobs/ services/ subscribers/
71
75
  ```
72
76
 
73
- plus `src/database/migrations/<timestamp>M-AddBusinessClientTable.ts`, and it
74
- registers `BusinessModule` in `src/app.module.ts`.
77
+ plus `src/database/migrations/<timestamp>M-AddShapeTable.ts`, and it
78
+ registers `ShapeModule` in `src/app.module.ts`.
79
+
80
+ It also registers the entity and its job in the project constant registries —
81
+ `shapeJob: 'shape.job'` in `src/constants/job.constants.ts` (which the
82
+ generated job references as `JobConstants.shapeJob`) and `shape: 'shape'` in
83
+ `src/constants/entity.constants.ts` — and mirrors them in the
84
+ identifier → class map registries: `[JobConstants.shapeJob]: ShapeJob` in
85
+ `src/constants/job.maps.ts` and `[EntityConstants.shape]: ShapeEntity` in
86
+ `src/constants/entity.maps.ts` (imports included). Missing registry files are
87
+ created spreading `baseJobConstants` / `baseEntityConstants` /
88
+ `baseJobMaps` / `baseEntityMaps` from `@neohm/nestend`; existing ones
89
+ (whatever their export is named) just get the entry appended.
75
90
 
76
91
  Generated artifacts extend the `@neohm/nestend` base classes:
77
92
 
78
93
  - Entity → `CommonEntity` (with an empty `attributes` jsonb column)
79
94
  - DTO → `CommonPayloadDto`
80
95
  - Data processor → `CommonDataProcessor` (`process()` → `validate()` → `set()`)
96
+ - List filter DTO → `CommonListFilterDto` (plus a `custom_filter` example field)
97
+ - Listing processor → `CommonListProcessor` (column allowlist config, `id` default sort, and an example custom filter)
98
+
99
+ When both a controller and a listing processor are generated, the controller
100
+ ships with a `POST list` endpoint that runs the listing processor (with
101
+ `SqlService` injected via the constructor).
81
102
  - Migration → `MigrationUtility`
82
103
  - Seed → `SeederUtility`
83
104
 
@@ -98,7 +119,7 @@ never silently dropped. The operation is idempotent.
98
119
  ```text
99
120
  $ nh check
100
121
  es6.classes barrels:
101
- ok src/business/es6.classes.ts
122
+ ok src/shape/es6.classes.ts
102
123
  update src/test/es6.classes.ts
103
124
  + services: TestService
104
125
 
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.0",
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,41 @@ async function gen() {
119
194
  report(writeFile(procPath, tpl.processor(entityName)), procPath, paths.root);
120
195
  }
121
196
 
197
+ // 6. Optional: listing processor (includes *ListFilterDto) -----------------
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 listDtoPath = path.join(moduleDir, "dtos", n.listFilterDtoFile);
209
+ report(
210
+ writeFile(listDtoPath, tpl.listFilterDto(entityName)),
211
+ listDtoPath,
212
+ paths.root,
213
+ );
214
+
215
+ const listProcPath = path.join(moduleDir, "libraries", n.listProcessorFile);
216
+ report(
217
+ writeFile(listProcPath, tpl.listProcessor(entityName, tableName)),
218
+ listProcPath,
219
+ paths.root,
220
+ );
221
+ }
222
+
223
+ if (controller) {
224
+ const p = path.join(moduleDir, "controllers", n.controllerFile);
225
+ report(
226
+ writeFile(p, tpl.controller(entityName, { listProcessor })),
227
+ p,
228
+ paths.root,
229
+ );
230
+ }
231
+
122
232
  // Auto-wire: rebuild this module's barrel and register it in app.module.ts.
123
233
  log.info("\nWiring imports...");
124
234
  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,10 @@ function names(base) {
30
30
  dtoFile: `add.${dot}.data.dto.ts`,
31
31
  processorClass: `${pascal}DataProcessor`,
32
32
  processorFile: `${dot}.data.processor.ts`,
33
+ listFilterDtoClass: `${pascal}ListFilterDto`,
34
+ listFilterDtoFile: `${dot}.list.filter.dto.ts`,
35
+ listProcessorClass: `${pascal}ListProcessor`,
36
+ listProcessorFile: `${dot}.list.processor.ts`,
33
37
  subscriberClass: `${pascal}Subscriber`,
34
38
  subscriberFile: `${dot}.subscriber.ts`,
35
39
  jobClass: `${pascal}Job`,
@@ -59,9 +63,10 @@ function attributesDto(base) {
59
63
  `;
60
64
  }
61
65
 
62
- function controller(base) {
66
+ function controller(base, { listProcessor = false } = {}) {
63
67
  const n = names(base);
64
- return `import { Controller } from '@nestjs/common';
68
+ if (!listProcessor) {
69
+ return `import { Controller } from '@nestjs/common';
65
70
  import { ApiTags } from '@nestjs/swagger';
66
71
 
67
72
  @ApiTags('${n.kebab}')
@@ -69,6 +74,24 @@ import { ApiTags } from '@nestjs/swagger';
69
74
  export class ${n.controllerClass} {
70
75
  constructor() {}
71
76
  }
77
+ `;
78
+ }
79
+ return `import { Body, Controller, Post } from '@nestjs/common';
80
+ import { ApiTags } from '@nestjs/swagger';
81
+ import { SqlService } from '@neohm/nestend';
82
+ import { ${n.listFilterDtoClass} } from '../dtos/${n.dot}.list.filter.dto';
83
+ import { ${n.listProcessorClass} } from '../libraries/${n.dot}.list.processor';
84
+
85
+ @ApiTags('${n.kebab}')
86
+ @Controller('api/v1/${n.kebab}')
87
+ export class ${n.controllerClass} {
88
+ constructor(private readonly sqlService: SqlService) {}
89
+
90
+ @Post('list')
91
+ async list(@Body() body: ${n.listFilterDtoClass}) {
92
+ return new ${n.listProcessorClass}(this.sqlService).process(body);
93
+ }
94
+ }
72
95
  `;
73
96
  }
74
97
 
@@ -99,8 +122,8 @@ export class ${n.processorClass} extends CommonDataProcessor {
99
122
  }
100
123
 
101
124
  private async validate() {
102
- // TODO: collect validation errors via this.addColumnError(...).
103
- this.throwPresentErrors();
125
+ // TODO: collect validation errors via this.addErrors({ column: 'message' }).
126
+ this.throwExceptionOnError();
104
127
  }
105
128
 
106
129
  private async set() {
@@ -110,6 +133,69 @@ export class ${n.processorClass} extends CommonDataProcessor {
110
133
  `;
111
134
  }
112
135
 
136
+ function listFilterDto(base) {
137
+ const n = names(base);
138
+ return `import { CommonListFilterDto } from '@neohm/nestend';
139
+ import { Expose } from 'class-transformer';
140
+ import { IsOptional, IsString } from 'class-validator';
141
+
142
+ export class ${n.listFilterDtoClass} extends CommonListFilterDto {
143
+ @Expose()
144
+ @IsOptional()
145
+ @IsString()
146
+ custom_filter?: string;
147
+ }
148
+ `;
149
+ }
150
+
151
+ function listProcessor(base, tableName) {
152
+ const n = names(base);
153
+ return `import {
154
+ CommonListFilterConfig,
155
+ CommonListProcessor,
156
+ SortDirection,
157
+ SqlService,
158
+ } from '@neohm/nestend';
159
+ import { ${n.listFilterDtoClass} } from '../dtos/${n.dot}.list.filter.dto';
160
+
161
+ export class ${n.listProcessorClass} extends CommonListProcessor {
162
+ protected config: CommonListFilterConfig = {
163
+ query: '${tableName} a',
164
+ columns: [
165
+ { key: 'a.id', identifier: 'id' },
166
+ { key: 'a.attributes', identifier: 'attributes' },
167
+ { key: 'a.created_at', identifier: 'created_at' },
168
+ ],
169
+ defaultSort: { field: 'id', direction: SortDirection.DESC },
170
+ };
171
+
172
+ constructor(protected readonly sqlService: SqlService) {
173
+ super();
174
+ }
175
+
176
+ async process(payload: ${n.listFilterDtoClass}) {
177
+ this.payload = payload;
178
+
179
+ this.applyCustomFilters(payload);
180
+
181
+ return this.handle();
182
+ }
183
+
184
+ private async applyCustomFilters(payload: ${n.listFilterDtoClass}) {
185
+ this.applyCustomFilter(payload.custom_filter);
186
+ }
187
+
188
+ private async applyCustomFilter(custom_filter: string) {
189
+ if (!custom_filter) return;
190
+
191
+ // Example of a hand-rolled condition beyond the config-driven filters.
192
+ // TODO: replace test_column with a real filterable column of ${n.pascal}.
193
+ this.conditions.push(\`test_column ilike '%\${custom_filter}%'\`);
194
+ }
195
+ }
196
+ `;
197
+ }
198
+
113
199
  function subscriber(base) {
114
200
  const n = names(base);
115
201
  return `import { CommonSubscriber } from '@neohm/nestend';
@@ -139,12 +225,12 @@ function job(base) {
139
225
  const n = names(base);
140
226
  return `import { CommonJob, QueueService } from '@neohm/nestend';
141
227
  import { Injectable } from '@nestjs/common';
142
- import { JobMap } from '../../constants/job.constants';
228
+ import { JobConstants } from '../../constants/job.constants';
143
229
 
144
230
  @Injectable()
145
231
  export class ${n.jobClass} extends CommonJob {
146
232
  constructor(protected readonly queueService: QueueService) {
147
- super(JobMap.${n.camel}Job);
233
+ super(JobConstants.${n.camel}Job);
148
234
  }
149
235
 
150
236
  async handle(event: any): Promise<void> {
@@ -220,6 +306,58 @@ export class ${className} extends SeederUtility {
220
306
  `;
221
307
  }
222
308
 
309
+ /** Fresh src/constants/job.constants.ts (first entry included). */
310
+ function jobConstantsFile(base) {
311
+ const n = names(base);
312
+ return `import { baseJobConstants } from '@neohm/nestend';
313
+
314
+ export const JobConstants = {
315
+ ...baseJobConstants,
316
+ ${n.camel}Job: '${n.dot}.job',
317
+ };
318
+ `;
319
+ }
320
+
321
+ /** Fresh src/constants/entity.constants.ts (first entry included). */
322
+ function entityConstantsFile(base) {
323
+ const n = names(base);
324
+ return `import { baseEntityConstants } from '@neohm/nestend';
325
+
326
+ export const EntityConstants = {
327
+ ...baseEntityConstants,
328
+ ${n.camel}: '${n.dot}',
329
+ };
330
+ `;
331
+ }
332
+
333
+ /** Fresh src/constants/job.maps.ts (first entry included). */
334
+ function jobMapsFile(base, moduleDot, constants) {
335
+ const n = names(base);
336
+ return `import { baseJobMaps } from '@neohm/nestend';
337
+ import { ${n.jobClass} } from '../${moduleDot}/jobs/${n.dot}.job';
338
+ import { ${constants.exportName} } from '${constants.importPath}';
339
+
340
+ export const JobMaps = {
341
+ ...baseJobMaps,
342
+ [${constants.exportName}.${n.camel}Job]: ${n.jobClass},
343
+ };
344
+ `;
345
+ }
346
+
347
+ /** Fresh src/constants/entity.maps.ts (first entry included). */
348
+ function entityMapsFile(base, moduleDot, constants) {
349
+ const n = names(base);
350
+ return `import { baseEntityMaps } from '@neohm/nestend';
351
+ import { ${n.entityClass} } from '../${moduleDot}/entities/${n.dot}.entity';
352
+ import { ${constants.exportName} } from '${constants.importPath}';
353
+
354
+ export const EntityMaps = {
355
+ ...baseEntityMaps,
356
+ [${constants.exportName}.${n.camel}]: ${n.entityClass},
357
+ };
358
+ `;
359
+ }
360
+
223
361
  /** A freshly scaffolded module before any es6.classes content exists. */
224
362
  function emptyEs6Classes() {
225
363
  return `export const es6Classes = {
@@ -265,11 +403,17 @@ module.exports = {
265
403
  controller,
266
404
  dto,
267
405
  processor,
406
+ listFilterDto,
407
+ listProcessor,
268
408
  subscriber,
269
409
  job,
270
410
  genMigration,
271
411
  migration,
272
412
  seed,
413
+ jobConstantsFile,
414
+ entityConstantsFile,
415
+ jobMapsFile,
416
+ entityMapsFile,
273
417
  emptyEs6Classes,
274
418
  moduleFile,
275
419
  };