@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 +39 -20
- package/package.json +1 -1
- package/src/commands/gen.js +107 -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 +123 -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,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.
|
|
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
|
-
entities/
|
|
69
|
-
libraries/
|
|
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-
|
|
74
|
-
registers `
|
|
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/
|
|
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
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,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.
|
|
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,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
|
-
|
|
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.
|
|
103
|
-
this.
|
|
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 {
|
|
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(
|
|
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
|
};
|