@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 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. nh_business_clients): nh_business_clients
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neohm/nh-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "nh — Command Line Interface for neohm. NestJS projects",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,15 +1,15 @@
1
- 'use strict';
1
+ "use strict";
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
5
- const inquirer = require('inquirer');
6
- const chalk = require('chalk');
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('../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');
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: 'input',
22
- name: 'moduleNameRaw',
23
- message: 'Module name (e.g. business):',
24
- validate: (v) => (v && v.trim() ? true : 'Module name is required'),
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, 'es6.classes.ts');
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: 'input',
58
- name: 'entityName',
59
- message: 'Entity name (e.g. BusinessClient):',
60
- validate: (v) => (v && v.trim() ? true : 'Entity name is required'),
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: 'input',
69
- name: 'tableName',
70
- message: 'Table name (e.g. nh_business_clients):',
71
- default: `nh_${naming.toWords(entityName).join('_')}s`,
72
- validate: (v) => (v && v.trim() ? true : 'Table name is required'),
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
- const entityPath = path.join(moduleDir, 'entities', n.entityFile);
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
- // 4-7. Optional artifacts -------------------------------------------------
84
- const answers = await inquirer.prompt([
85
- { type: 'confirm', name: 'controller', message: `Create controller (${n.controllerClass})?`, default: true },
86
- { type: 'confirm', name: 'dto', message: `Create DTO (${n.dtoClass})?`, default: true },
87
- { type: 'confirm', name: 'processor', message: `Create data processor (${n.processorClass})?`, default: true },
88
- { type: 'confirm', name: 'migration', message: `Create migration (M-Add${n.pascal}Table)?`, default: true },
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 (answers.controller) {
92
- const p = path.join(moduleDir, 'controllers', n.controllerFile);
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
- // A processor depends on its DTO type, so generate the DTO when either the
97
- // DTO step or the processor step is selected.
98
- if (answers.dto || answers.processor) {
99
- if (!answers.dto) {
100
- log.warn(`DTO ${n.dtoClass} is generated because the data processor depends on it.`);
101
- }
102
- const p = path.join(moduleDir, 'dtos', n.dtoFile);
103
- report(writeFile(p, tpl.dto(entityName)), p, paths.root);
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 (answers.processor) {
107
- const p = path.join(moduleDir, 'libraries', n.processorFile);
108
- report(writeFile(p, tpl.processor(entityName)), p, paths.root);
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
- if (answers.migration) {
112
- const ts = Date.now();
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('\nWiring imports...');
120
- const mod = sync.scanModule(moduleDir, path.join(moduleDir, `${moduleDot}.module.ts`));
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('overwritten', w, paths.root);
124
- if (!written.length) log.info(' (already in sync)');
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(chalk.dim('Run `nh sync` anytime to re-wire imports after manual edits.'));
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
- 'use strict';
1
+ "use strict";
2
2
 
3
- const path = require('path');
4
- const inquirer = require('inquirer');
3
+ const path = require("path");
4
+ const inquirer = require("inquirer");
5
5
 
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');
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: 'input',
17
- name: 'tableName',
18
- message: 'Table name (e.g. nh_business_clients):',
19
- validate: (v) => (v && v.trim() ? true : 'Table name is required'),
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: 'input',
23
- name: 'migrationName',
24
- message: 'Migration name (e.g. AddBusinessClientTable):',
25
- validate: (v) => (v && v.trim() ? true : 'Migration name is required'),
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(writeFile(p, tpl.migration(migrationName, tableName, ts)), p, paths.root);
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
 
@@ -1,28 +1,28 @@
1
- 'use strict';
1
+ "use strict";
2
2
 
3
- const path = require('path');
4
- const inquirer = require('inquirer');
3
+ const path = require("path");
4
+ const inquirer = require("inquirer");
5
5
 
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');
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: 'input',
17
- name: 'tableName',
18
- message: 'Table name (e.g. nh_business_clients):',
19
- validate: (v) => (v && v.trim() ? true : 'Table name is required'),
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: 'input',
23
- name: 'seedName',
24
- message: 'Seed name (e.g. SeedBusinessClients):',
25
- validate: (v) => (v && v.trim() ? true : 'Seed name is required'),
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
 
@@ -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
  ];
@@ -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: Record<string, any>;
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,