Skip to content

Commit 4e116f9

Browse files
committed
fix: Aprove the node-module-generator to ensure that every module generated follows the specified Clean Architecture standard, including all layers and naming conventions.
1 parent b3f8888 commit 4e116f9

22 files changed

Lines changed: 301 additions & 51 deletions

DOCS_STEPS.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Langkah-langkah Setelah Generate Modul Baru
2+
3+
Setelah menjalankan perintah `nmg module <name>`, ikuti langkah-langkah berikut untuk mengintegrasikan modul ke dalam proyek Node.js Anda:
4+
5+
## 1. Registrasi di Awilix Container (`src/container.js`)
6+
7+
Buka file `src/container.js` dan tambahkan registrasi untuk repository jika Anda ingin menggunakan alias spesifik:
8+
9+
```javascript
10+
import { asFunction } from 'awilix';
11+
12+
// ... di dalam container.register({ ... })
13+
container.register({
14+
// Contoh alias: [module]Repository -> prisma[Module]Repository
15+
authRepository: asFunction(({ prismaAuthRepository }) => prismaAuthRepository).scoped(),
16+
});
17+
```
18+
19+
*Catatan: Secara default, generator Awilix biasanya mengauto-load file. Pastikan loader Anda dikonfigurasi untuk memuat folder `usecases`, `repositories`, `controllers`, dan `routes`.*
20+
21+
## 2. Registrasi Route di Express (`src/app.js`)
22+
23+
Buka file `src/app.js` dan daftarkan router dari modul yang baru dibuat:
24+
25+
```javascript
26+
// ...
27+
app.use('/api/v1/auth', container.resolve('authRoutes'));
28+
// ...
29+
```
30+
31+
## 3. Implementasi Detail Bisnis
32+
* **Domain**: Tentukan skema data di `domain/entities`.
33+
* **Repository Interface**: Tambahkan method yang dibutuhkan di `domain/repositories`.
34+
* **Repository Implementation**: Implementasikan query database (Prisma) di `infrastructure/repositories`.
35+
* **UseCase**: Tulis logika bisnis utama di `application/usecases`.
36+
* **Controller**: Tangani input request dan panggil UseCase di `interfaces/controllers`.
37+
* **Routes**: Definisikan endpoint HTTP di `interfaces/routes`.
38+
39+
---
40+
*Generated by node-module-generator*

generator/dto.generator.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ const { pascalCase, camelCase } = require("../utils/case.util");
55

