@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 +41 -20
- package/package.json +1 -1
- package/src/commands/gen.js +114 -4
- package/src/commands/migration.js +1 -1
- package/src/commands/seed.js +1 -1
- package/src/lib/constants.engine.js +155 -0
- package/src/lib/naming.js +6 -6
- package/src/lib/paths.js +1 -0
- package/src/lib/templates.js +151 -7
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
|
-
`
|
|
16
|
-
|
|
17
|
-
|
|
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.
|
|
52
|
-
? Entity name (e.g.
|
|
53
|
-
? Table name (e.g.
|
|
54
|
-
? Create controller (
|
|
55
|
-
? Create DTO (
|
|
56
|
-
? Create data processor (
|
|
57
|
-
? Create
|
|
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/
|
|
62
|
+
Produces, under `src/shape/`:
|
|
61
63
|
|
|
62
64
|
```text
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
shape/
|
|
66
|
+
shape.module.ts # ShapeModule (imports CommonModule, QueueModule, ...)
|
|
65
67
|
es6.classes.ts # barrel of controllers/services/jobs/subscribers
|
|
66
|
-
controllers/
|
|
67
|
-
dtos/add.
|
|
68
|
-
|
|
69
|
-
|
|
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-
|
|
74
|
-
registers `
|
|
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/
|
|
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
package/src/commands/gen.js
CHANGED
|
@@ -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
|
-
|
|
100
|
-
|
|
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.
|
|
24
|
+
message: "Migration name (e.g. AddShapeTable):",
|
|
25
25
|
validate: (v) => (v && v.trim() ? true : "Migration name is required"),
|
|
26
26
|
},
|
|
27
27
|
]);
|
package/src/commands/seed.js
CHANGED
|
@@ -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.
|
|
6
|
-
* (e.g.
|
|
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
|
-
/**
|
|
22
|
+
/** round.shape */
|
|
23
23
|
function toDotCase(input) {
|
|
24
24
|
return toWords(input).join('.');
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
}
|
package/src/lib/templates.js
CHANGED
|
@@ -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. "
|
|
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
|
-
|
|
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.
|
|
103
|
-
this.
|
|
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 {
|
|
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(
|
|
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
|
};
|