@neohm/nh-cli 1.0.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 ADDED
@@ -0,0 +1,133 @@
1
+ # @neohm/nh-cli — `nh`
2
+
3
+ A scaffolding CLI that accelerates development on **neohm. NestJS projects**
4
+ built on top of the shared [`@neohm/nestend`](../nestend) library.
5
+
6
+ `nh` generates fully wired modules, entities, controllers, DTOs, data
7
+ processors, migrations and seeds — and keeps your import barrels
8
+ (`es6.classes.ts`) and `app.module.ts` registration in sync, so the project
9
+ builds without any manual import wiring.
10
+
11
+ Every generated artifact matches the conventions of the `nestjs-boilerplate`
12
+ reference project:
13
+
14
+ - **Filenames** are `dot.separated.lowercase` (no hyphens) — e.g.
15
+ `business.client.entity.ts`, `add.business.client.data.dto.ts`.
16
+ - **Class names** are `PascalCase` — e.g. `BusinessClientEntity`,
17
+ `AddBusinessClientDataDto`.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ # from the neohm-cli directory
23
+ npm install
24
+ npm link # exposes `nh` globally
25
+
26
+ # or run without linking
27
+ npx nh --help
28
+ ```
29
+
30
+ Run `nh` from **inside** a target NestJS project (e.g. `nestjs-boilerplate`).
31
+ The project root is auto-detected by walking up from the current directory for a
32
+ `package.json` next to a `src/` folder.
33
+
34
+ ## Commands
35
+
36
+ | Command | Description |
37
+ | --------------------- | ---------------------------------------------------------------------------------------------------------------- |
38
+ | `nh gen` | Interactive wizard: scaffold a module + entity + optional controller/DTO/processor/migration, then auto-wire it. |
39
+ | `nh migration` | Generate a standalone migration `<timestamp>M-<Name>.ts`. |
40
+ | `nh seed` | Generate a seed file `<timestamp>S-<Name>.ts`. |
41
+ | `nh sync` | Rebuild every `es6.classes.ts` barrel and register modules in `app.module.ts`. |
42
+ | `nh check` | Dry-run of `sync` — show what would change without writing. |
43
+ | `nh list` | List scaffolded modules with their entities and providers. |
44
+ | `nh remove <name>` | Safely remove a scaffolded module and de-register it (migrations are preserved). |
45
+ | `nh --help` / `nh -v` | Help and version. |
46
+
47
+ ### `nh gen`
48
+
49
+ ```text
50
+ $ nh gen
51
+ ? Module name (e.g. business): business
52
+ ? Entity name (e.g. BusinessClient): BusinessClient
53
+ ? Table name (e.g. nh_business_clients): nh_business_clients
54
+ ? Create controller (BusinessClientController)? Yes
55
+ ? Create DTO (AddBusinessClientDataDto)? Yes
56
+ ? Create data processor (BusinessClientDataProcessor)? Yes
57
+ ? Create migration (M-AddBusinessClientTable)? Yes
58
+ ```
59
+
60
+ Produces, under `src/business/`:
61
+
62
+ ```text
63
+ business/
64
+ business.module.ts # BusinessModule (imports CommonModule, QueueModule, ...)
65
+ es6.classes.ts # barrel of controllers/services/jobs/subscribers
66
+ controllers/business.client.controller.ts
67
+ dtos/add.business.client.data.dto.ts
68
+ entities/business.client.entity.ts
69
+ libraries/business.client.data.processor.ts
70
+ enums/ jobs/ services/ subscribers/
71
+ ```
72
+
73
+ plus `src/database/migrations/<timestamp>M-AddBusinessClientTable.ts`, and it
74
+ registers `BusinessModule` in `src/app.module.ts`.
75
+
76
+ Generated artifacts extend the `@neohm/nestend` base classes:
77
+
78
+ - Entity → `CommonEntity` (with an empty `attributes` jsonb column)
79
+ - DTO → `CommonPayloadDto`
80
+ - Data processor → `CommonDataProcessor` (`process()` → `validate()` → `set()`)
81
+ - Migration → `MigrationUtility`
82
+ - Seed → `SeederUtility`
83
+
84
+ Re-running `gen` for an existing module reuses it and only adds new files —
85
+ existing files are never overwritten.
86
+
87
+ ### `nh sync` and `nh check`
88
+
89
+ `sync` scans every module under `src/` and regenerates its `es6.classes.ts`
90
+ barrel from the files actually present in `controllers/`, `services/`, `jobs/`
91
+ and `subscribers/`, then ensures each module is imported and listed in
92
+ `app.module.ts`. Files are classified by their **folder** and the presence of an
93
+ exported class — not by a fragile filename suffix — so non-standard names are
94
+ never silently dropped. The operation is idempotent.
95
+
96
+ `check` performs the same analysis but only **prints** what would change:
97
+
98
+ ```text
99
+ $ nh check
100
+ es6.classes barrels:
101
+ ok src/business/es6.classes.ts
102
+ update src/test/es6.classes.ts
103
+ + services: TestService
104
+
105
+ app.module.ts:
106
+ ok src/app.module.ts
107
+ ```
108
+
109
+ ## Project layout
110
+
111
+ ```text
112
+ neohm-cli/
113
+ bin/nh.js # executable entry
114
+ src/index.js # commander program
115
+ src/commands/ # gen, migration, seed, sync, check, list, remove
116
+ src/lib/naming.js # word-splitting + case conversions
117
+ src/lib/paths.js # project-root detection
118
+ src/lib/templates.js # code templates
119
+ src/lib/sync.engine.js # module scan + barrel + app.module wiring
120
+ src/lib/fs.utils.js # safe writes + console reporting
121
+ ```
122
+
123
+ See [`documentation.tex`](./documentation.tex) for the full technical
124
+ reference.
125
+
126
+ ## Requirements
127
+
128
+ - Node.js (active LTS) and a recent npm
129
+ - A target project that depends on `@neohm/nestend`
130
+
131
+ ```
132
+
133
+ ```
package/bin/nh.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { program } = require('../src/index');
5
+
6
+ program.parseAsync(process.argv);
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@neohm/nh-cli",
3
+ "version": "1.0.0",
4
+ "description": "nh — Command Line Interface for neohm. NestJS projects",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "nh": "bin/nh.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/nh.js",
11
+ "prepublishOnly": "find bin src -name '*.js' | xargs node --check"
12
+ },
13
+ "packageManager": "yarn@1.22.22",
14
+ "files": [
15
+ "bin",
16
+ "src"
17
+ ],
18
+ "keywords": [
19
+ "nestjs",
20
+ "cli",
21
+ "scaffold",
22
+ "neohm",
23
+ "nestend"
24
+ ],
25
+ "author": "Ashim Baral",
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=22 <25"
29
+ },
30
+ "dependencies": {
31
+ "chalk": "^4.1.2",
32
+ "commander": "^12.1.0",
33
+ "inquirer": "^8.2.6"
34
+ }
35
+ }
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const chalk = require('chalk');
5
+
6
+ const { projectPaths } = require('../lib/paths');
7
+ const engine = require('../lib/sync.engine');
8
+ const { log } = require('../lib/fs.utils');
9
+
10
+ /** Dry-run of `nh sync`: report what would change without writing anything. */
11
+ async function check() {
12
+ const paths = projectPaths();
13
+ const modules = engine.scanModules(paths);
14
+
15
+ if (!modules.length) {
16
+ log.warn('No scaffolded modules found under src/.');
17
+ return;
18
+ }
19
+
20
+ const plan = engine.buildPlan(paths, modules);
21
+ let changes = 0;
22
+
23
+ console.log(chalk.bold('\nes6.classes barrels:'));
24
+ for (const p of plan.es6) {
25
+ const rel = path.relative(paths.root, p.path);
26
+ if (p.changed) {
27
+ changes++;
28
+ const verb = p.current === null ? 'create' : 'update';
29
+ console.log(` ${chalk.yellow(verb.padEnd(7))} ${rel}`);
30
+ printArrayDiff(p);
31
+ } else {
32
+ console.log(` ${chalk.green('ok'.padEnd(7))} ${rel}`);
33
+ }
34
+ }
35
+
36
+ console.log(chalk.bold('\napp.module.ts:'));
37
+ const am = plan.appModule;
38
+ const amRel = path.relative(paths.root, am.path);
39
+ if (am.missing) {
40
+ log.warn(` not found at ${amRel}`);
41
+ } else if (am.changed) {
42
+ changes++;
43
+ console.log(` ${chalk.yellow('update'.padEnd(7))} ${amRel}`);
44
+ for (const a of am.additions) console.log(` ${chalk.green('+')} ${a}`);
45
+ } else {
46
+ console.log(` ${chalk.green('ok'.padEnd(7))} ${amRel}`);
47
+ }
48
+
49
+ if (changes) {
50
+ console.log(chalk.yellow(`\n${changes} file(s) would change. Run \`nh sync\` to apply.`));
51
+ } else {
52
+ log.done('In sync — `nh sync` would make no changes.');
53
+ }
54
+ }
55
+
56
+ /** Show the registered class names a barrel would contain. */
57
+ function printArrayDiff(p) {
58
+ for (const cat of engine.CATEGORIES) {
59
+ const re = new RegExp(`${cat.key}:\\s*\\[([^\\]]*)\\]`);
60
+ const next = (p.next.match(re) || [])[1] || '';
61
+ const cur = p.current ? (p.current.match(re) || [])[1] || '' : '';
62
+ const nextItems = split(next);
63
+ const curItems = split(cur);
64
+ const added = nextItems.filter((x) => !curItems.includes(x));
65
+ for (const a of added) console.log(` ${chalk.green('+')} ${cat.key}: ${a}`);
66
+ }
67
+ }
68
+
69
+ function split(s) {
70
+ return s
71
+ .split(',')
72
+ .map((x) => x.trim())
73
+ .filter(Boolean);
74
+ }
75
+
76
+ module.exports = check;
@@ -0,0 +1,130 @@
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
+
14
+ async function gen() {
15
+ const paths = projectPaths();
16
+ log.info(`Project: ${paths.root}\n`);
17
+
18
+ // 1. Module ---------------------------------------------------------------
19
+ const { moduleNameRaw } = await inquirer.prompt([
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'),
25
+ },
26
+ ]);
27
+ const moduleDot = naming.toDotCase(moduleNameRaw);
28
+ const moduleDir = paths.moduleDir(moduleDot);
29
+ const moduleExisted = fs.existsSync(moduleDir);
30
+
31
+ if (moduleExisted) {
32
+ log.info(`Using existing module "${moduleDot}".`);
33
+ } else {
34
+ log.info(`Creating module "${moduleDot}".`);
35
+ }
36
+
37
+ // Always ensure the module skeleton (folders + module file + es6 barrel).
38
+ for (const sub of MODULE_SUBFOLDERS) ensureDir(path.join(moduleDir, sub));
39
+ report(
40
+ writeFile(
41
+ path.join(moduleDir, `${moduleDot}.module.ts`),
42
+ tpl.moduleFile(moduleDot),
43
+ ),
44
+ path.join(moduleDir, `${moduleDot}.module.ts`),
45
+ paths.root,
46
+ );
47
+ const es6Path = path.join(moduleDir, 'es6.classes.ts');
48
+ report(
49
+ writeFile(es6Path, tpl.emptyEs6Classes()),
50
+ es6Path,
51
+ paths.root,
52
+ );
53
+
54
+ // 2. Entity ---------------------------------------------------------------
55
+ const { entityName } = await inquirer.prompt([
56
+ {
57
+ type: 'input',
58
+ name: 'entityName',
59
+ message: 'Entity name (e.g. BusinessClient):',
60
+ validate: (v) => (v && v.trim() ? true : 'Entity name is required'),
61
+ },
62
+ ]);
63
+ const n = tpl.names(entityName);
64
+
65
+ // 3. Table ----------------------------------------------------------------
66
+ const { tableName } = await inquirer.prompt([
67
+ {
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'),
73
+ },
74
+ ]);
75
+
76
+ const entityPath = path.join(moduleDir, 'entities', n.entityFile);
77
+ report(
78
+ writeFile(entityPath, tpl.entity(entityName, tableName)),
79
+ entityPath,
80
+ paths.root,
81
+ );
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 },
89
+ ]);
90
+
91
+ if (answers.controller) {
92
+ const p = path.join(moduleDir, 'controllers', n.controllerFile);
93
+ report(writeFile(p, tpl.controller(entityName)), p, paths.root);
94
+ }
95
+
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
+ }
105
+
106
+ if (answers.processor) {
107
+ const p = path.join(moduleDir, 'libraries', n.processorFile);
108
+ report(writeFile(p, tpl.processor(entityName)), p, paths.root);
109
+ }
110
+
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);
116
+ }
117
+
118
+ // 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`));
121
+ const plan = sync.buildPlan(paths, [mod]);
122
+ 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)');
125
+
126
+ log.done(`Module "${moduleDot}" scaffolded.`);
127
+ console.log(chalk.dim('Run `nh sync` anytime to re-wire imports after manual edits.'));
128
+ }
129
+
130
+ module.exports = gen;
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const chalk = require('chalk');
5
+
6
+ const { projectPaths } = require('../lib/paths');
7
+ const engine = require('../lib/sync.engine');
8
+ const { log } = require('../lib/fs.utils');
9
+
10
+ /** List scaffolded modules and their entities/providers. */
11
+ async function list() {
12
+ const paths = projectPaths();
13
+ const modules = engine.scanModules(paths);
14
+
15
+ if (!modules.length) {
16
+ log.warn('No scaffolded modules found under src/.');
17
+ return;
18
+ }
19
+
20
+ console.log(chalk.bold(`\nModules in ${path.relative(process.cwd(), paths.srcDir) || 'src'}:\n`));
21
+ for (const mod of modules) {
22
+ console.log(`${chalk.cyan.bold(mod.moduleDot)} ${chalk.dim(`(${mod.moduleClass})`)}`);
23
+ printGroup('entities', mod.entities.map(base));
24
+ printGroup('controllers', mod.providers.controllers.map((x) => x.className));
25
+ printGroup('services', mod.providers.services.map((x) => x.className));
26
+ printGroup('jobs', mod.providers.jobs.map((x) => x.className));
27
+ printGroup('subscribers', mod.providers.subscribers.map((x) => x.className));
28
+ printGroup('dtos', mod.dtos.map(base));
29
+ console.log('');
30
+ }
31
+
32
+ console.log(chalk.dim(`${modules.length} module(s).`));
33
+ }
34
+
35
+ function base(file) {
36
+ return path.basename(file);
37
+ }
38
+
39
+ function printGroup(label, items) {
40
+ if (!items.length) return;
41
+ console.log(` ${chalk.dim(label.padEnd(12))} ${items.join(', ')}`);
42
+ }
43
+
44
+ module.exports = list;
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const inquirer = require('inquirer');
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');
10
+
11
+ async function migration() {
12
+ const paths = projectPaths();
13
+
14
+ const { tableName, migrationName } = await inquirer.prompt([
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'),
20
+ },
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'),
26
+ },
27
+ ]);
28
+
29
+ const ts = Date.now();
30
+ const file = `${ts}M-${naming.toPascalCase(migrationName)}.ts`;
31
+ const p = path.join(paths.migrationsDir, file);
32
+ report(writeFile(p, tpl.migration(migrationName, tableName, ts)), p, paths.root);
33
+ log.done(`Migration created (${file}).`);
34
+ }
35
+
36
+ module.exports = migration;
@@ -0,0 +1,95 @@
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 } = require('../lib/paths');
9
+ const naming = require('../lib/naming');
10
+ const engine = require('../lib/sync.engine');
11
+ const { log } = require('../lib/fs.utils');
12
+
13
+ /** Safely remove a scaffolded module and de-register it from app.module.ts. */
14
+ async function remove(name) {
15
+ const paths = projectPaths();
16
+ const moduleDot = naming.toDotCase(name);
17
+ const moduleDir = paths.moduleDir(moduleDot);
18
+
19
+ if (!fs.existsSync(moduleDir) || !engine.scanModules(paths).some((m) => m.moduleDot === moduleDot)) {
20
+ log.warn(`Module "${moduleDot}" not found under src/.`);
21
+ return;
22
+ }
23
+
24
+ const mod = engine.scanModule(
25
+ moduleDir,
26
+ path.join(moduleDir, `${moduleDot}.module.ts`),
27
+ );
28
+
29
+ console.log(chalk.bold(`\nThis will permanently delete:`));
30
+ console.log(` ${chalk.red(path.relative(paths.root, moduleDir))}/ ${chalk.dim('(entire module folder)')}`);
31
+ console.log(chalk.dim('Migrations/seeds in database/migrations are NOT touched.'));
32
+
33
+ const { confirm } = await inquirer.prompt([
34
+ {
35
+ type: 'confirm',
36
+ name: 'confirm',
37
+ message: `Remove module "${moduleDot}"?`,
38
+ default: false,
39
+ },
40
+ ]);
41
+ if (!confirm) {
42
+ log.info('Aborted.');
43
+ return;
44
+ }
45
+
46
+ // De-register from app.module.ts before deleting files.
47
+ if (fs.existsSync(paths.appModule) && mod.moduleClass) {
48
+ const before = fs.readFileSync(paths.appModule, 'utf8');
49
+ const after = deregister(before, mod.moduleClass, mod.moduleImportPath);
50
+ if (after !== before) {
51
+ fs.writeFileSync(paths.appModule, after);
52
+ log.removed(`${path.relative(paths.root, paths.appModule)} ${chalk.dim(`(-${mod.moduleClass})`)}`);
53
+ }
54
+ }
55
+
56
+ fs.rmSync(moduleDir, { recursive: true, force: true });
57
+ log.removed(`${path.relative(paths.root, moduleDir)}/`);
58
+
59
+ log.done(`Module "${moduleDot}" removed.`);
60
+ }
61
+
62
+ /** Strip a module's import line and its entry from the @Module imports array. */
63
+ function deregister(content, className, importPath) {
64
+ let out = content
65
+ .split('\n')
66
+ .filter((line) => {
67
+ const isImport =
68
+ /^import\b/.test(line) &&
69
+ new RegExp(`from\\s+['"]${escapeRe(importPath)}['"]`).test(line);
70
+ return !isImport;
71
+ })
72
+ .join('\n');
73
+
74
+ const idx = out.indexOf('imports:');
75
+ if (idx !== -1) {
76
+ const open = out.indexOf('[', idx);
77
+ const close = out.indexOf(']', open);
78
+ if (open !== -1 && close !== -1) {
79
+ const items = out
80
+ .slice(open + 1, close)
81
+ .split(',')
82
+ .map((s) => s.trim())
83
+ .filter((s) => s && s !== className);
84
+ const rebuilt = '\n ' + items.join(',\n ') + ',\n ';
85
+ out = out.slice(0, open + 1) + rebuilt + out.slice(close);
86
+ }
87
+ }
88
+ return out;
89
+ }
90
+
91
+ function escapeRe(str) {
92
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
93
+ }
94
+
95
+ module.exports = remove;
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const inquirer = require('inquirer');
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');
10
+
11
+ async function seed() {
12
+ const paths = projectPaths();
13
+
14
+ const { tableName, seedName } = await inquirer.prompt([
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'),
20
+ },
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'),
26
+ },
27
+ ]);
28
+
29
+ // The S- prefix (vs M- for migrations) distinguishes seeds visually in the
30
+ // shared database/migrations folder; both run through the TypeORM runner.
31
+ const ts = Date.now();
32
+ const file = `${ts}S-${naming.toPascalCase(seedName)}.ts`;
33
+ const p = path.join(paths.migrationsDir, file);
34
+ report(writeFile(p, tpl.seed(seedName, tableName, ts)), p, paths.root);
35
+ log.done(`Seed created (${file}).`);
36
+ }
37
+
38
+ module.exports = seed;
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const chalk = require('chalk');
5
+
6
+ const { projectPaths } = require('../lib/paths');
7
+ const engine = require('../lib/sync.engine');
8
+ const { log } = require('../lib/fs.utils');
9
+
10
+ async function sync() {
11
+ const paths = projectPaths();
12
+ const modules = engine.scanModules(paths);
13
+
14
+ if (!modules.length) {
15
+ log.warn('No scaffolded modules found under src/. Nothing to sync.');
16
+ return;
17
+ }
18
+
19
+ const plan = engine.buildPlan(paths, modules);
20
+ const written = engine.applyPlan(plan);
21
+
22
+ for (const p of plan.es6) {
23
+ const rel = path.relative(paths.root, p.path);
24
+ if (p.changed) log.overwritten(rel);
25
+ else console.log(` ${chalk.dim('ok')} ${rel}`);
26
+ }
27
+
28
+ const am = plan.appModule;
29
+ const amRel = path.relative(paths.root, am.path);
30
+ if (am.missing) {
31
+ log.warn(`app.module.ts not found at ${amRel} — skipped module registration.`);
32
+ } else if (am.changed) {
33
+ log.overwritten(`${amRel} ${chalk.dim(`(+${am.additions.join(', ')})`)}`);
34
+ } else {
35
+ console.log(` ${chalk.dim('ok')} ${amRel}`);
36
+ }
37
+
38
+ if (written.length) log.done(`Synced ${written.length} file(s).`);
39
+ else log.done('Everything already in sync.');
40
+ }
41
+
42
+ module.exports = sync;
package/src/index.js ADDED
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const { Command } = require('commander');
4
+ const chalk = require('chalk');
5
+ const pkg = require('../package.json');
6
+
7
+ const program = new Command();
8
+
9
+ function run(action) {
10
+ return async (...args) => {
11
+ try {
12
+ await action(...args);
13
+ } catch (err) {
14
+ console.error(chalk.red(`\n✗ ${err.message}`));
15
+ process.exitCode = 1;
16
+ }
17
+ };
18
+ }
19
+
20
+ program
21
+ .name('nh')
22
+ .description('nh — scaffolding CLI for neohm. NestJS projects (built on @neohm/nestend)')
23
+ .version(pkg.version, '-v, --version', 'output the current version');
24
+
25
+ program
26
+ .command('gen')
27
+ .description('interactive scaffolding wizard (module, entity, controller, dto, processor, migration)')
28
+ .action(run(() => require('./commands/gen')()));
29
+
30
+ program
31
+ .command('migration')
32
+ .description('generate a standalone migration file (<timestamp>M-<Name>.ts)')
33
+ .action(run(() => require('./commands/migration')()));
34
+
35
+ program
36
+ .command('seed')
37
+ .description('generate a seed file (<timestamp>S-<Name>.ts)')
38
+ .action(run(() => require('./commands/seed')()));
39
+
40
+ program
41
+ .command('sync')
42
+ .description('rebuild es6.classes barrels and register modules in app.module.ts')
43
+ .action(run(() => require('./commands/sync')()));
44
+
45
+ program
46
+ .command('check')
47
+ .description('dry-run of sync — show what would change without writing')
48
+ .action(run(() => require('./commands/check')()));
49
+
50
+ program
51
+ .command('list')
52
+ .description('list scaffolded modules and their entities/providers')
53
+ .action(run(() => require('./commands/list')()));
54
+
55
+ program
56
+ .command('remove')
57
+ .argument('<name>', 'module name to remove')
58
+ .description('safely remove a scaffolded module and de-register it')
59
+ .action(run((name) => require('./commands/remove')(name)));
60
+
61
+ module.exports = { program };
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
6
+
7
+ function ensureDir(dir) {
8
+ fs.mkdirSync(dir, { recursive: true });
9
+ }
10
+
11
+ /**
12
+ * Write a generated file. Returns one of: 'created' | 'skipped' | 'overwritten'.
13
+ * Existing files are never silently clobbered unless `force` is set.
14
+ */
15
+ function writeFile(filePath, content, { force = false } = {}) {
16
+ ensureDir(path.dirname(filePath));
17
+ const exists = fs.existsSync(filePath);
18
+ if (exists && !force) return 'skipped';
19
+ fs.writeFileSync(filePath, content);
20
+ return exists ? 'overwritten' : 'created';
21
+ }
22
+
23
+ function readFile(filePath) {
24
+ return fs.readFileSync(filePath, 'utf8');
25
+ }
26
+
27
+ /** List files in a dir matching a suffix (e.g. '.controller.ts'). Safe if dir missing. */
28
+ function listBySuffix(dir, suffix) {
29
+ if (!fs.existsSync(dir)) return [];
30
+ return fs
31
+ .readdirSync(dir)
32
+ .filter((f) => f.endsWith(suffix))
33
+ .map((f) => path.join(dir, f));
34
+ }
35
+
36
+ const log = {
37
+ created: (rel) => console.log(` ${chalk.green('create')} ${rel}`),
38
+ skipped: (rel) => console.log(` ${chalk.yellow('skip')} ${rel} ${chalk.dim('(exists)')}`),
39
+ overwritten: (rel) => console.log(` ${chalk.cyan('update')} ${rel}`),
40
+ removed: (rel) => console.log(` ${chalk.red('remove')} ${rel}`),
41
+ info: (msg) => console.log(chalk.dim(msg)),
42
+ warn: (msg) => console.log(chalk.yellow(`! ${msg}`)),
43
+ done: (msg) => console.log(chalk.green.bold(`\n✓ ${msg}`)),
44
+ };
45
+
46
+ /** Report a writeFile result against a path relative to the project root. */
47
+ function report(result, filePath, root) {
48
+ const rel = path.relative(root, filePath);
49
+ if (result === 'created') log.created(rel);
50
+ else if (result === 'skipped') log.skipped(rel);
51
+ else if (result === 'overwritten') log.overwritten(rel);
52
+ }
53
+
54
+ module.exports = {
55
+ ensureDir,
56
+ writeFile,
57
+ readFile,
58
+ listBySuffix,
59
+ log,
60
+ report,
61
+ };
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Naming helpers. Every generated filename follows the dot.separated.lowercase
5
+ * convention (e.g. business.client.entity.ts) and every class name is PascalCase
6
+ * (e.g. BusinessClientEntity). These helpers are the single source of truth for
7
+ * turning a raw user input (PascalCase, kebab-case, snake_case, spaced, or
8
+ * dotted) into the canonical word list both conventions are derived from.
9
+ */
10
+
11
+ /** Split any reasonable input into lowercase words. */
12
+ function toWords(input) {
13
+ if (!input) return [];
14
+ return String(input)
15
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2') // camelCase boundary
16
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') // ACRONYMBoundary
17
+ .split(/[\s._\-]+/)
18
+ .map((w) => w.trim().toLowerCase())
19
+ .filter(Boolean);
20
+ }
21
+
22
+ /** business.client */
23
+ function toDotCase(input) {
24
+ return toWords(input).join('.');
25
+ }
26
+
27
+ /** BusinessClient */
28
+ function toPascalCase(input) {
29
+ return toWords(input)
30
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
31
+ .join('');
32
+ }
33
+
34
+ /** businessClient */
35
+ function toCamelCase(input) {
36
+ const pascal = toPascalCase(input);
37
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
38
+ }
39
+
40
+ /** business-client (used for HTTP route segments / swagger tags) */
41
+ function toKebabCase(input) {
42
+ return toWords(input).join('-');
43
+ }
44
+
45
+ module.exports = {
46
+ toWords,
47
+ toDotCase,
48
+ toPascalCase,
49
+ toCamelCase,
50
+ toKebabCase,
51
+ };
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Locate the NestJS project root by walking up from the current working
8
+ * directory looking for a package.json that sits next to a `src/` folder.
9
+ * `nh` commands are expected to run from inside the target project
10
+ * (e.g. nestjs-boilerplate).
11
+ */
12
+ function findProjectRoot(start = process.cwd()) {
13
+ let dir = path.resolve(start);
14
+ while (true) {
15
+ const pkg = path.join(dir, 'package.json');
16
+ const src = path.join(dir, 'src');
17
+ if (fs.existsSync(pkg) && fs.existsSync(src)) return dir;
18
+ const parent = path.dirname(dir);
19
+ if (parent === dir) return null;
20
+ dir = parent;
21
+ }
22
+ }
23
+
24
+ function requireProjectRoot() {
25
+ const root = findProjectRoot();
26
+ if (!root) {
27
+ throw new Error(
28
+ 'Could not locate a NestJS project (no package.json next to a src/ folder). ' +
29
+ 'Run nh from inside your project, e.g. nestjs-boilerplate.',
30
+ );
31
+ }
32
+ return root;
33
+ }
34
+
35
+ /** Resolve the canonical paths used by every command. */
36
+ function projectPaths(root = requireProjectRoot()) {
37
+ const srcDir = path.join(root, 'src');
38
+ return {
39
+ root,
40
+ srcDir,
41
+ appModule: path.join(srcDir, 'app.module.ts'),
42
+ migrationsDir: path.join(srcDir, 'database', 'migrations'),
43
+ moduleDir: (moduleDot) => path.join(srcDir, moduleDot),
44
+ };
45
+ }
46
+
47
+ /** Sub-folders every scaffolded module owns. */
48
+ const MODULE_SUBFOLDERS = [
49
+ 'controllers',
50
+ 'subscribers',
51
+ 'services',
52
+ 'enums',
53
+ 'jobs',
54
+ 'libraries',
55
+ 'entities',
56
+ 'dtos',
57
+ ];
58
+
59
+ module.exports = {
60
+ findProjectRoot,
61
+ requireProjectRoot,
62
+ projectPaths,
63
+ MODULE_SUBFOLDERS,
64
+ };
@@ -0,0 +1,241 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * The sync engine is the single brain behind `nh sync`, `nh check` and the
8
+ * auto-wiring step of `nh gen`. It scans scaffolded modules, regenerates each
9
+ * module's es6.classes.ts barrel from the files actually present on disk, and
10
+ * registers every module in app.module.ts — so the project builds without any
11
+ * manual import wiring.
12
+ */
13
+
14
+ // Provider categories registered through es6.classes.ts. Files are classified
15
+ // by the folder they live in (not by a fragile filename suffix) so that
16
+ // non-standard names — e.g. the boilerplate's `test.service..ts` — are still
17
+ // picked up and never silently dropped from the barrel.
18
+ const CATEGORIES = [
19
+ { key: 'controllers', folder: 'controllers' },
20
+ { key: 'services', folder: 'services' },
21
+ { key: 'jobs', folder: 'jobs' },
22
+ { key: 'subscribers', folder: 'subscribers' },
23
+ ];
24
+
25
+ // Files in a category folder that are not providers themselves.
26
+ const NON_PROVIDER_FILES = new Set(['es6.classes.ts', 'index.ts']);
27
+
28
+ function exportedClassName(filePath) {
29
+ const content = fs.readFileSync(filePath, 'utf8');
30
+ const match = content.match(/export\s+class\s+(\w+)/);
31
+ return match ? match[1] : null;
32
+ }
33
+
34
+ /** Every .ts file in `dir` that exports a class (skips barrels/helpers). */
35
+ function listClassFiles(dir) {
36
+ if (!fs.existsSync(dir)) return [];
37
+ return fs
38
+ .readdirSync(dir)
39
+ .filter((f) => f.endsWith('.ts') && !NON_PROVIDER_FILES.has(f))
40
+ .sort()
41
+ .map((f) => path.join(dir, f))
42
+ .filter((f) => exportedClassName(f));
43
+ }
44
+
45
+ /** .ts files in `dir` matching a suffix (used for display-only listings). */
46
+ function listTsFiles(dir, suffix) {
47
+ if (!fs.existsSync(dir)) return [];
48
+ return fs
49
+ .readdirSync(dir)
50
+ .filter((f) => f.endsWith(suffix))
51
+ .sort()
52
+ .map((f) => path.join(dir, f));
53
+ }
54
+
55
+ /** Is this src sub-folder a scaffolded module (has its own *.module.ts)? */
56
+ function moduleFileIn(dir) {
57
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return null;
58
+ const found = fs
59
+ .readdirSync(dir)
60
+ .find((f) => f.endsWith('.module.ts'));
61
+ return found ? path.join(dir, found) : null;
62
+ }
63
+
64
+ /** Discover every scaffolded module under src/ (excludes the root app.module.ts). */
65
+ function scanModules(paths) {
66
+ const result = [];
67
+ if (!fs.existsSync(paths.srcDir)) return result;
68
+ for (const name of fs.readdirSync(paths.srcDir).sort()) {
69
+ const dir = path.join(paths.srcDir, name);
70
+ const modFile = moduleFileIn(dir);
71
+ if (!modFile) continue;
72
+ result.push(scanModule(dir, modFile));
73
+ }
74
+ return result;
75
+ }
76
+
77
+ function scanModule(dir, modFile) {
78
+ const moduleDot = path.basename(dir);
79
+ const providers = {};
80
+ for (const cat of CATEGORIES) {
81
+ providers[cat.key] = listClassFiles(path.join(dir, cat.folder)).map(
82
+ (file) => ({
83
+ file,
84
+ className: exportedClassName(file),
85
+ importPath: `./${cat.folder}/${path.basename(file, '.ts')}`,
86
+ }),
87
+ );
88
+ }
89
+ return {
90
+ moduleDot,
91
+ dir,
92
+ moduleFile: modFile,
93
+ moduleClass: exportedClassName(modFile),
94
+ moduleImportPath: `./${moduleDot}/${path.basename(modFile, '.ts')}`,
95
+ es6Path: path.join(dir, 'es6.classes.ts'),
96
+ providers,
97
+ entities: listTsFiles(path.join(dir, 'entities'), '.entity.ts'),
98
+ dtos: listTsFiles(path.join(dir, 'dtos'), '.dto.ts'),
99
+ };
100
+ }
101
+
102
+ /** Render the es6.classes.ts content for a scanned module. */
103
+ function buildEs6Content(mod) {
104
+ const importLines = [];
105
+ const arrayLines = [];
106
+ for (const cat of CATEGORIES) {
107
+ const entries = mod.providers[cat.key];
108
+ for (const e of entries) {
109
+ importLines.push(`import { ${e.className} } from '${e.importPath}';`);
110
+ }
111
+ const classes = entries.map((e) => e.className).join(', ');
112
+ arrayLines.push(` ${cat.key}: [${classes}],`);
113
+ }
114
+ const header = importLines.length ? importLines.join('\n') + '\n\n' : '';
115
+ return `${header}export const es6Classes = {\n${arrayLines.join('\n')}\n};\n`;
116
+ }
117
+
118
+ /** Plan the es6.classes.ts rewrite for one module. */
119
+ function planEs6(mod) {
120
+ const next = buildEs6Content(mod);
121
+ const current = fs.existsSync(mod.es6Path)
122
+ ? fs.readFileSync(mod.es6Path, 'utf8')
123
+ : null;
124
+ return {
125
+ type: 'es6',
126
+ moduleDot: mod.moduleDot,
127
+ path: mod.es6Path,
128
+ current,
129
+ next,
130
+ changed: current !== next,
131
+ };
132
+ }
133
+
134
+ // ---- app.module.ts registration --------------------------------------------
135
+
136
+ function ensureImportLine(content, className, importPath) {
137
+ if (new RegExp(`from\\s+['"]${escapeRe(importPath)}['"]`).test(content)) {
138
+ return content; // already imported
139
+ }
140
+ const importLine = `import { ${className} } from '${importPath}';`;
141
+ const lines = content.split('\n');
142
+ let lastImport = -1;
143
+ for (let i = 0; i < lines.length; i++) {
144
+ if (/^import\b/.test(lines[i]) || /^\s*}\s+from\s+/.test(lines[i])) {
145
+ lastImport = i;
146
+ }
147
+ }
148
+ if (lastImport === -1) return importLine + '\n' + content;
149
+ lines.splice(lastImport + 1, 0, importLine);
150
+ return lines.join('\n');
151
+ }
152
+
153
+ function ensureInImportsArray(content, className) {
154
+ const idx = content.indexOf('imports:');
155
+ if (idx === -1) return content;
156
+ const open = content.indexOf('[', idx);
157
+ const close = content.indexOf(']', open);
158
+ if (open === -1 || close === -1) return content;
159
+ const inner = content.slice(open + 1, close);
160
+ const items = inner
161
+ .split(',')
162
+ .map((s) => s.trim())
163
+ .filter(Boolean);
164
+ if (items.includes(className)) return content;
165
+ items.push(className);
166
+ const rebuilt = '\n ' + items.join(',\n ') + ',\n ';
167
+ return content.slice(0, open + 1) + rebuilt + content.slice(close);
168
+ }
169
+
170
+ function escapeRe(str) {
171
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
172
+ }
173
+
174
+ /** Plan app.module.ts additions for the given modules. */
175
+ function planAppModule(paths, modules) {
176
+ if (!fs.existsSync(paths.appModule)) {
177
+ return {
178
+ type: 'appModule',
179
+ path: paths.appModule,
180
+ current: null,
181
+ next: null,
182
+ changed: false,
183
+ additions: [],
184
+ missing: true,
185
+ };
186
+ }
187
+ const current = fs.readFileSync(paths.appModule, 'utf8');
188
+ let next = current;
189
+ const additions = [];
190
+ for (const mod of modules) {
191
+ if (!mod.moduleClass) continue;
192
+ const before = next;
193
+ next = ensureImportLine(next, mod.moduleClass, mod.moduleImportPath);
194
+ next = ensureInImportsArray(next, mod.moduleClass);
195
+ if (next !== before) additions.push(mod.moduleClass);
196
+ }
197
+ return {
198
+ type: 'appModule',
199
+ path: paths.appModule,
200
+ current,
201
+ next,
202
+ changed: next !== current,
203
+ additions,
204
+ missing: false,
205
+ };
206
+ }
207
+
208
+ /** Full plan: every module's es6 barrel + app.module registration. */
209
+ function buildPlan(paths, modules = scanModules(paths)) {
210
+ return {
211
+ modules,
212
+ es6: modules.map(planEs6),
213
+ appModule: planAppModule(paths, modules),
214
+ };
215
+ }
216
+
217
+ function applyPlan(plan) {
218
+ const written = [];
219
+ for (const p of plan.es6) {
220
+ if (p.changed) {
221
+ fs.writeFileSync(p.path, p.next);
222
+ written.push(p.path);
223
+ }
224
+ }
225
+ if (plan.appModule.changed) {
226
+ fs.writeFileSync(plan.appModule.path, plan.appModule.next);
227
+ written.push(plan.appModule.path);
228
+ }
229
+ return written;
230
+ }
231
+
232
+ module.exports = {
233
+ CATEGORIES,
234
+ scanModules,
235
+ scanModule,
236
+ buildEs6Content,
237
+ planEs6,
238
+ planAppModule,
239
+ buildPlan,
240
+ applyPlan,
241
+ };
@@ -0,0 +1,209 @@
1
+ 'use strict';
2
+
3
+ const { toDotCase, toPascalCase, toKebabCase } = require('./naming');
4
+
5
+ /**
6
+ * Code templates. All generated code imports shared base classes from the
7
+ * published library `@neohm/nestend` (verified exports: CommonEntity,
8
+ * CommonPayloadDto, CommonDataProcessor, MigrationUtility, SeederUtility, etc.)
9
+ * and matches the dot.separated filename / PascalCase class conventions used
10
+ * across nestjs-boilerplate.
11
+ */
12
+
13
+ /** Build the canonical names for a given entity-ish base (e.g. "BusinessClient"). */
14
+ function names(base) {
15
+ const dot = toDotCase(base);
16
+ const pascal = toPascalCase(base);
17
+ return {
18
+ dot,
19
+ pascal,
20
+ kebab: toKebabCase(base),
21
+ entityClass: `${pascal}Entity`,
22
+ entityFile: `${dot}.entity.ts`,
23
+ controllerClass: `${pascal}Controller`,
24
+ controllerFile: `${dot}.controller.ts`,
25
+ dtoClass: `Add${pascal}DataDto`,
26
+ dtoFile: `add.${dot}.data.dto.ts`,
27
+ processorClass: `${pascal}DataProcessor`,
28
+ processorFile: `${dot}.data.processor.ts`,
29
+ };
30
+ }
31
+
32
+ function entity(base, tableName) {
33
+ const n = names(base);
34
+ return `import { Column, Entity } from 'typeorm';
35
+ import { CommonEntity } from '@neohm/nestend';
36
+
37
+ @Entity({ name: '${tableName}' })
38
+ export class ${n.entityClass} extends CommonEntity {
39
+ // TODO: declare columns for ${n.pascal}.
40
+ @Column({ type: 'jsonb' })
41
+ attributes: Record<string, any>;
42
+ }
43
+ `;
44
+ }
45
+
46
+ function controller(base) {
47
+ const n = names(base);
48
+ return `import { Controller } from '@nestjs/common';
49
+ import { ApiTags } from '@nestjs/swagger';
50
+
51
+ @ApiTags('${n.kebab}')
52
+ @Controller('api/v1/${n.kebab}')
53
+ export class ${n.controllerClass} {
54
+ constructor() {}
55
+ }
56
+ `;
57
+ }
58
+
59
+ function dto(base) {
60
+ const n = names(base);
61
+ return `import { CommonPayloadDto } from '@neohm/nestend';
62
+
63
+ export class ${n.dtoClass} extends CommonPayloadDto {
64
+ // TODO: declare validated payload fields for ${n.pascal}.
65
+ }
66
+ `;
67
+ }
68
+
69
+ function processor(base) {
70
+ const n = names(base);
71
+ return `import { CommonDataProcessor } from '@neohm/nestend';
72
+ import { ${n.dtoClass} } from '../dtos/add.${n.dot}.data.dto';
73
+
74
+ export class ${n.processorClass} extends CommonDataProcessor {
75
+ protected payload: ${n.dtoClass};
76
+
77
+ async process(data: ${n.dtoClass}) {
78
+ this.payload = data;
79
+
80
+ await this.validate();
81
+
82
+ return this.set();
83
+ }
84
+
85
+ private async validate() {
86
+ // TODO: collect validation errors via this.addColumnError(...).
87
+ this.throwPresentErrors();
88
+ }
89
+
90
+ private async set() {
91
+ // TODO: persist ${n.pascal} using this.payload.
92
+ }
93
+ }
94
+ `;
95
+ }
96
+
97
+ /**
98
+ * Migration generated as part of `nh gen`, stubbed with the columns the
99
+ * generated entity carries (rootPrimary + created_by FK from CommonEntity,
100
+ * attributes JSON, timestamps).
101
+ */
102
+ function genMigration(base, tableName, timestamp) {
103
+ const n = names(base);
104
+ const className = `Add${n.pascal}Table${timestamp}`;
105
+ return `import { MigrationUtility } from '@neohm/nestend';
106
+
107
+ export class ${className} extends MigrationUtility {
108
+ constructor() {
109
+ super('${tableName}');
110
+ this.process();
111
+ }
112
+
113
+ process() {
114
+ this.rootPrimary();
115
+ this.foreign({ name: 'created_by', foreignTable: 'sys_users' });
116
+ // TODO: add columns matching ${n.entityClass} attributes.
117
+ this.json('attributes');
118
+ this.timestamps();
119
+ }
120
+ }
121
+ `;
122
+ }
123
+
124
+ /** Standalone migration (`nh migration`). */
125
+ function migration(migrationName, tableName, timestamp) {
126
+ const className = `${toPascalCase(migrationName)}${timestamp}`;
127
+ return `import { MigrationUtility } from '@neohm/nestend';
128
+
129
+ export class ${className} extends MigrationUtility {
130
+ constructor() {
131
+ super('${tableName}');
132
+ this.process();
133
+ }
134
+
135
+ process() {
136
+ this.rootPrimary();
137
+ // TODO: define columns for ${tableName}.
138
+ this.timestamps();
139
+ }
140
+ }
141
+ `;
142
+ }
143
+
144
+ /** Seed file (`nh seed`). */
145
+ function seed(seedName, tableName, timestamp) {
146
+ const className = `${toPascalCase(seedName)}${timestamp}`;
147
+ return `import { SeederUtility } from '@neohm/nestend';
148
+
149
+ export class ${className} extends SeederUtility {
150
+ constructor() {
151
+ super('${tableName}');
152
+ this.process();
153
+ }
154
+
155
+ process() {
156
+ // TODO: seed rows, e.g.
157
+ // this.addRecord({ id: 1, name: 'Example' });
158
+ }
159
+ }
160
+ `;
161
+ }
162
+
163
+ /** A freshly scaffolded module before any es6.classes content exists. */
164
+ function emptyEs6Classes() {
165
+ return `export const es6Classes = {
166
+ controllers: [],
167
+ services: [],
168
+ jobs: [],
169
+ subscribers: [],
170
+ };
171
+ `;
172
+ }
173
+
174
+ function moduleFile(moduleBase) {
175
+ const pascal = toPascalCase(moduleBase);
176
+ return `import { Module } from '@nestjs/common';
177
+ import {
178
+ CommonModule,
179
+ QueueModule,
180
+ AuthModule,
181
+ SystemPropertyModule,
182
+ } from '@neohm/nestend';
183
+ import { es6Classes } from './es6.classes';
184
+
185
+ @Module({
186
+ imports: [CommonModule, QueueModule, AuthModule, SystemPropertyModule],
187
+ controllers: [...es6Classes.controllers],
188
+ providers: [
189
+ ...es6Classes.services,
190
+ ...es6Classes.jobs,
191
+ ...es6Classes.subscribers,
192
+ ],
193
+ })
194
+ export class ${pascal}Module {}
195
+ `;
196
+ }
197
+
198
+ module.exports = {
199
+ names,
200
+ entity,
201
+ controller,
202
+ dto,
203
+ processor,
204
+ genMigration,
205
+ migration,
206
+ seed,
207
+ emptyEs6Classes,
208
+ moduleFile,
209
+ };