66
module.exports = async function (schemaName, moduleName) {
77
const basePath = path.join(process.cwd(), "src/modules", moduleName);
8-
const validationDir = path.join(basePath, "infrastructure/validation");
9-
fs.ensureDirSync(validationDir);
8+
const dtoDir = path.join(basePath, "application/dtos");
9+
fs.ensureDirSync(dtoDir);
1010

1111
const templateData = {
1212
name: moduleName,
@@ -15,7 +15,7 @@ module.exports = async function (schemaName, moduleName) {
1515
};
1616

1717
const templateContent = await ejs.renderFile(path.join(__dirname, "../templates/module/dto.ejs"), templateData);
18-
fs.writeFileSync(path.join(validationDir, `${schemaName}.schema.js`), templateContent);
18+
fs.writeFileSync(path.join(dtoDir, `${schemaName.toLowerCase()}.dto.js`), templateContent);
1919

20-
console.log(`✔ DTO Schema ${schemaName} generated inside module ${moduleName}.`);
20+
console.log(`✔ DTO ${schemaName} generated inside module ${moduleName} at application/dtos.`);
2121
};

generator/module.generator.js

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,36 @@ module.exports = async function (name) {
1919
// Create directories
2020
dirs.forEach((dir) => fs.ensureDirSync(path.join(basePath, dir)));
2121

22-
console.log(`✔ Module ${name} directory structure created.`);
22+
const templateData = {
23+
name,
24+
className: pascalCase(name),
25+
camelName: camelCase(name),
26+
useCaseClassName: `${pascalCase(name)}UseCase`,
27+
useCaseFileName: `${name.toLowerCase()}.usecase`,
28+
};
29+
30+
const renderAndWrite = async (templateName, outputPath) => {
31+
const templateContent = await ejs.renderFile(
32+
path.join(__dirname, "../templates/module", templateName),
33+
templateData
34+
);
35+
fs.writeFileSync(path.join(basePath, outputPath), templateContent);
36+
};
37+
38+
await renderAndWrite("controller.ejs", `interfaces/controllers/${pascalCase(name)}Controller.js`);
39+
await renderAndWrite("controller.test.ejs", `interfaces/controllers/${pascalCase(name)}Controller.test.js`);
40+
await renderAndWrite("route.ejs", `interfaces/routes/${name.toLowerCase()}.routes.js`);
41+
42+
await renderAndWrite("usecase.ejs", `application/usecases/${pascalCase(name)}UseCase.js`);
43+
await renderAndWrite("usecase.test.ejs", `application/usecases/${pascalCase(name)}UseCase.test.js`);
2344

24-
// Optionally create an empty module DI file
25-
const diFile = path.join(basePath, `${name}.module.js`);
26-
if (!fs.existsSync(diFile)) {
27-
fs.writeFileSync(diFile, `module.exports = function register${pascalCase(name)}Module(container) {\n container.register({\n // Inject dependencies here\n });\n};\n`);
28-
console.log(`✔ Module DI registry ${name}.module.js created.`);
29-
}
45+
await renderAndWrite("entity.ejs", `domain/entities/${pascalCase(name)}.js`);
46+
await renderAndWrite("repository.interface.ejs", `domain/repositories/${pascalCase(name)}Repository.js`);
47+
48+
await renderAndWrite("repository.impl.ejs", `infrastructure/repositories/Prisma${pascalCase(name)}Repository.js`);
49+
await renderAndWrite("dto.ejs", `application/dtos/${name.toLowerCase()}.dto.js`);
50+
51+
await renderAndWrite("di.ejs", `${name}.module.js`);
52+
53+
console.log(`✔ Module ${name} directory structure, standard files, and tests created.`);
3054
};

generator/resource.generator.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ module.exports = async function (name) {
2222
name,
2323
className: pascalCase(name),
2424
camelName: camelCase(name),
25-
useCaseClassName: `Create${pascalCase(name)}UseCase`,
26-
useCaseFileName: `Create${pascalCase(name)}UseCase`,
25+
useCaseClassName: `${pascalCase(name)}UseCase`,
26+
useCaseFileName: `${name.toLowerCase()}.usecase`,
2727
};
2828

2929
const renderAndWrite = async (templateName, outputPath) => {
@@ -38,8 +38,8 @@ module.exports = async function (name) {
3838
await renderAndWrite("controller.test.ejs", `interfaces/controllers/${pascalCase(name)}Controller.test.js`);
3939
await renderAndWrite("route.ejs", `interfaces/routes/${name.toLowerCase()}.routes.js`);
4040

41-
await renderAndWrite("usecase.ejs", `application/usecases/Create${pascalCase(name)}UseCase.js`);
42-
await renderAndWrite("usecase.test.ejs", `application/usecases/Create${pascalCase(name)}UseCase.test.js`);
41+
await renderAndWrite("usecase.ejs", `application/usecases/${pascalCase(name)}UseCase.js`);
42+
await renderAndWrite("usecase.test.ejs", `application/usecases/${pascalCase(name)}UseCase.test.js`);
4343

4444
await renderAndWrite("entity.ejs", `domain/entities/${pascalCase(name)}.js`);
4545
await renderAndWrite("repository.interface.ejs", `domain/repositories/${pascalCase(name)}Repository.js`);

src/modules/Auth/Auth.module.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { asClass, asFunction } from 'awilix';
2+
3+
import AuthController from './interfaces/controllers/AuthController';
4+
import createAuthRoutes from './interfaces/routes/auth.routes';
5+
import CreateAuthUseCase from './application/usecases/CreateAuthUseCase';
6+
import PrismaAuthRepository from './infrastructure/repositories/PrismaAuthRepository';
7+
8+
export default function registerAuthModule(container) {
9+
container.register({
10+
authController: asClass(AuthController).singleton(),
11+
authRoutes: asFunction(createAuthRoutes).singleton(),
12+
CreateAuthUseCase: asClass(CreateAuthUseCase).singleton(),
13+
authRepository: asFunction(({ prismaAuthRepository }) => prismaAuthRepository).scoped(),
14+
});
15+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// DTO Template
2+
export default {
3+
// Define DTO validation schemas here (e.g. Joi, Zod)
4+
createSchema: {
5+
// rules
6+
},
7+
updateSchema: {
8+
// rules
9+
}
10+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export default class AuthUseCase {
2+
constructor({ authRepository, jwtService }) {
3+
this.authRepository = authRepository;
4+
this.jwtService = jwtService;
5+
}
6+
7+
async execute(inputData) {
8+
// Logic for AuthUseCase
9+
return { success: true, data: inputData };
10+
}
11+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const AuthUseCase = require('./AuthUseCase');
2+
3+
describe('AuthUseCase', () => {
4+
let useCase;
5+
let mockRepository;
6+
7+
beforeEach(() => {
8+
mockRepository = {
9+
create: jest.fn()
10+
};
11+
useCase = new AuthUseCase({
12+
authRepository: mockRepository,
13+
jwtService: {}
14+
});
15+
});
16+
17+
describe('execute', () => {
18+
it('should call repository create and return success', async () => {
19+
const mockDto = { name: 'Test' };
20+
const mockResult = { id: 1, ...mockDto };
21+
mockRepository.create.mockResolvedValue(mockResult);
22+
23+
const result = await useCase.execute(mockDto);
24+
25+
expect(mockRepository.create).toHaveBeenCalledWith(mockDto);
26+
expect(result).toEqual({ success: true, data: mockDto });
27+
});
28+
});
29+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default class Auth {
2+
constructor(props) {
3+
Object.assign(this, props);
4+
}
5+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export default class AuthRepository {
2+
async findById(id) {
3+
throw new Error('Method not implemented.');
4+
}
5+
6+
async create(data) {
7+
throw new Error('Method not implemented.');
8+
}
9+
}

0 commit comments

Comments
 (0)