@neohm/nh-cli 1.0.0 → 1.0.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 +1 -1
- package/package.json +1 -1
- package/src/commands/gen.js +70 -61
- package/src/commands/migration.js +20 -16
- package/src/commands/seed.js +15 -15
- package/src/lib/sync.engine.js +2 -0
- package/src/lib/templates.js +69 -3
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ The project root is auto-detected by walking up from the current directory for a
|
|
|
50
50
|
$ nh gen
|
|
51
51
|
? Module name (e.g. business): business
|
|
52
52
|
? Entity name (e.g. BusinessClient): BusinessClient
|
|
53
|
-
? Table name (e.g.
|
|
53
|
+
? Table name (e.g. nh_user_details): nh_user_details
|
|
54
54
|
? Create controller (BusinessClientController)? Yes
|
|
55
55
|
? Create DTO (AddBusinessClientDataDto)? Yes
|
|
56
56
|
? Create data processor (BusinessClientDataProcessor)? Yes
|
package/package.json
CHANGED
package/src/commands/gen.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
const fs = require(
|
|
4
|
-
const path = require(
|
|
5
|
-
const inquirer = require(
|
|
6
|
-
const chalk = require(
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const inquirer = require("inquirer");
|
|
6
|
+
const chalk = require("chalk");
|
|
7
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(
|
|
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
13
|
|
|
14
14
|
async function gen() {
|
|
15
15
|
const paths = projectPaths();
|
|
@@ -18,10 +18,10 @@ async function gen() {
|
|
|
18
18
|
// 1. Module ---------------------------------------------------------------
|
|
19
19
|
const { moduleNameRaw } = await inquirer.prompt([
|
|
20
20
|
{
|
|
21
|
-
type:
|
|
22
|
-
name:
|
|
23
|
-
message:
|
|
24
|
-
validate: (v) => (v && v.trim() ? true :
|
|
21
|
+
type: "input",
|
|
22
|
+
name: "moduleNameRaw",
|
|
23
|
+
message: "Module name:",
|
|
24
|
+
validate: (v) => (v && v.trim() ? true : "Module name is required"),
|
|
25
25
|
},
|
|
26
26
|
]);
|
|
27
27
|
const moduleDot = naming.toDotCase(moduleNameRaw);
|
|
@@ -44,20 +44,16 @@ async function gen() {
|
|
|
44
44
|
path.join(moduleDir, `${moduleDot}.module.ts`),
|
|
45
45
|
paths.root,
|
|
46
46
|
);
|
|
47
|
-
const es6Path = path.join(moduleDir,
|
|
48
|
-
report(
|
|
49
|
-
writeFile(es6Path, tpl.emptyEs6Classes()),
|
|
50
|
-
es6Path,
|
|
51
|
-
paths.root,
|
|
52
|
-
);
|
|
47
|
+
const es6Path = path.join(moduleDir, "es6.classes.ts");
|
|
48
|
+
report(writeFile(es6Path, tpl.emptyEs6Classes()), es6Path, paths.root);
|
|
53
49
|
|
|
54
50
|
// 2. Entity ---------------------------------------------------------------
|
|
55
51
|
const { entityName } = await inquirer.prompt([
|
|
56
52
|
{
|
|
57
|
-
type:
|
|
58
|
-
name:
|
|
59
|
-
message:
|
|
60
|
-
validate: (v) => (v && v.trim() ? true :
|
|
53
|
+
type: "input",
|
|
54
|
+
name: "entityName",
|
|
55
|
+
message: "Entity name (PascalCase):",
|
|
56
|
+
validate: (v) => (v && v.trim() ? true : "Entity name is required"),
|
|
61
57
|
},
|
|
62
58
|
]);
|
|
63
59
|
const n = tpl.names(entityName);
|
|
@@ -65,66 +61,79 @@ async function gen() {
|
|
|
65
61
|
// 3. Table ----------------------------------------------------------------
|
|
66
62
|
const { tableName } = await inquirer.prompt([
|
|
67
63
|
{
|
|
68
|
-
type:
|
|
69
|
-
name:
|
|
70
|
-
message:
|
|
71
|
-
default: `nh_${naming.toWords(entityName).join(
|
|
72
|
-
validate: (v) => (v && v.trim() ? true :
|
|
64
|
+
type: "input",
|
|
65
|
+
name: "tableName",
|
|
66
|
+
message: "Table name:",
|
|
67
|
+
default: `nh_${naming.toWords(entityName).join("_")}s`,
|
|
68
|
+
validate: (v) => (v && v.trim() ? true : "Table name is required"),
|
|
73
69
|
},
|
|
74
70
|
]);
|
|
75
71
|
|
|
76
|
-
|
|
72
|
+
// Always generate: attributes DTO, entity, subscriber, job
|
|
73
|
+
const attrDtoPath = path.join(moduleDir, "dtos", n.attributesDtoFile);
|
|
74
|
+
report(writeFile(attrDtoPath, tpl.attributesDto(entityName)), attrDtoPath, paths.root);
|
|
75
|
+
|
|
76
|
+
const entityPath = path.join(moduleDir, "entities", n.entityFile);
|
|
77
77
|
report(
|
|
78
78
|
writeFile(entityPath, tpl.entity(entityName, tableName)),
|
|
79
79
|
entityPath,
|
|
80
80
|
paths.root,
|
|
81
81
|
);
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
const jobPath = path.join(moduleDir, "jobs", n.jobFile);
|
|
84
|
+
report(writeFile(jobPath, tpl.job(entityName)), jobPath, paths.root);
|
|
85
|
+
|
|
86
|
+
const subscriberPath = path.join(moduleDir, "subscribers", n.subscriberFile);
|
|
87
|
+
report(writeFile(subscriberPath, tpl.subscriber(entityName)), subscriberPath, paths.root);
|
|
88
|
+
|
|
89
|
+
// 4. Optional: controller -------------------------------------------------
|
|
90
|
+
const { controller } = await inquirer.prompt([
|
|
91
|
+
{
|
|
92
|
+
type: "confirm",
|
|
93
|
+
name: "controller",
|
|
94
|
+
message: `Create controller (${n.controllerClass})?`,
|
|
95
|
+
default: true,
|
|
96
|
+
},
|
|
89
97
|
]);
|
|
90
98
|
|
|
91
|
-
if (
|
|
92
|
-
const p = path.join(moduleDir,
|
|
99
|
+
if (controller) {
|
|
100
|
+
const p = path.join(moduleDir, "controllers", n.controllerFile);
|
|
93
101
|
report(writeFile(p, tpl.controller(entityName)), p, paths.root);
|
|
94
102
|
}
|
|
95
103
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
// 5. Optional: data processor (includes Add*DataDto) ----------------------
|
|
105
|
+
const { processor } = await inquirer.prompt([
|
|
106
|
+
{
|
|
107
|
+
type: "confirm",
|
|
108
|
+
name: "processor",
|
|
109
|
+
message: `Create data processor (${n.processorClass})?`,
|
|
110
|
+
default: true,
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
105
113
|
|
|
106
|
-
if (
|
|
107
|
-
const
|
|
108
|
-
report(writeFile(
|
|
109
|
-
}
|
|
114
|
+
if (processor) {
|
|
115
|
+
const dtoPath = path.join(moduleDir, "dtos", n.dtoFile);
|
|
116
|
+
report(writeFile(dtoPath, tpl.dto(entityName)), dtoPath, paths.root);
|
|
110
117
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const file = `${ts}M-Add${n.pascal}Table.ts`;
|
|
114
|
-
const p = path.join(paths.migrationsDir, file);
|
|
115
|
-
report(writeFile(p, tpl.genMigration(entityName, tableName, ts)), p, paths.root);
|
|
118
|
+
const procPath = path.join(moduleDir, "libraries", n.processorFile);
|
|
119
|
+
report(writeFile(procPath, tpl.processor(entityName)), procPath, paths.root);
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
// Auto-wire: rebuild this module's barrel and register it in app.module.ts.
|
|
119
|
-
log.info(
|
|
120
|
-
const mod = sync.scanModule(
|
|
123
|
+
log.info("\nWiring imports...");
|
|
124
|
+
const mod = sync.scanModule(
|
|
125
|
+
moduleDir,
|
|
126
|
+
path.join(moduleDir, `${moduleDot}.module.ts`),
|
|
127
|
+
);
|
|
121
128
|
const plan = sync.buildPlan(paths, [mod]);
|
|
122
129
|
const written = sync.applyPlan(plan);
|
|
123
|
-
for (const w of written) report(
|
|
124
|
-
if (!written.length) log.info(
|
|
130
|
+
for (const w of written) report("overwritten", w, paths.root);
|
|
131
|
+
if (!written.length) log.info(" (already in sync)");
|
|
125
132
|
|
|
126
133
|
log.done(`Module "${moduleDot}" scaffolded.`);
|
|
127
|
-
console.log(
|
|
134
|
+
console.log(
|
|
135
|
+
chalk.dim("Run `nh sync` anytime to re-wire imports after manual edits."),
|
|
136
|
+
);
|
|
128
137
|
}
|
|
129
138
|
|
|
130
139
|
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. AddBusinessClientTable):",
|
|
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. SeedBusinessClients):",
|
|
25
|
+
validate: (v) => (v && v.trim() ? true : "Seed name is required"),
|
|
26
26
|
},
|
|
27
27
|
]);
|
|
28
28
|
|
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
|
|
@@ -14,18 +14,26 @@ const { toDotCase, toPascalCase, toKebabCase } = require('./naming');
|
|
|
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
|
+
subscriberClass: `${pascal}Subscriber`,
|
|
34
|
+
subscriberFile: `${dot}.subscriber.ts`,
|
|
35
|
+
jobClass: `${pascal}Job`,
|
|
36
|
+
jobFile: `${dot}.job.ts`,
|
|
29
37
|
};
|
|
30
38
|
}
|
|
31
39
|
|
|
@@ -33,12 +41,20 @@ function entity(base, tableName) {
|
|
|
33
41
|
const n = names(base);
|
|
34
42
|
return `import { Column, Entity } from 'typeorm';
|
|
35
43
|
import { CommonEntity } from '@neohm/nestend';
|
|
44
|
+
import { ${n.attributesDtoClass} } from '../dtos/${n.dot}.attributes.dto';
|
|
36
45
|
|
|
37
46
|
@Entity({ name: '${tableName}' })
|
|
38
47
|
export class ${n.entityClass} extends CommonEntity {
|
|
39
|
-
// TODO: declare columns for ${n.pascal}.
|
|
40
48
|
@Column({ type: 'jsonb' })
|
|
41
|
-
attributes:
|
|
49
|
+
attributes: ${n.attributesDtoClass};
|
|
50
|
+
}
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function attributesDto(base) {
|
|
55
|
+
const n = names(base);
|
|
56
|
+
return `export class ${n.attributesDtoClass} {
|
|
57
|
+
// TODO: declare attribute fields for ${n.pascal}.
|
|
42
58
|
}
|
|
43
59
|
`;
|
|
44
60
|
}
|
|
@@ -94,6 +110,50 @@ export class ${n.processorClass} extends CommonDataProcessor {
|
|
|
94
110
|
`;
|
|
95
111
|
}
|
|
96
112
|
|
|
113
|
+
function subscriber(base) {
|
|
114
|
+
const n = names(base);
|
|
115
|
+
return `import { CommonSubscriber } from '@neohm/nestend';
|
|
116
|
+
import { EventSubscriber } from 'typeorm';
|
|
117
|
+
import { ${n.entityClass} } from '../entities/${n.dot}.entity';
|
|
118
|
+
import { DataSource } from 'typeorm';
|
|
119
|
+
import { ${n.jobClass} } from '../jobs/${n.dot}.job';
|
|
120
|
+
|
|
121
|
+
@EventSubscriber()
|
|
122
|
+
export class ${n.subscriberClass} extends CommonSubscriber<${n.entityClass}> {
|
|
123
|
+
constructor(
|
|
124
|
+
protected readonly datasource: DataSource,
|
|
125
|
+
protected readonly entityJob: ${n.jobClass},
|
|
126
|
+
) {
|
|
127
|
+
super();
|
|
128
|
+
datasource.subscribers.push(this);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
listenTo() {
|
|
132
|
+
return ${n.entityClass};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function job(base) {
|
|
139
|
+
const n = names(base);
|
|
140
|
+
return `import { CommonJob, QueueService } from '@neohm/nestend';
|
|
141
|
+
import { Injectable } from '@nestjs/common';
|
|
142
|
+
import { JobMap } from '../../constants/job.constants';
|
|
143
|
+
|
|
144
|
+
@Injectable()
|
|
145
|
+
export class ${n.jobClass} extends CommonJob {
|
|
146
|
+
constructor(protected readonly queueService: QueueService) {
|
|
147
|
+
super(JobMap.${n.camel}Job);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async handle(event: any): Promise<void> {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
156
|
+
|
|
97
157
|
/**
|
|
98
158
|
* Migration generated as part of `nh gen`, stubbed with the columns the
|
|
99
159
|
* generated entity carries (rootPrimary + created_by FK from CommonEntity,
|
|
@@ -165,6 +225,8 @@ function emptyEs6Classes() {
|
|
|
165
225
|
return `export const es6Classes = {
|
|
166
226
|
controllers: [],
|
|
167
227
|
services: [],
|
|
228
|
+
libraries: [],
|
|
229
|
+
dtos: [],
|
|
168
230
|
jobs: [],
|
|
169
231
|
subscribers: [],
|
|
170
232
|
};
|
|
@@ -187,6 +249,7 @@ import { es6Classes } from './es6.classes';
|
|
|
187
249
|
controllers: [...es6Classes.controllers],
|
|
188
250
|
providers: [
|
|
189
251
|
...es6Classes.services,
|
|
252
|
+
...es6Classes.libraries,
|
|
190
253
|
...es6Classes.jobs,
|
|
191
254
|
...es6Classes.subscribers,
|
|
192
255
|
],
|
|
@@ -198,9 +261,12 @@ export class ${pascal}Module {}
|
|
|
198
261
|
module.exports = {
|
|
199
262
|
names,
|
|
200
263
|
entity,
|
|
264
|
+
attributesDto,
|
|
201
265
|
controller,
|
|
202
266
|
dto,
|
|
203
267
|
processor,
|
|
268
|
+
subscriber,
|
|
269
|
+
job,
|
|
204
270
|
genMigration,
|
|
205
271
|
migration,
|
|
206
272
|
seed,
|