@neohm/nh-cli 1.0.0 → 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 +182 -63
- package/src/commands/migration.js +20 -16
- package/src/commands/seed.js +15 -15
- 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/sync.engine.js +2 -0
- package/src/lib/templates.js +218 -8
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
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const fs = require(
|
|
4
|
-
const path = require(
|
|
5
|
-
const inquirer = require(
|
|
6
|
-
const chalk = require(
|
|
7
|
-
|
|
8
|
-
const { projectPaths, MODULE_SUBFOLDERS } = require(
|
|
9
|
-
const naming = require(
|
|
10
|
-
const tpl = require(
|
|
11
|
-
const { writeFile, report, log, ensureDir } = require(
|
|
12
|
-
const sync = require(
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const inquirer = require("inquirer");
|
|
6
|
+
const chalk = require("chalk");
|
|
7
|
+
|
|
8
|
+
const { projectPaths, MODULE_SUBFOLDERS } = require("../lib/paths");
|
|
9
|
+
const naming = require("../lib/naming");
|
|
10
|
+
const tpl = require("../lib/templates");
|
|
11
|
+
const { writeFile, report, log, ensureDir } = require("../lib/fs.utils");
|
|
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();
|
|
@@ -18,10 +24,10 @@ async function gen() {
|
|
|
18
24
|
// 1. Module ---------------------------------------------------------------
|
|
19
25
|
const { moduleNameRaw } = await inquirer.prompt([
|
|
20
26
|
{
|
|
21
|
-
type:
|
|
22
|
-
name:
|
|
23
|
-
message:
|
|
24
|
-
validate: (v) => (v && v.trim() ? true :
|
|
27
|
+
type: "input",
|
|
28
|
+
name: "moduleNameRaw",
|
|
29
|
+
message: "Module name:",
|
|
30
|
+
validate: (v) => (v && v.trim() ? true : "Module name is required"),
|
|
25
31
|
},
|
|
26
32
|
]);
|
|
27
33
|
const moduleDot = naming.toDotCase(moduleNameRaw);
|
|
@@ -44,20 +50,16 @@ async function gen() {
|
|
|
44
50
|
path.join(moduleDir, `${moduleDot}.module.ts`),
|
|
45
51
|
paths.root,
|
|
46
52
|
);
|
|
47
|
-
const es6Path = path.join(moduleDir,
|
|
48
|
-
report(
|
|
49
|
-
writeFile(es6Path, tpl.emptyEs6Classes()),
|
|
50
|
-
es6Path,
|
|
51
|
-
paths.root,
|
|
52
|
-
);
|
|
53
|
+
const es6Path = path.join(moduleDir, "es6.classes.ts");
|
|
54
|
+
report(writeFile(es6Path, tpl.emptyEs6Classes()), es6Path, paths.root);
|
|
53
55
|
|
|
54
56
|
// 2. Entity ---------------------------------------------------------------
|
|
55
57
|
const { entityName } = await inquirer.prompt([
|
|
56
58
|
{
|
|
57
|
-
type:
|
|
58
|
-
name:
|
|
59
|
-
message:
|
|
60
|
-
validate: (v) => (v && v.trim() ? true :
|
|
59
|
+
type: "input",
|
|
60
|
+
name: "entityName",
|
|
61
|
+
message: "Entity name (PascalCase):",
|
|
62
|
+
validate: (v) => (v && v.trim() ? true : "Entity name is required"),
|
|
61
63
|
},
|
|
62
64
|
]);
|
|
63
65
|
const n = tpl.names(entityName);
|
|
@@ -65,66 +67,183 @@ async function gen() {
|
|
|
65
67
|
// 3. Table ----------------------------------------------------------------
|
|
66
68
|
const { tableName } = await inquirer.prompt([
|
|
67
69
|
{
|
|
68
|
-
type:
|
|
69
|
-
name:
|
|
70
|
-
message:
|
|
71
|
-
default: `nh_${naming.toWords(entityName).join(
|
|
72
|
-
validate: (v) => (v && v.trim() ? true :
|
|
70
|
+
type: "input",
|
|
71
|
+
name: "tableName",
|
|
72
|
+
message: "Table name:",
|
|
73
|
+
default: `nh_${naming.toWords(entityName).join("_")}s`,
|
|
74
|
+
validate: (v) => (v && v.trim() ? true : "Table name is required"),
|
|
73
75
|
},
|
|
74
76
|
]);
|
|
75
77
|
|
|
76
|
-
|
|
78
|
+
// Always generate: attributes DTO, entity, subscriber, job
|
|
79
|
+
const attrDtoPath = path.join(moduleDir, "dtos", n.attributesDtoFile);
|
|
80
|
+
report(writeFile(attrDtoPath, tpl.attributesDto(entityName)), attrDtoPath, paths.root);
|
|
81
|
+
|
|
82
|
+
const entityPath = path.join(moduleDir, "entities", n.entityFile);
|
|
77
83
|
report(
|
|
78
84
|
writeFile(entityPath, tpl.entity(entityName, tableName)),
|
|
79
85
|
entityPath,
|
|
80
86
|
paths.root,
|
|
81
87
|
);
|
|
82
88
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
{ type: 'confirm', name: 'migration', message: `Create migration (M-Add${n.pascal}Table)?`, default: true },
|
|
89
|
-
]);
|
|
89
|
+
const jobPath = path.join(moduleDir, "jobs", n.jobFile);
|
|
90
|
+
report(writeFile(jobPath, tpl.job(entityName)), jobPath, paths.root);
|
|
91
|
+
|
|
92
|
+
const subscriberPath = path.join(moduleDir, "subscribers", n.subscriberFile);
|
|
93
|
+
report(writeFile(subscriberPath, tpl.subscriber(entityName)), subscriberPath, paths.root);
|
|
90
94
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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);
|
|
94
108
|
}
|
|
95
109
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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);
|
|
104
121
|
}
|
|
105
122
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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);
|
|
109
143
|
}
|
|
110
144
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
|
|
166
|
+
// 4. Optional: controller -------------------------------------------------
|
|
167
|
+
const { controller } = await inquirer.prompt([
|
|
168
|
+
{
|
|
169
|
+
type: "confirm",
|
|
170
|
+
name: "controller",
|
|
171
|
+
message: `Create controller (${n.controllerClass})?`,
|
|
172
|
+
default: true,
|
|
173
|
+
},
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
// (written after the listing-processor prompt — the answer decides whether
|
|
177
|
+
// the controller ships with a POST list endpoint)
|
|
178
|
+
|
|
179
|
+
// 5. Optional: data processor (includes Add*DataDto) ----------------------
|
|
180
|
+
const { processor } = await inquirer.prompt([
|
|
181
|
+
{
|
|
182
|
+
type: "confirm",
|
|
183
|
+
name: "processor",
|
|
184
|
+
message: `Create data processor (${n.processorClass})?`,
|
|
185
|
+
default: true,
|
|
186
|
+
},
|
|
187
|
+
]);
|
|
188
|
+
|
|
189
|
+
if (processor) {
|
|
190
|
+
const dtoPath = path.join(moduleDir, "dtos", n.dtoFile);
|
|
191
|
+
report(writeFile(dtoPath, tpl.dto(entityName)), dtoPath, paths.root);
|
|
192
|
+
|
|
193
|
+
const procPath = path.join(moduleDir, "libraries", n.processorFile);
|
|
194
|
+
report(writeFile(procPath, tpl.processor(entityName)), procPath, paths.root);
|
|
195
|
+
}
|
|
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
|
+
);
|
|
116
230
|
}
|
|
117
231
|
|
|
118
232
|
// Auto-wire: rebuild this module's barrel and register it in app.module.ts.
|
|
119
|
-
log.info(
|
|
120
|
-
const mod = sync.scanModule(
|
|
233
|
+
log.info("\nWiring imports...");
|
|
234
|
+
const mod = sync.scanModule(
|
|
235
|
+
moduleDir,
|
|
236
|
+
path.join(moduleDir, `${moduleDot}.module.ts`),
|
|
237
|
+
);
|
|
121
238
|
const plan = sync.buildPlan(paths, [mod]);
|
|
122
239
|
const written = sync.applyPlan(plan);
|
|
123
|
-
for (const w of written) report(
|
|
124
|
-
if (!written.length) log.info(
|
|
240
|
+
for (const w of written) report("overwritten", w, paths.root);
|
|
241
|
+
if (!written.length) log.info(" (already in sync)");
|
|
125
242
|
|
|
126
243
|
log.done(`Module "${moduleDot}" scaffolded.`);
|
|
127
|
-
console.log(
|
|
244
|
+
console.log(
|
|
245
|
+
chalk.dim("Run `nh sync` anytime to re-wire imports after manual edits."),
|
|
246
|
+
);
|
|
128
247
|
}
|
|
129
248
|
|
|
130
249
|
module.exports = gen;
|
|
@@ -1,35 +1,39 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
const path = require(
|
|
4
|
-
const inquirer = require(
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const inquirer = require("inquirer");
|
|
5
5
|
|
|
6
|
-
const { projectPaths } = require(
|
|
7
|
-
const naming = require(
|
|
8
|
-
const tpl = require(
|
|
9
|
-
const { writeFile, report, log } = require(
|
|
6
|
+
const { projectPaths } = require("../lib/paths");
|
|
7
|
+
const naming = require("../lib/naming");
|
|
8
|
+
const tpl = require("../lib/templates");
|
|
9
|
+
const { writeFile, report, log } = require("../lib/fs.utils");
|
|
10
10
|
|
|
11
11
|
async function migration() {
|
|
12
12
|
const paths = projectPaths();
|
|
13
13
|
|
|
14
14
|
const { tableName, migrationName } = await inquirer.prompt([
|
|
15
15
|
{
|
|
16
|
-
type:
|
|
17
|
-
name:
|
|
18
|
-
message:
|
|
19
|
-
validate: (v) => (v && v.trim() ? true :
|
|
16
|
+
type: "input",
|
|
17
|
+
name: "tableName",
|
|
18
|
+
message: "Table name (e.g. nh_user_details):",
|
|
19
|
+
validate: (v) => (v && v.trim() ? true : "Table name is required"),
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
|
-
type:
|
|
23
|
-
name:
|
|
24
|
-
message:
|
|
25
|
-
validate: (v) => (v && v.trim() ? true :
|
|
22
|
+
type: "input",
|
|
23
|
+
name: "migrationName",
|
|
24
|
+
message: "Migration name (e.g. AddShapeTable):",
|
|
25
|
+
validate: (v) => (v && v.trim() ? true : "Migration name is required"),
|
|
26
26
|
},
|
|
27
27
|
]);
|
|
28
28
|
|
|
29
29
|
const ts = Date.now();
|
|
30
30
|
const file = `${ts}M-${naming.toPascalCase(migrationName)}.ts`;
|
|
31
31
|
const p = path.join(paths.migrationsDir, file);
|
|
32
|
-
report(
|
|
32
|
+
report(
|
|
33
|
+
writeFile(p, tpl.migration(migrationName, tableName, ts)),
|
|
34
|
+
p,
|
|
35
|
+
paths.root,
|
|
36
|
+
);
|
|
33
37
|
log.done(`Migration created (${file}).`);
|
|
34
38
|
}
|
|
35
39
|
|
package/src/commands/seed.js
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
const path = require(
|
|
4
|
-
const inquirer = require(
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const inquirer = require("inquirer");
|
|
5
5
|
|
|
6
|
-
const { projectPaths } = require(
|
|
7
|
-
const naming = require(
|
|
8
|
-
const tpl = require(
|
|
9
|
-
const { writeFile, report, log } = require(
|
|
6
|
+
const { projectPaths } = require("../lib/paths");
|
|
7
|
+
const naming = require("../lib/naming");
|
|
8
|
+
const tpl = require("../lib/templates");
|
|
9
|
+
const { writeFile, report, log } = require("../lib/fs.utils");
|
|
10
10
|
|
|
11
11
|
async function seed() {
|
|
12
12
|
const paths = projectPaths();
|
|
13
13
|
|
|
14
14
|
const { tableName, seedName } = await inquirer.prompt([
|
|
15
15
|
{
|
|
16
|
-
type:
|
|
17
|
-
name:
|
|
18
|
-
message:
|
|
19
|
-
validate: (v) => (v && v.trim() ? true :
|
|
16
|
+
type: "input",
|
|
17
|
+
name: "tableName",
|
|
18
|
+
message: "Table name (e.g. nh_user_details):",
|
|
19
|
+
validate: (v) => (v && v.trim() ? true : "Table name is required"),
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
|
-
type:
|
|
23
|
-
name:
|
|
24
|
-
message:
|
|
25
|
-
validate: (v) => (v && v.trim() ? true :
|
|
22
|
+
type: "input",
|
|
23
|
+
name: "seedName",
|
|
24
|
+
message: "Seed name (e.g. SeedShapes):",
|
|
25
|
+
validate: (v) => (v && v.trim() ? true : "Seed name is required"),
|
|
26
26
|
},
|
|
27
27
|
]);
|
|
28
28
|
|
|
@@ -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/sync.engine.js
CHANGED
|
@@ -18,6 +18,8 @@ const path = require('path');
|
|
|
18
18
|
const CATEGORIES = [
|
|
19
19
|
{ key: 'controllers', folder: 'controllers' },
|
|
20
20
|
{ key: 'services', folder: 'services' },
|
|
21
|
+
{ key: 'libraries', folder: 'libraries' },
|
|
22
|
+
{ key: 'dtos', folder: 'dtos' },
|
|
21
23
|
{ key: 'jobs', folder: 'jobs' },
|
|
22
24
|
{ key: 'subscribers', folder: 'subscribers' },
|
|
23
25
|
];
|
package/src/lib/templates.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { toDotCase, toPascalCase, toKebabCase } = require('./naming');
|
|
3
|
+
const { toDotCase, toPascalCase, toKebabCase, toCamelCase } = require('./naming');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Code templates. All generated code imports shared base classes from the
|
|
@@ -10,22 +10,34 @@ const { toDotCase, toPascalCase, toKebabCase } = 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);
|
|
17
|
+
const camel = toCamelCase(base);
|
|
17
18
|
return {
|
|
18
19
|
dot,
|
|
19
20
|
pascal,
|
|
21
|
+
camel,
|
|
20
22
|
kebab: toKebabCase(base),
|
|
21
23
|
entityClass: `${pascal}Entity`,
|
|
22
24
|
entityFile: `${dot}.entity.ts`,
|
|
25
|
+
attributesDtoClass: `${pascal}AttributesDto`,
|
|
26
|
+
attributesDtoFile: `${dot}.attributes.dto.ts`,
|
|
23
27
|
controllerClass: `${pascal}Controller`,
|
|
24
28
|
controllerFile: `${dot}.controller.ts`,
|
|
25
29
|
dtoClass: `Add${pascal}DataDto`,
|
|
26
30
|
dtoFile: `add.${dot}.data.dto.ts`,
|
|
27
31
|
processorClass: `${pascal}DataProcessor`,
|
|
28
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`,
|
|
37
|
+
subscriberClass: `${pascal}Subscriber`,
|
|
38
|
+
subscriberFile: `${dot}.subscriber.ts`,
|
|
39
|
+
jobClass: `${pascal}Job`,
|
|
40
|
+
jobFile: `${dot}.job.ts`,
|
|
29
41
|
};
|
|
30
42
|
}
|
|
31
43
|
|
|
@@ -33,19 +45,28 @@ function entity(base, tableName) {
|
|
|
33
45
|
const n = names(base);
|
|
34
46
|
return `import { Column, Entity } from 'typeorm';
|
|
35
47
|
import { CommonEntity } from '@neohm/nestend';
|
|
48
|
+
import { ${n.attributesDtoClass} } from '../dtos/${n.dot}.attributes.dto';
|
|
36
49
|
|
|
37
50
|
@Entity({ name: '${tableName}' })
|
|
38
51
|
export class ${n.entityClass} extends CommonEntity {
|
|
39
|
-
// TODO: declare columns for ${n.pascal}.
|
|
40
52
|
@Column({ type: 'jsonb' })
|
|
41
|
-
attributes:
|
|
53
|
+
attributes: ${n.attributesDtoClass};
|
|
42
54
|
}
|
|
43
55
|
`;
|
|
44
56
|
}
|
|
45
57
|
|
|
46
|
-
function
|
|
58
|
+
function attributesDto(base) {
|
|
47
59
|
const n = names(base);
|
|
48
|
-
return `
|
|
60
|
+
return `export class ${n.attributesDtoClass} {
|
|
61
|
+
// TODO: declare attribute fields for ${n.pascal}.
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function controller(base, { listProcessor = false } = {}) {
|
|
67
|
+
const n = names(base);
|
|
68
|
+
if (!listProcessor) {
|
|
69
|
+
return `import { Controller } from '@nestjs/common';
|
|
49
70
|
import { ApiTags } from '@nestjs/swagger';
|
|
50
71
|
|
|
51
72
|
@ApiTags('${n.kebab}')
|
|
@@ -53,6 +74,24 @@ import { ApiTags } from '@nestjs/swagger';
|
|
|
53
74
|
export class ${n.controllerClass} {
|
|
54
75
|
constructor() {}
|
|
55
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
|
+
}
|
|
56
95
|
`;
|
|
57
96
|
}
|
|
58
97
|
|
|
@@ -83,8 +122,8 @@ export class ${n.processorClass} extends CommonDataProcessor {
|
|
|
83
122
|
}
|
|
84
123
|
|
|
85
124
|
private async validate() {
|
|
86
|
-
// TODO: collect validation errors via this.
|
|
87
|
-
this.
|
|
125
|
+
// TODO: collect validation errors via this.addErrors({ column: 'message' }).
|
|
126
|
+
this.throwExceptionOnError();
|
|
88
127
|
}
|
|
89
128
|
|
|
90
129
|
private async set() {
|
|
@@ -94,6 +133,113 @@ export class ${n.processorClass} extends CommonDataProcessor {
|
|
|
94
133
|
`;
|
|
95
134
|
}
|
|
96
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
|
+
|
|
199
|
+
function subscriber(base) {
|
|
200
|
+
const n = names(base);
|
|
201
|
+
return `import { CommonSubscriber } from '@neohm/nestend';
|
|
202
|
+
import { EventSubscriber } from 'typeorm';
|
|
203
|
+
import { ${n.entityClass} } from '../entities/${n.dot}.entity';
|
|
204
|
+
import { DataSource } from 'typeorm';
|
|
205
|
+
import { ${n.jobClass} } from '../jobs/${n.dot}.job';
|
|
206
|
+
|
|
207
|
+
@EventSubscriber()
|
|
208
|
+
export class ${n.subscriberClass} extends CommonSubscriber<${n.entityClass}> {
|
|
209
|
+
constructor(
|
|
210
|
+
protected readonly datasource: DataSource,
|
|
211
|
+
protected readonly entityJob: ${n.jobClass},
|
|
212
|
+
) {
|
|
213
|
+
super();
|
|
214
|
+
datasource.subscribers.push(this);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
listenTo() {
|
|
218
|
+
return ${n.entityClass};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function job(base) {
|
|
225
|
+
const n = names(base);
|
|
226
|
+
return `import { CommonJob, QueueService } from '@neohm/nestend';
|
|
227
|
+
import { Injectable } from '@nestjs/common';
|
|
228
|
+
import { JobConstants } from '../../constants/job.constants';
|
|
229
|
+
|
|
230
|
+
@Injectable()
|
|
231
|
+
export class ${n.jobClass} extends CommonJob {
|
|
232
|
+
constructor(protected readonly queueService: QueueService) {
|
|
233
|
+
super(JobConstants.${n.camel}Job);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async handle(event: any): Promise<void> {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
`;
|
|
241
|
+
}
|
|
242
|
+
|
|
97
243
|
/**
|
|
98
244
|
* Migration generated as part of `nh gen`, stubbed with the columns the
|
|
99
245
|
* generated entity carries (rootPrimary + created_by FK from CommonEntity,
|
|
@@ -160,11 +306,65 @@ export class ${className} extends SeederUtility {
|
|
|
160
306
|
`;
|
|
161
307
|
}
|
|
162
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
|
+
|
|
163
361
|
/** A freshly scaffolded module before any es6.classes content exists. */
|
|
164
362
|
function emptyEs6Classes() {
|
|
165
363
|
return `export const es6Classes = {
|
|
166
364
|
controllers: [],
|
|
167
365
|
services: [],
|
|
366
|
+
libraries: [],
|
|
367
|
+
dtos: [],
|
|
168
368
|
jobs: [],
|
|
169
369
|
subscribers: [],
|
|
170
370
|
};
|
|
@@ -187,6 +387,7 @@ import { es6Classes } from './es6.classes';
|
|
|
187
387
|
controllers: [...es6Classes.controllers],
|
|
188
388
|
providers: [
|
|
189
389
|
...es6Classes.services,
|
|
390
|
+
...es6Classes.libraries,
|
|
190
391
|
...es6Classes.jobs,
|
|
191
392
|
...es6Classes.subscribers,
|
|
192
393
|
],
|
|
@@ -198,12 +399,21 @@ export class ${pascal}Module {}
|
|
|
198
399
|
module.exports = {
|
|
199
400
|
names,
|
|
200
401
|
entity,
|
|
402
|
+
attributesDto,
|
|
201
403
|
controller,
|
|
202
404
|
dto,
|
|
203
405
|
processor,
|
|
406
|
+
listFilterDto,
|
|
407
|
+
listProcessor,
|
|
408
|
+
subscriber,
|
|
409
|
+
job,
|
|
204
410
|
genMigration,
|
|
205
411
|
migration,
|
|
206
412
|
seed,
|
|
413
|
+
jobConstantsFile,
|
|
414
|
+
entityConstantsFile,
|
|
415
|
+
jobMapsFile,
|
|
416
|
+
entityMapsFile,
|
|
207
417
|
emptyEs6Classes,
|
|
208
418
|
moduleFile,
|
|
209
419
|
};
|