@saulpaulus17/node-module-generator 2.0.2
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/.github/workflows/ci.yml +28 -0
- package/.github/workflows/release.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/SECURITY.md +21 -0
- package/bin/cli.js +14 -0
- package/commands/dto.command.js +15 -0
- package/commands/module.command.js +10 -0
- package/commands/repository.command.js +10 -0
- package/commands/resource.command.js +10 -0
- package/commands/usecase.command.js +15 -0
- package/generator/dto.generator.js +21 -0
- package/generator/module.generator.js +34 -0
- package/generator/repository.generator.js +36 -0
- package/generator/resource.generator.js +57 -0
- package/generator/usecase.generator.js +26 -0
- package/package.json +30 -0
- package/release.yml +0 -0
- package/src/modules/order/application/usecases/create-order.usecase.js +15 -0
- package/src/modules/order/application/usecases/create-order.usecase.test.js +26 -0
- package/src/modules/order/domain/entities/order.entity.js +8 -0
- package/src/modules/order/domain/repositories/order.repository.interface.js +11 -0
- package/src/modules/order/infrastructure/repositories/order.repository.impl.js +23 -0
- package/src/modules/order/infrastructure/validation/create-order.schema.js +7 -0
- package/src/modules/order/interfaces/controllers/order.controller.js +16 -0
- package/src/modules/order/interfaces/controllers/order.controller.test.js +46 -0
- package/src/modules/order/interfaces/routes/order.routes.js +9 -0
- package/src/modules/order/order.module.js +16 -0
- package/src/modules/product/application/usecases/getProduct.usecase.js +15 -0
- package/src/modules/product/application/usecases/getProduct.usecase.test.js +26 -0
- package/src/modules/product/domain/entities/product.entity.js +8 -0
- package/src/modules/product/domain/repositories/product.repository.interface.js +11 -0
- package/src/modules/product/infrastructure/repositories/product.repository.impl.js +23 -0
- package/src/modules/product/infrastructure/validation/getProduct.schema.js +7 -0
- package/src/modules/product/product.module.js +5 -0
- package/src/modules/user/application/usecases/create-user.usecase.js +15 -0
- package/src/modules/user/application/usecases/create-user.usecase.test.js +26 -0
- package/src/modules/user/domain/entities/user.entity.js +8 -0
- package/src/modules/user/domain/repositories/user.repository.interface.js +11 -0
- package/src/modules/user/infrastructure/repositories/user.repository.impl.js +23 -0
- package/src/modules/user/infrastructure/validation/create-user.schema.js +7 -0
- package/src/modules/user/interfaces/controllers/user.controller.js +16 -0
- package/src/modules/user/interfaces/controllers/user.controller.test.js +46 -0
- package/src/modules/user/interfaces/routes/user.routes.js +9 -0
- package/src/modules/user/user.module.js +16 -0
- package/templates/module/controller.ejs +16 -0
- package/templates/module/controller.test.ejs +46 -0
- package/templates/module/di.ejs +16 -0
- package/templates/module/dto.ejs +7 -0
- package/templates/module/entity.ejs +8 -0
- package/templates/module/repository.impl.ejs +23 -0
- package/templates/module/repository.interface.ejs +11 -0
- package/templates/module/route.ejs +9 -0
- package/templates/module/usecase.ejs +15 -0
- package/templates/module/usecase.test.ejs +26 -0
- package/utils/case.util.js +10 -0
- package/utils/file.util.js +0 -0
- package/utils/logger.util.js +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: ["main"]
|
|
5
|
+
pull_request:
|
|
6
|
+
branches: ["main"]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
node-version: [18, 20]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
17
|
+
uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: ${{ matrix.node-version }}
|
|
20
|
+
- name: Cache node modules
|
|
21
|
+
uses: actions/cache@v4
|
|
22
|
+
with:
|
|
23
|
+
path: ~/.npm
|
|
24
|
+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: npm ci
|
|
27
|
+
- name: Run tests
|
|
28
|
+
run: npm test
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*.*.*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout
|
|
14
|
+
uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Setup Node
|
|
17
|
+
uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: 20
|
|
20
|
+
registry-url: 'https://registry.npmjs.org'
|
|
21
|
+
|
|
22
|
+
- name: Install deps
|
|
23
|
+
run: npm install
|
|
24
|
+
|
|
25
|
+
- name: Run test
|
|
26
|
+
run: npm test
|
|
27
|
+
|
|
28
|
+
- name: Publish to npm
|
|
29
|
+
run: npm publish --access public
|
|
30
|
+
env:
|
|
31
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ixspx-dev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>🚀 Node Module Generator</h1>
|
|
3
|
+
<p>A robust CLI tool for scaffolding Express.js projects using Clean Architecture & Dependency Injection.</p>
|
|
4
|
+
<p>
|
|
5
|
+
<a href="https://github.com/saul-paulus/node-module-generator/actions/workflows/ci.yml">
|
|
6
|
+
<img src="https://github.com/saul-paulus/node-module-generator/actions/workflows/ci.yml/badge.svg" alt="CI Status">
|
|
7
|
+
</a>
|
|
8
|
+
</p>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 📖 Overview
|
|
14
|
+
|
|
15
|
+
**Node Module Generator** provides a world-class Developer Experience (DX) equivalent to the robust CLIs found in ecosystems like NestJS or Angular, but designed specifically for pure Node.js environments utilizing **Express.js** and **Clean Architecture**.
|
|
16
|
+
|
|
17
|
+
It instantly scaffolds fully-tested, decoupled, and highly cohesive module structures with built-in Dependency Injection using **Awilix**.
|
|
18
|
+
|
|
19
|
+
## ✨ Features
|
|
20
|
+
|
|
21
|
+
- 🏗️ **Clean Architecture by Default**: Automatically separates concerns into Domain, Application, Infrastructure, and Interface layers.
|
|
22
|
+
- 💉 **Dependency Injection Ready**: Auto-generates Awilix configurations mapped correctly across use cases, controllers, and repositories.
|
|
23
|
+
- 🧪 **Test-Driven Design**: Scaffolds adjoining `*.test.js` files containing boilerplates for Jest to promote TDD.
|
|
24
|
+
- 🤖 **Continuous Integration**: Codebase natively incorporates GitHub Actions for automated unit testing checks.
|
|
25
|
+
- 🧩 **Granular Scaffolding**: Generate specific components (UseCases, Repositories, DTOs) dynamically on demand without overriding existing folders!
|
|
26
|
+
|
|
27
|
+
## 📦 Installation
|
|
28
|
+
|
|
29
|
+
To use this CLI tool locally or globally on your machine, you can install it directly from NPM.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Install globally via NPM
|
|
33
|
+
npm install -g @saulpaulus17/node-module-generator
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 🚀 Usage
|
|
37
|
+
|
|
38
|
+
Once installed globally, you can execute the CLI commands from any of your Node.js project directories using the `nmg` command keyword.
|
|
39
|
+
|
|
40
|
+
### Granular CLI Commands
|
|
41
|
+
Scaffold specifically what you need, exactly how you do it in NestJS:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# 1. Scaffolds a new module architecture and empty DI registry
|
|
45
|
+
nmg module product
|
|
46
|
+
|
|
47
|
+
# 2. Creates a specific Use Case and its Test inside an existing module
|
|
48
|
+
nmg usecase updateProduct --module=product
|
|
49
|
+
|
|
50
|
+
# 3. Creates Domain Entity and Repository Interfaces/Implementations
|
|
51
|
+
nmg repository product
|
|
52
|
+
|
|
53
|
+
# 4. Scaffolds a DTO validation schema
|
|
54
|
+
nmg dto getProduct --module=product
|
|
55
|
+
|
|
56
|
+
# 5. Generates an entire full-stack CRUD Resource (Controller, Entity, Repos, DI, etc.)
|
|
57
|
+
nmg resource order
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
_(Note: If you haven't installed the package globally, you can run it via `npx @saulpaulus17/node-module-generator <command> <name>`)_
|
|
61
|
+
|
|
62
|
+
## 📂 Full Resource Structure
|
|
63
|
+
|
|
64
|
+
Running the powerhouse command `nmg resource user` instantly generates the following decoupled blueprint within your project's `src/modules` directory:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
src/modules/user/
|
|
68
|
+
├── application/ # Orchestration & Use Cases
|
|
69
|
+
│ ├── dtos/ # (Generated schema references)
|
|
70
|
+
│ └── usecases/
|
|
71
|
+
│ ├── create-user.usecase.js # Business logically perfectly isolated
|
|
72
|
+
│ └── create-user.usecase.test.js # Scaffolded test harness
|
|
73
|
+
├── domain/ # Blueprints & Business Rules (Pure Logic)
|
|
74
|
+
│ ├── entities/
|
|
75
|
+
│ │ └── user.entity.js
|
|
76
|
+
│ └── repositories/ # Repository Interfaces (Contracts)
|
|
77
|
+
│ └── user.repository.interface.js
|
|
78
|
+
├── infrastructure/ # Technical Details & Implementation
|
|
79
|
+
│ ├── repositories/ # Implementations (mocked to replace with Prisma/TypeORM)
|
|
80
|
+
│ │ └── user.repository.impl.js
|
|
81
|
+
│ └── validation/ # Input validation schemas
|
|
82
|
+
│ └── create-user.schema.js # Auto-stubbed validation schema
|
|
83
|
+
├── interfaces/ # Entry Points (Web/API)
|
|
84
|
+
│ ├── controllers/
|
|
85
|
+
│ │ ├── user.controller.js # Express.js class handlers
|
|
86
|
+
│ │ └── user.controller.test.js
|
|
87
|
+
│ └── routes/
|
|
88
|
+
│ └── user.routes.js # Express routers integrating the controller
|
|
89
|
+
└── user.module.js # Centralised Awilix Dependency Injection bindings
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## 🛠️ Technological Footprint
|
|
93
|
+
|
|
94
|
+
The scaffolded code integrates perfectly if you are using the following libraries in your Express application:
|
|
95
|
+
|
|
96
|
+
- **[Express.js](https://expressjs.com/)** - Web Framework
|
|
97
|
+
- **[Awilix](https://github.com/jeffijoe/awilix)** - Powerful Dependency Injection container
|
|
98
|
+
- **[Jest](https://jestjs.io/)** - For the colocated testing environments
|
|
99
|
+
- **[Joi](https://joi.dev/)** - For infrastructure schema validations
|
|
100
|
+
|
|
101
|
+
## 📝 License
|
|
102
|
+
|
|
103
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
Use this section to tell people about which versions of your project are
|
|
6
|
+
currently being supported with security updates.
|
|
7
|
+
|
|
8
|
+
| Version | Supported |
|
|
9
|
+
| ------- | ------------------ |
|
|
10
|
+
| 5.1.x | :white_check_mark: |
|
|
11
|
+
| 5.0.x | :x: |
|
|
12
|
+
| 4.0.x | :white_check_mark: |
|
|
13
|
+
| < 4.0 | :x: |
|
|
14
|
+
|
|
15
|
+
## Reporting a Vulnerability
|
|
16
|
+
|
|
17
|
+
Use this section to tell people how to report a vulnerability.
|
|
18
|
+
|
|
19
|
+
Tell them where to go, how often they can expect to get an update on a
|
|
20
|
+
reported vulnerability, what to expect if the vulnerability is accepted or
|
|
21
|
+
declined, etc.
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require("commander");
|
|
4
|
+
const program = new Command();
|
|
5
|
+
|
|
6
|
+
program.name("nmg").version("1.0.0").description("Clean nodejs CLI");
|
|
7
|
+
|
|
8
|
+
require("../commands/module.command")(program);
|
|
9
|
+
require("../commands/usecase.command")(program);
|
|
10
|
+
require("../commands/resource.command")(program);
|
|
11
|
+
require("../commands/repository.command")(program);
|
|
12
|
+
require("../commands/dto.command")(program);
|
|
13
|
+
|
|
14
|
+
program.parse();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const generateDto = require("../generator/dto.generator");
|
|
2
|
+
|
|
3
|
+
module.exports = (program) => {
|
|
4
|
+
program
|
|
5
|
+
.command("dto <name>")
|
|
6
|
+
.description("Create a new DTO (validation schema)")
|
|
7
|
+
.option("-m, --module <moduleName>", "Specify the target module")
|
|
8
|
+
.action((name, options) => {
|
|
9
|
+
if (!options.module) {
|
|
10
|
+
console.error("error: required option '-m, --module <moduleName>' not specified");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
generateDto(name, options.module);
|
|
14
|
+
});
|
|
15
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const generateRepository = require("../generator/repository.generator");
|
|
2
|
+
|
|
3
|
+
module.exports = (program) => {
|
|
4
|
+
program
|
|
5
|
+
.command("repository <moduleName>")
|
|
6
|
+
.description("Create domain and infrastructure repositories for a module")
|
|
7
|
+
.action((moduleName) => {
|
|
8
|
+
generateRepository(moduleName);
|
|
9
|
+
});
|
|
10
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const generateUsecase = require("../generator/usecase.generator");
|
|
2
|
+
|
|
3
|
+
module.exports = (program) => {
|
|
4
|
+
program
|
|
5
|
+
.command("usecase <name>")
|
|
6
|
+
.description("Create a new usecase inside an existing module")
|
|
7
|
+
.option("-m, --module <moduleName>", "Specify the target module")
|
|
8
|
+
.action((name, options) => {
|
|
9
|
+
if (!options.module) {
|
|
10
|
+
console.error("error: required option '-m, --module <moduleName>' not specified");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
generateUsecase(name, options.module);
|
|
14
|
+
});
|
|
15
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs-extra");
|
|
3
|
+
const ejs = require("ejs");
|
|
4
|
+
const { pascalCase, camelCase } = require("../utils/case.util");
|
|
5
|
+
|
|
6
|
+
module.exports = async function (schemaName, moduleName) {
|
|
7
|
+
const basePath = path.join(process.cwd(), "src/modules", moduleName);
|
|
8
|
+
const validationDir = path.join(basePath, "infrastructure/validation");
|
|
9
|
+
fs.ensureDirSync(validationDir);
|
|
10
|
+
|
|
11
|
+
const templateData = {
|
|
12
|
+
name: moduleName,
|
|
13
|
+
className: pascalCase(schemaName),
|
|
14
|
+
camelName: camelCase(moduleName),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const templateContent = await ejs.renderFile(path.join(__dirname, "../templates/module/dto.ejs"), templateData);
|
|
18
|
+
fs.writeFileSync(path.join(validationDir, `${schemaName}.schema.js`), templateContent);
|
|
19
|
+
|
|
20
|
+
console.log(`✔ DTO Schema ${schemaName} generated inside module ${moduleName}.`);
|
|
21
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs-extra");
|
|
3
|
+
const ejs = require("ejs");
|
|
4
|
+
const { pascalCase, camelCase } = require("../utils/case.util");
|
|
5
|
+
|
|
6
|
+
module.exports = async function (name) {
|
|
7
|
+
const basePath = path.join(process.cwd(), "src/modules", name);
|
|
8
|
+
|
|
9
|
+
const dirs = [
|
|
10
|
+
"domain/entities",
|
|
11
|
+
"domain/repositories",
|
|
12
|
+
"domain/services",
|
|
13
|
+
"application/usecases",
|
|
14
|
+
"application/dtos",
|
|
15
|
+
"infrastructure/repositories",
|
|
16
|
+
"infrastructure/validation",
|
|
17
|
+
"infrastructure/security",
|
|
18
|
+
"infrastructure/services",
|
|
19
|
+
"interfaces/controllers",
|
|
20
|
+
"interfaces/routes",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// Create directories
|
|
24
|
+
dirs.forEach((dir) => fs.ensureDirSync(path.join(basePath, dir)));
|
|
25
|
+
|
|
26
|
+
console.log(`✔ Module ${name} directory structure created.`);
|
|
27
|
+
|
|
28
|
+
// Optionally create an empty module DI file
|
|
29
|
+
const diFile = path.join(basePath, `${name}.module.js`);
|
|
30
|
+
if (!fs.existsSync(diFile)) {
|
|
31
|
+
fs.writeFileSync(diFile, `module.exports = function register${pascalCase(name)}Module(container) {\n container.register({\n // Inject dependencies here\n });\n};\n`);
|
|
32
|
+
console.log(`✔ Module DI registry ${name}.module.js created.`);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs-extra");
|
|
3
|
+
const ejs = require("ejs");
|
|
4
|
+
const { pascalCase, camelCase } = require("../utils/case.util");
|
|
5
|
+
|
|
6
|
+
module.exports = async function (moduleName) {
|
|
7
|
+
const basePath = path.join(process.cwd(), "src/modules", moduleName);
|
|
8
|
+
|
|
9
|
+
const entityDir = path.join(basePath, "domain/entities");
|
|
10
|
+
const repoInterfaceDir = path.join(basePath, "domain/repositories");
|
|
11
|
+
const repoImplDir = path.join(basePath, "infrastructure/repositories");
|
|
12
|
+
|
|
13
|
+
fs.ensureDirSync(entityDir);
|
|
14
|
+
fs.ensureDirSync(repoInterfaceDir);
|
|
15
|
+
fs.ensureDirSync(repoImplDir);
|
|
16
|
+
|
|
17
|
+
const templateData = {
|
|
18
|
+
name: moduleName,
|
|
19
|
+
className: pascalCase(moduleName),
|
|
20
|
+
camelName: camelCase(moduleName),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const renderAndWrite = async (templateName, outputPath) => {
|
|
24
|
+
const templateContent = await ejs.renderFile(
|
|
25
|
+
path.join(__dirname, "../templates/module", templateName),
|
|
26
|
+
templateData
|
|
27
|
+
);
|
|
28
|
+
fs.writeFileSync(path.join(basePath, outputPath), templateContent);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
await renderAndWrite("entity.ejs", `domain/entities/${moduleName}.entity.js`);
|
|
32
|
+
await renderAndWrite("repository.interface.ejs", `domain/repositories/${moduleName}.repository.interface.js`);
|
|
33
|
+
await renderAndWrite("repository.impl.ejs", `infrastructure/repositories/${moduleName}.repository.impl.js`);
|
|
34
|
+
|
|
35
|
+
console.log(`✔ Repository patterns for ${moduleName} generated successfully.`);
|
|
36
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs-extra");
|
|
3
|
+
const ejs = require("ejs");
|
|
4
|
+
const { pascalCase, camelCase } = require("../utils/case.util");
|
|
5
|
+
|
|
6
|
+
module.exports = async function (name) {
|
|
7
|
+
const basePath = path.join(process.cwd(), "src/modules", name);
|
|
8
|
+
|
|
9
|
+
const dirs = [
|
|
10
|
+
"domain/entities",
|
|
11
|
+
"domain/repositories",
|
|
12
|
+
"domain/services",
|
|
13
|
+
"application/usecases",
|
|
14
|
+
"application/dtos",
|
|
15
|
+
"infrastructure/repositories",
|
|
16
|
+
"infrastructure/validation",
|
|
17
|
+
"infrastructure/security",
|
|
18
|
+
"infrastructure/services",
|
|
19
|
+
"interfaces/controllers",
|
|
20
|
+
"interfaces/routes",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
dirs.forEach((dir) => fs.ensureDirSync(path.join(basePath, dir)));
|
|
24
|
+
|
|
25
|
+
const templateData = {
|
|
26
|
+
name,
|
|
27
|
+
className: pascalCase(name),
|
|
28
|
+
camelName: camelCase(name),
|
|
29
|
+
useCaseClassName: `Create${pascalCase(name)}UseCase`,
|
|
30
|
+
useCaseFileName: `create-${name}`,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const renderAndWrite = async (templateName, outputPath) => {
|
|
34
|
+
const templateContent = await ejs.renderFile(
|
|
35
|
+
path.join(__dirname, "../templates/module", templateName),
|
|
36
|
+
templateData
|
|
37
|
+
);
|
|
38
|
+
fs.writeFileSync(path.join(basePath, outputPath), templateContent);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
await renderAndWrite("controller.ejs", `interfaces/controllers/${name}.controller.js`);
|
|
42
|
+
await renderAndWrite("controller.test.ejs", `interfaces/controllers/${name}.controller.test.js`);
|
|
43
|
+
await renderAndWrite("route.ejs", `interfaces/routes/${name}.routes.js`);
|
|
44
|
+
|
|
45
|
+
await renderAndWrite("usecase.ejs", `application/usecases/create-${name}.usecase.js`);
|
|
46
|
+
await renderAndWrite("usecase.test.ejs", `application/usecases/create-${name}.usecase.test.js`);
|
|
47
|
+
|
|
48
|
+
await renderAndWrite("entity.ejs", `domain/entities/${name}.entity.js`);
|
|
49
|
+
await renderAndWrite("repository.interface.ejs", `domain/repositories/${name}.repository.interface.js`);
|
|
50
|
+
|
|
51
|
+
await renderAndWrite("repository.impl.ejs", `infrastructure/repositories/${name}.repository.impl.js`);
|
|
52
|
+
await renderAndWrite("dto.ejs", `infrastructure/validation/create-${name}.schema.js`);
|
|
53
|
+
|
|
54
|
+
await renderAndWrite("di.ejs", `${name}.module.js`);
|
|
55
|
+
|
|
56
|
+
console.log(`✔ Resource ${name} generated successfully with Clean Architecture and Awilix DI!`);
|
|
57
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs-extra");
|
|
3
|
+
const ejs = require("ejs");
|
|
4
|
+
const { pascalCase, camelCase } = require("../utils/case.util");
|
|
5
|
+
|
|
6
|
+
module.exports = async function (useCaseName, moduleName) {
|
|
7
|
+
const basePath = path.join(process.cwd(), "src/modules", moduleName);
|
|
8
|
+
const ucDir = path.join(basePath, "application/usecases");
|
|
9
|
+
fs.ensureDirSync(ucDir);
|
|
10
|
+
|
|
11
|
+
const templateData = {
|
|
12
|
+
name: moduleName,
|
|
13
|
+
className: pascalCase(moduleName),
|
|
14
|
+
camelName: camelCase(moduleName),
|
|
15
|
+
useCaseClassName: `${pascalCase(useCaseName)}UseCase`,
|
|
16
|
+
useCaseFileName: useCaseName,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const templateContent = await ejs.renderFile(path.join(__dirname, "../templates/module/usecase.ejs"), templateData);
|
|
20
|
+
fs.writeFileSync(path.join(ucDir, `${useCaseName}.usecase.js`), templateContent);
|
|
21
|
+
|
|
22
|
+
const testContent = await ejs.renderFile(path.join(__dirname, "../templates/module/usecase.test.ejs"), templateData);
|
|
23
|
+
fs.writeFileSync(path.join(ucDir, `${useCaseName}.usecase.test.js`), testContent);
|
|
24
|
+
|
|
25
|
+
console.log(`✔ Usecase ${useCaseName} generated inside module ${moduleName}.`);
|
|
26
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saulpaulus17/node-module-generator",
|
|
3
|
+
"version": "2.0.2",
|
|
4
|
+
"description": "CLI tool to grenerate modular scaffolding for nodejs projects following clean arsitecture principles. ",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nodejs",
|
|
7
|
+
"module-generator",
|
|
8
|
+
"module",
|
|
9
|
+
"clean-architecture"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "saul-paulus (ixspx)",
|
|
13
|
+
"type": "commonjs",
|
|
14
|
+
"main": "index.js",
|
|
15
|
+
"bin": {
|
|
16
|
+
"nmg": "./bin/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "jest"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"chalk": "^5.6.2",
|
|
23
|
+
"commander": "^14.0.3",
|
|
24
|
+
"ejs": "^5.0.1",
|
|
25
|
+
"fs-extra": "^11.3.4"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"jest": "^30.3.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/release.yml
ADDED
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class CreateOrderUseCase {
|
|
2
|
+
constructor({ orderRepository }) {
|
|
3
|
+
this.orderRepository = orderRepository;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
async execute(dto) {
|
|
7
|
+
// Validate DTO...
|
|
8
|
+
// Apply business logic...
|
|
9
|
+
|
|
10
|
+
const entity = await this.orderRepository.save(dto);
|
|
11
|
+
return entity;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = CreateOrderUseCase;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const CreateOrderUseCase = require('./create-order.usecase');
|
|
2
|
+
|
|
3
|
+
describe('CreateOrderUseCase', () => {
|
|
4
|
+
let useCase;
|
|
5
|
+
let mockRepository;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
mockRepository = {
|
|
9
|
+
save: jest.fn()
|
|
10
|
+
};
|
|
11
|
+
useCase = new CreateOrderUseCase({ orderRepository: mockRepository });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('execute', () => {
|
|
15
|
+
it('should save data using the repository and return the entity', async () => {
|
|
16
|
+
const mockDto = { name: 'Test' };
|
|
17
|
+
const mockEntity = { id: 1, ...mockDto };
|
|
18
|
+
mockRepository.save.mockResolvedValue(mockEntity);
|
|
19
|
+
|
|
20
|
+
const result = await useCase.execute(mockDto);
|
|
21
|
+
|
|
22
|
+
expect(mockRepository.save).toHaveBeenCalledWith(mockDto);
|
|
23
|
+
expect(result).toEqual(mockEntity);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const IOrderRepository = require('../../domain/repositories/order.repository.interface');
|
|
2
|
+
const OrderEntity = require('../../domain/entities/order.entity');
|
|
3
|
+
|
|
4
|
+
class OrderRepositoryImpl extends IOrderRepository {
|
|
5
|
+
// If using Prisma, you would do: constructor({ prisma }) { super(); this.prisma = prisma; }
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.db = new Map();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async save(dto) {
|
|
12
|
+
const id = Date.now().toString(); // Mock ID generation
|
|
13
|
+
const entity = new OrderEntity({ id, ...dto });
|
|
14
|
+
this.db.set(id, entity);
|
|
15
|
+
return entity;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async findById(id) {
|
|
19
|
+
return this.db.get(id) || null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = OrderRepositoryImpl;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class OrderController {
|
|
2
|
+
constructor({ orderUseCase }) {
|
|
3
|
+
this.orderUseCase = orderUseCase;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
create = async (req, res, next) => {
|
|
7
|
+
try {
|
|
8
|
+
const result = await this.orderUseCase.execute(req.body);
|
|
9
|
+
res.status(201).json({ success: true, data: result });
|
|
10
|
+
} catch (error) {
|
|
11
|
+
next(error);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = OrderController;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const OrderController = require('./order.controller');
|
|
2
|
+
|
|
3
|
+
describe('OrderController', () => {
|
|
4
|
+
let controller;
|
|
5
|
+
let mockUseCase;
|
|
6
|
+
let mockReq;
|
|
7
|
+
let mockRes;
|
|
8
|
+
let mockNext;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
mockUseCase = {
|
|
12
|
+
execute: jest.fn()
|
|
13
|
+
};
|
|
14
|
+
controller = new OrderController({ orderUseCase: mockUseCase });
|
|
15
|
+
mockReq = {
|
|
16
|
+
body: { test: 'data' }
|
|
17
|
+
};
|
|
18
|
+
mockRes = {
|
|
19
|
+
status: jest.fn().mockReturnThis(),
|
|
20
|
+
json: jest.fn()
|
|
21
|
+
};
|
|
22
|
+
mockNext = jest.fn();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('create', () => {
|
|
26
|
+
it('should return 201 and result if use case succeeds', async () => {
|
|
27
|
+
const mockResult = { id: 1, ...mockReq.body };
|
|
28
|
+
mockUseCase.execute.mockResolvedValue(mockResult);
|
|
29
|
+
|
|
30
|
+
await controller.create(mockReq, mockRes, mockNext);
|
|
31
|
+
|
|
32
|
+
expect(mockUseCase.execute).toHaveBeenCalledWith(mockReq.body);
|
|
33
|
+
expect(mockRes.status).toHaveBeenCalledWith(201);
|
|
34
|
+
expect(mockRes.json).toHaveBeenCalledWith({ success: true, data: mockResult });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should call next with error if use case fails', async () => {
|
|
38
|
+
const error = new Error('Test error');
|
|
39
|
+
mockUseCase.execute.mockRejectedValue(error);
|
|
40
|
+
|
|
41
|
+
await controller.create(mockReq, mockRes, mockNext);
|
|
42
|
+
|
|
43
|
+
expect(mockNext).toHaveBeenCalledWith(error);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const { asClass, asFunction } = require('awilix');
|
|
2
|
+
|
|
3
|
+
const OrderController = require('./interfaces/controllers/order.controller');
|
|
4
|
+
const createOrderRoutes = require('./interfaces/routes/order.routes');
|
|
5
|
+
const CreateOrderUseCase = require('./application/usecases/create-order.usecase');
|
|
6
|
+
const OrderRepository = require('./infrastructure/repositories/order.repository.impl');
|
|
7
|
+
|
|
8
|
+
module.exports = function registerOrderModule(container) {
|
|
9
|
+
container.register({
|
|
10
|
+
orderController: asClass(OrderController).singleton(),
|
|
11
|
+
orderRoutes: asFunction(createOrderRoutes).singleton(),
|
|
12
|
+
orderUseCase: asClass(CreateOrderUseCase).singleton(),
|
|
13
|
+
// Providing the interface's name as the injection token for the implementation
|
|
14
|
+
orderRepository: asClass(OrderRepository).singleton(),
|
|
15
|
+
});
|
|
16
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class GetproductUseCase {
|
|
2
|
+
constructor({ productRepository }) {
|
|
3
|
+
this.productRepository = productRepository;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
async execute(dto) {
|
|
7
|
+
// Validate DTO...
|
|
8
|
+
// Apply business logic...
|
|
9
|
+
|
|
10
|
+
const entity = await this.productRepository.save(dto);
|
|
11
|
+
return entity;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = GetproductUseCase;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const GetproductUseCase = require('./getProduct.usecase');
|
|
2
|
+
|
|
3
|
+
describe('GetproductUseCase', () => {
|
|
4
|
+
let useCase;
|
|
5
|
+
let mockRepository;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
mockRepository = {
|
|
9
|
+
save: jest.fn()
|
|
10
|
+
};
|
|
11
|
+
useCase = new GetproductUseCase({ productRepository: mockRepository });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('execute', () => {
|
|
15
|
+
it('should save data using the repository and return the entity', async () => {
|
|
16
|
+
const mockDto = { name: 'Test' };
|
|
17
|
+
const mockEntity = { id: 1, ...mockDto };
|
|
18
|
+
mockRepository.save.mockResolvedValue(mockEntity);
|
|
19
|
+
|
|
20
|
+
const result = await useCase.execute(mockDto);
|
|
21
|
+
|
|
22
|
+
expect(mockRepository.save).toHaveBeenCalledWith(mockDto);
|
|
23
|
+
expect(result).toEqual(mockEntity);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const IProductRepository = require('../../domain/repositories/product.repository.interface');
|
|
2
|
+
const ProductEntity = require('../../domain/entities/product.entity');
|
|
3
|
+
|
|
4
|
+
class ProductRepositoryImpl extends IProductRepository {
|
|
5
|
+
// If using Prisma, you would do: constructor({ prisma }) { super(); this.prisma = prisma; }
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.db = new Map();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async save(dto) {
|
|
12
|
+
const id = Date.now().toString(); // Mock ID generation
|
|
13
|
+
const entity = new ProductEntity({ id, ...dto });
|
|
14
|
+
this.db.set(id, entity);
|
|
15
|
+
return entity;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async findById(id) {
|
|
19
|
+
return this.db.get(id) || null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = ProductRepositoryImpl;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class CreateUserUseCase {
|
|
2
|
+
constructor({ userRepository }) {
|
|
3
|
+
this.userRepository = userRepository;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
async execute(dto) {
|
|
7
|
+
// Validate DTO...
|
|
8
|
+
// Apply business logic...
|
|
9
|
+
|
|
10
|
+
const entity = await this.userRepository.save(dto);
|
|
11
|
+
return entity;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = CreateUserUseCase;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const CreateUserUseCase = require('./create-user.usecase');
|
|
2
|
+
|
|
3
|
+
describe('CreateUserUseCase', () => {
|
|
4
|
+
let useCase;
|
|
5
|
+
let mockRepository;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
mockRepository = {
|
|
9
|
+
save: jest.fn()
|
|
10
|
+
};
|
|
11
|
+
useCase = new CreateUserUseCase({ userRepository: mockRepository });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('execute', () => {
|
|
15
|
+
it('should save data using the repository and return the entity', async () => {
|
|
16
|
+
const mockDto = { name: 'Test' };
|
|
17
|
+
const mockEntity = { id: 1, ...mockDto };
|
|
18
|
+
mockRepository.save.mockResolvedValue(mockEntity);
|
|
19
|
+
|
|
20
|
+
const result = await useCase.execute(mockDto);
|
|
21
|
+
|
|
22
|
+
expect(mockRepository.save).toHaveBeenCalledWith(mockDto);
|
|
23
|
+
expect(result).toEqual(mockEntity);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const IUserRepository = require('../../domain/repositories/user.repository.interface');
|
|
2
|
+
const UserEntity = require('../../domain/entities/user.entity');
|
|
3
|
+
|
|
4
|
+
class UserRepositoryImpl extends IUserRepository {
|
|
5
|
+
// If using Prisma, you would do: constructor({ prisma }) { super(); this.prisma = prisma; }
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.db = new Map();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async save(dto) {
|
|
12
|
+
const id = Date.now().toString(); // Mock ID generation
|
|
13
|
+
const entity = new UserEntity({ id, ...dto });
|
|
14
|
+
this.db.set(id, entity);
|
|
15
|
+
return entity;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async findById(id) {
|
|
19
|
+
return this.db.get(id) || null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = UserRepositoryImpl;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class UserController {
|
|
2
|
+
constructor({ userUseCase }) {
|
|
3
|
+
this.userUseCase = userUseCase;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
create = async (req, res, next) => {
|
|
7
|
+
try {
|
|
8
|
+
const result = await this.userUseCase.execute(req.body);
|
|
9
|
+
res.status(201).json({ success: true, data: result });
|
|
10
|
+
} catch (error) {
|
|
11
|
+
next(error);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = UserController;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const UserController = require('./user.controller');
|
|
2
|
+
|
|
3
|
+
describe('UserController', () => {
|
|
4
|
+
let controller;
|
|
5
|
+
let mockUseCase;
|
|
6
|
+
let mockReq;
|
|
7
|
+
let mockRes;
|
|
8
|
+
let mockNext;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
mockUseCase = {
|
|
12
|
+
execute: jest.fn()
|
|
13
|
+
};
|
|
14
|
+
controller = new UserController({ userUseCase: mockUseCase });
|
|
15
|
+
mockReq = {
|
|
16
|
+
body: { test: 'data' }
|
|
17
|
+
};
|
|
18
|
+
mockRes = {
|
|
19
|
+
status: jest.fn().mockReturnThis(),
|
|
20
|
+
json: jest.fn()
|
|
21
|
+
};
|
|
22
|
+
mockNext = jest.fn();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('create', () => {
|
|
26
|
+
it('should return 201 and result if use case succeeds', async () => {
|
|
27
|
+
const mockResult = { id: 1, ...mockReq.body };
|
|
28
|
+
mockUseCase.execute.mockResolvedValue(mockResult);
|
|
29
|
+
|
|
30
|
+
await controller.create(mockReq, mockRes, mockNext);
|
|
31
|
+
|
|
32
|
+
expect(mockUseCase.execute).toHaveBeenCalledWith(mockReq.body);
|
|
33
|
+
expect(mockRes.status).toHaveBeenCalledWith(201);
|
|
34
|
+
expect(mockRes.json).toHaveBeenCalledWith({ success: true, data: mockResult });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should call next with error if use case fails', async () => {
|
|
38
|
+
const error = new Error('Test error');
|
|
39
|
+
mockUseCase.execute.mockRejectedValue(error);
|
|
40
|
+
|
|
41
|
+
await controller.create(mockReq, mockRes, mockNext);
|
|
42
|
+
|
|
43
|
+
expect(mockNext).toHaveBeenCalledWith(error);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const { asClass, asFunction } = require('awilix');
|
|
2
|
+
|
|
3
|
+
const UserController = require('./interfaces/controllers/user.controller');
|
|
4
|
+
const createUserRoutes = require('./interfaces/routes/user.routes');
|
|
5
|
+
const CreateUserUseCase = require('./application/usecases/create-user.usecase');
|
|
6
|
+
const UserRepository = require('./infrastructure/repositories/user.repository.impl');
|
|
7
|
+
|
|
8
|
+
module.exports = function registerUserModule(container) {
|
|
9
|
+
container.register({
|
|
10
|
+
userController: asClass(UserController).singleton(),
|
|
11
|
+
userRoutes: asFunction(createUserRoutes).singleton(),
|
|
12
|
+
userUseCase: asClass(CreateUserUseCase).singleton(),
|
|
13
|
+
// Providing the interface's name as the injection token for the implementation
|
|
14
|
+
userRepository: asClass(UserRepository).singleton(),
|
|
15
|
+
});
|
|
16
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class <%= className %>Controller {
|
|
2
|
+
constructor({ <%= camelName %>UseCase }) {
|
|
3
|
+
this.<%= camelName %>UseCase = <%= camelName %>UseCase;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
create = async (req, res, next) => {
|
|
7
|
+
try {
|
|
8
|
+
const result = await this.<%= camelName %>UseCase.execute(req.body);
|
|
9
|
+
res.status(201).json({ success: true, data: result });
|
|
10
|
+
} catch (error) {
|
|
11
|
+
next(error);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = <%= className %>Controller;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const <%= className %>Controller = require('./<%= name %>.controller');
|
|
2
|
+
|
|
3
|
+
describe('<%= className %>Controller', () => {
|
|
4
|
+
let controller;
|
|
5
|
+
let mockUseCase;
|
|
6
|
+
let mockReq;
|
|
7
|
+
let mockRes;
|
|
8
|
+
let mockNext;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
mockUseCase = {
|
|
12
|
+
execute: jest.fn()
|
|
13
|
+
};
|
|
14
|
+
controller = new <%= className %>Controller({ <%= camelName %>UseCase: mockUseCase });
|
|
15
|
+
mockReq = {
|
|
16
|
+
body: { test: 'data' }
|
|
17
|
+
};
|
|
18
|
+
mockRes = {
|
|
19
|
+
status: jest.fn().mockReturnThis(),
|
|
20
|
+
json: jest.fn()
|
|
21
|
+
};
|
|
22
|
+
mockNext = jest.fn();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('create', () => {
|
|
26
|
+
it('should return 201 and result if use case succeeds', async () => {
|
|
27
|
+
const mockResult = { id: 1, ...mockReq.body };
|
|
28
|
+
mockUseCase.execute.mockResolvedValue(mockResult);
|
|
29
|
+
|
|
30
|
+
await controller.create(mockReq, mockRes, mockNext);
|
|
31
|
+
|
|
32
|
+
expect(mockUseCase.execute).toHaveBeenCalledWith(mockReq.body);
|
|
33
|
+
expect(mockRes.status).toHaveBeenCalledWith(201);
|
|
34
|
+
expect(mockRes.json).toHaveBeenCalledWith({ success: true, data: mockResult });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should call next with error if use case fails', async () => {
|
|
38
|
+
const error = new Error('Test error');
|
|
39
|
+
mockUseCase.execute.mockRejectedValue(error);
|
|
40
|
+
|
|
41
|
+
await controller.create(mockReq, mockRes, mockNext);
|
|
42
|
+
|
|
43
|
+
expect(mockNext).toHaveBeenCalledWith(error);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const { asClass, asFunction } = require('awilix');
|
|
2
|
+
|
|
3
|
+
const <%= className %>Controller = require('./interfaces/controllers/<%= name %>.controller');
|
|
4
|
+
const create<%= className %>Routes = require('./interfaces/routes/<%= name %>.routes');
|
|
5
|
+
const Create<%= className %>UseCase = require('./application/usecases/create-<%= name %>.usecase');
|
|
6
|
+
const <%= className %>Repository = require('./infrastructure/repositories/<%= name %>.repository.impl');
|
|
7
|
+
|
|
8
|
+
module.exports = function register<%= className %>Module(container) {
|
|
9
|
+
container.register({
|
|
10
|
+
<%= camelName %>Controller: asClass(<%= className %>Controller).singleton(),
|
|
11
|
+
<%= camelName %>Routes: asFunction(create<%= className %>Routes).singleton(),
|
|
12
|
+
<%= camelName %>UseCase: asClass(Create<%= className %>UseCase).singleton(),
|
|
13
|
+
// Providing the interface's name as the injection token for the implementation
|
|
14
|
+
<%= camelName %>Repository: asClass(<%= className %>Repository).singleton(),
|
|
15
|
+
});
|
|
16
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const I<%= className %>Repository = require('../../domain/repositories/<%= name %>.repository.interface');
|
|
2
|
+
const <%= className %>Entity = require('../../domain/entities/<%= name %>.entity');
|
|
3
|
+
|
|
4
|
+
class <%= className %>RepositoryImpl extends I<%= className %>Repository {
|
|
5
|
+
// If using Prisma, you would do: constructor({ prisma }) { super(); this.prisma = prisma; }
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.db = new Map();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async save(dto) {
|
|
12
|
+
const id = Date.now().toString(); // Mock ID generation
|
|
13
|
+
const entity = new <%= className %>Entity({ id, ...dto });
|
|
14
|
+
this.db.set(id, entity);
|
|
15
|
+
return entity;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async findById(id) {
|
|
19
|
+
return this.db.get(id) || null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = <%= className %>RepositoryImpl;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class <%= useCaseClassName %> {
|
|
2
|
+
constructor({ <%= camelName %>Repository }) {
|
|
3
|
+
this.<%= camelName %>Repository = <%= camelName %>Repository;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
async execute(dto) {
|
|
7
|
+
// Validate DTO...
|
|
8
|
+
// Apply business logic...
|
|
9
|
+
|
|
10
|
+
const entity = await this.<%= camelName %>Repository.save(dto);
|
|
11
|
+
return entity;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = <%= useCaseClassName %>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const <%= useCaseClassName %> = require('./<%= useCaseFileName %>.usecase');
|
|
2
|
+
|
|
3
|
+
describe('<%= useCaseClassName %>', () => {
|
|
4
|
+
let useCase;
|
|
5
|
+
let mockRepository;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
mockRepository = {
|
|
9
|
+
save: jest.fn()
|
|
10
|
+
};
|
|
11
|
+
useCase = new <%= useCaseClassName %>({ <%= camelName %>Repository: mockRepository });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('execute', () => {
|
|
15
|
+
it('should save data using the repository and return the entity', async () => {
|
|
16
|
+
const mockDto = { name: 'Test' };
|
|
17
|
+
const mockEntity = { id: 1, ...mockDto };
|
|
18
|
+
mockRepository.save.mockResolvedValue(mockEntity);
|
|
19
|
+
|
|
20
|
+
const result = await useCase.execute(mockDto);
|
|
21
|
+
|
|
22
|
+
expect(mockRepository.save).toHaveBeenCalledWith(mockDto);
|
|
23
|
+
expect(result).toEqual(mockEntity);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
exports.pascalCase = (str) => {
|
|
2
|
+
const words = str.match(/[a-z0-9]+/gi) || [];
|
|
3
|
+
return words.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('');
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
exports.camelCase = (str) => {
|
|
7
|
+
const words = str.match(/[a-z0-9]+/gi) || [];
|
|
8
|
+
if (!words.length) return '';
|
|
9
|
+
return words[0].toLowerCase() + words.slice(1).map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('');
|
|
10
|
+
};
|
|
File without changes
|
|
File without changes
|