@mikemajesty/microservice-crud 7.1.0 → 7.1.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/README.md +40 -5
- package/package.json +1 -1
- package/src/cli.mjs +177 -5
- package/src/templates/mongo/modules/controller.mjs +1 -7
- package/src/templates/postgres/modules/controller.mjs +1 -7
- package/src/templates/typespec/controller.mjs +68 -0
- package/src/templates/typespec/exception.mjs +84 -0
- package/src/templates/typespec/model.mjs +71 -0
package/README.md
CHANGED
|
@@ -81,11 +81,18 @@ src/
|
|
|
81
81
|
│ ├── controller.ts # REST controller with decorators
|
|
82
82
|
│ ├── module.ts # NestJS module
|
|
83
83
|
│ └── repository.ts # Repository implementation
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
├── infra/
|
|
85
|
+
│ └── database/
|
|
86
|
+
│ └── [postgres|mongo]/
|
|
87
|
+
│ └── schemas/
|
|
88
|
+
│ └── [name].ts # Database schema
|
|
89
|
+
└── docs/
|
|
90
|
+
└── src/
|
|
91
|
+
└── modules/
|
|
92
|
+
└── [name]/
|
|
93
|
+
├── controller.tsp # TypeSpec API routes
|
|
94
|
+
├── model.tsp # TypeSpec models/DTOs
|
|
95
|
+
└── exception.tsp # TypeSpec error definitions
|
|
89
96
|
```
|
|
90
97
|
|
|
91
98
|
**Features:**
|
|
@@ -97,6 +104,7 @@ src/
|
|
|
97
104
|
- ✅ Permission-based access control
|
|
98
105
|
- ✅ Swagger documentation ready
|
|
99
106
|
- ✅ Full test coverage
|
|
107
|
+
- ✅ **TypeSpec API documentation** (auto-generated!)
|
|
100
108
|
|
|
101
109
|
### LIB (Library Module)
|
|
102
110
|
|
|
@@ -161,9 +169,36 @@ The CLI automatically registers generated modules:
|
|
|
161
169
|
- **CRUD/MODULE**: Registered in `src/app.module.ts`
|
|
162
170
|
- **LIB**: Registered in `src/libs/module.ts` with `LibModule` suffix
|
|
163
171
|
- **INFRA**: Registered in `src/infra/module.ts`
|
|
172
|
+
- **TypeSpec**: Auto-imports in `docs/src/main.tsp` (for CRUD templates)
|
|
164
173
|
|
|
165
174
|
No manual imports needed! 🎉
|
|
166
175
|
|
|
176
|
+
## TypeSpec Documentation
|
|
177
|
+
|
|
178
|
+
When generating CRUD modules (Postgres or Mongo), the CLI automatically creates TypeSpec documentation:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
docs/src/modules/[name]/
|
|
182
|
+
├── controller.tsp # API routes with all endpoints
|
|
183
|
+
├── model.tsp # Entity models and DTOs
|
|
184
|
+
└── exception.tsp # Error definitions (400, 404)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The import is automatically added to `docs/src/main.tsp`:
|
|
188
|
+
|
|
189
|
+
```typespec
|
|
190
|
+
import "./modules/[name]/controller.tsp";
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Generated TypeSpec includes:**
|
|
194
|
+
- ✅ All CRUD endpoints (POST, PUT, GET, DELETE)
|
|
195
|
+
- ✅ Input/Output models
|
|
196
|
+
- ✅ Pagination models
|
|
197
|
+
- ✅ Validation error definitions
|
|
198
|
+
- ✅ Not found error definitions
|
|
199
|
+
- ✅ Bearer authentication
|
|
200
|
+
- ✅ API versioning support
|
|
201
|
+
|
|
167
202
|
## Name Validation
|
|
168
203
|
|
|
169
204
|
Module names are automatically sanitized:
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -12,9 +12,12 @@ import { getModuleLib } from './templates/libs/module.mjs';
|
|
|
12
12
|
import { getServiceLib } from './templates/libs/service.mjs';
|
|
13
13
|
import { getModuleControllerModule } from './templates/module/controller.mjs';
|
|
14
14
|
import { getModuleModule } from './templates/module/module.mjs';
|
|
15
|
+
import { getTypeSpecController } from './templates/typespec/controller.mjs';
|
|
16
|
+
import { getTypeSpecModel } from './templates/typespec/model.mjs';
|
|
17
|
+
import { getTypeSpecException } from './templates/typespec/exception.mjs';
|
|
15
18
|
|
|
16
19
|
import fs from 'fs';
|
|
17
|
-
import { bold, green, red } from 'colorette';
|
|
20
|
+
import { bold, green, red, cyan, yellow, magenta, gray, blue, white } from 'colorette';
|
|
18
21
|
import fse from 'fs-extra';
|
|
19
22
|
import path from 'path';
|
|
20
23
|
import cliSelect from 'cli-select';
|
|
@@ -27,6 +30,99 @@ const __dirname = dirname(__filename);
|
|
|
27
30
|
|
|
28
31
|
const prompt = promptSync();
|
|
29
32
|
|
|
33
|
+
// Created files tracker
|
|
34
|
+
const createdFiles = [];
|
|
35
|
+
|
|
36
|
+
const trackFile = (filePath, category) => {
|
|
37
|
+
createdFiles.push({ path: filePath, category });
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const getFileIcon = (fileName) => {
|
|
41
|
+
if (fileName.endsWith('.spec.ts')) return '🧪';
|
|
42
|
+
if (fileName.includes('entity')) return '📦';
|
|
43
|
+
if (fileName.includes('repository')) return '🗄️';
|
|
44
|
+
if (fileName.includes('use-cases') || fileName.includes('usecase')) return '⚡';
|
|
45
|
+
if (fileName.includes('controller')) return '🎮';
|
|
46
|
+
if (fileName.includes('module')) return '📦';
|
|
47
|
+
if (fileName.includes('adapter')) return '🔌';
|
|
48
|
+
if (fileName.includes('schema')) return '📋';
|
|
49
|
+
if (fileName.includes('service')) return '⚙️';
|
|
50
|
+
if (fileName.includes('.tsp')) return '📝';
|
|
51
|
+
if (fileName.includes('index')) return '📤';
|
|
52
|
+
return '📄';
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const printCreatedFiles = (moduleName, moduleType) => {
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log('');
|
|
58
|
+
console.log(bold(cyan(' ╭───────────────────────────────────────────────────────────╮')));
|
|
59
|
+
console.log(bold(cyan(' │ │')));
|
|
60
|
+
console.log(bold(cyan(' │')) + bold(green(' 🚀 ')) + bold(white(moduleType.toUpperCase() + ' CREATED SUCCESSFULLY!')) + bold(cyan(' │')));
|
|
61
|
+
console.log(bold(cyan(' │ │')));
|
|
62
|
+
console.log(bold(cyan(' ╰───────────────────────────────────────────────────────────╯')));
|
|
63
|
+
console.log('');
|
|
64
|
+
console.log(bold(white(' 📋 Module: ')) + bold(yellow(moduleName)));
|
|
65
|
+
console.log('');
|
|
66
|
+
|
|
67
|
+
const categories = {};
|
|
68
|
+
createdFiles.forEach(file => {
|
|
69
|
+
if (!categories[file.category]) categories[file.category] = [];
|
|
70
|
+
categories[file.category].push(file.path);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const categoryConfig = {
|
|
74
|
+
'core/entity': { icon: '📦', label: 'Entity', color: magenta },
|
|
75
|
+
'core/repository': { icon: '🗄️ ', label: 'Repository', color: blue },
|
|
76
|
+
'core/use-cases': { icon: '⚡', label: 'Use Cases', color: yellow },
|
|
77
|
+
'core/tests': { icon: '🧪', label: 'Tests', color: green },
|
|
78
|
+
'modules': { icon: '🎮', label: 'Module Files', color: cyan },
|
|
79
|
+
'schemas': { icon: '📋', label: 'Database Schema', color: magenta },
|
|
80
|
+
'typespec': { icon: '📝', label: 'TypeSpec Docs', color: blue },
|
|
81
|
+
'libs': { icon: '📚', label: 'Library Files', color: yellow },
|
|
82
|
+
'infra': { icon: '🔧', label: 'Infrastructure', color: cyan },
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Define display order
|
|
86
|
+
const categoryOrder = [
|
|
87
|
+
'core/entity', 'core/repository', 'core/use-cases', 'core/tests',
|
|
88
|
+
'modules', 'schemas', 'typespec', 'libs', 'infra'
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const sortedCategories = categoryOrder.filter(cat => categories[cat]);
|
|
92
|
+
|
|
93
|
+
sortedCategories.forEach((category, catIndex) => {
|
|
94
|
+
const config = categoryConfig[category] || { icon: '📁', label: category, color: white };
|
|
95
|
+
const isLastCategory = catIndex === sortedCategories.length - 1;
|
|
96
|
+
const catPrefix = isLastCategory ? ' └─' : ' ├─';
|
|
97
|
+
|
|
98
|
+
console.log(gray(catPrefix) + ' ' + bold(config.color(`${config.icon} ${config.label}`)));
|
|
99
|
+
|
|
100
|
+
categories[category].forEach((file, index) => {
|
|
101
|
+
const isLast = index === categories[category].length - 1;
|
|
102
|
+
const linePrefix = isLastCategory ? ' ' : ' │ ';
|
|
103
|
+
const filePrefix = isLast ? '└──' : '├──';
|
|
104
|
+
const fileName = file.split('/').pop();
|
|
105
|
+
const icon = getFileIcon(fileName);
|
|
106
|
+
console.log(gray(`${linePrefix}${filePrefix}`) + ` ${icon} ` + white(fileName));
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const totalFiles = createdFiles.length;
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(bold(cyan(' ─────────────────────────────────────────────────────────────')));
|
|
113
|
+
console.log('');
|
|
114
|
+
console.log(bold(green(` ✨ ${totalFiles} file${totalFiles > 1 ? 's' : ''} created successfully!`)));
|
|
115
|
+
console.log('');
|
|
116
|
+
console.log(gray(' 💡 Next steps:'));
|
|
117
|
+
console.log(gray(' 1. ') + cyan('npm run build') + gray(' - Compile the project'));
|
|
118
|
+
console.log(gray(' 2. ') + cyan('npm run lint') + gray(' - Check code style'));
|
|
119
|
+
console.log(gray(' 3. ') + cyan('npm test') + gray(' - Run tests'));
|
|
120
|
+
console.log('');
|
|
121
|
+
|
|
122
|
+
// Clear for next run
|
|
123
|
+
createdFiles.length = 0;
|
|
124
|
+
};
|
|
125
|
+
|
|
30
126
|
// Function to add module to app.module.ts, libs/module.ts or infra/module.ts
|
|
31
127
|
const addModuleToAppModule = (dest, moduleName, importPath, targetFile = 'app.module.ts', moduleSuffix = 'Module') => {
|
|
32
128
|
try {
|
|
@@ -92,6 +188,50 @@ const addModuleToAppModule = (dest, moduleName, importPath, targetFile = 'app.mo
|
|
|
92
188
|
}
|
|
93
189
|
};
|
|
94
190
|
|
|
191
|
+
const createTypeSpecDocs = (dest, moduleName) => {
|
|
192
|
+
try {
|
|
193
|
+
const docsPath = `${dest}/docs/src/modules/${moduleName}`;
|
|
194
|
+
const mainTspPath = `${dest}/docs/src/main.tsp`;
|
|
195
|
+
|
|
196
|
+
if (!fs.existsSync(`${dest}/docs/src`)) {
|
|
197
|
+
console.log(bold(green(`TypeSpec docs folder not found, skipping...`)));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!fs.existsSync(docsPath)) {
|
|
202
|
+
fs.mkdirSync(docsPath, { recursive: true });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fs.writeFileSync(`${docsPath}/controller.tsp`, getTypeSpecController(moduleName));
|
|
206
|
+
trackFile(`docs/src/modules/${moduleName}/controller.tsp`, 'typespec');
|
|
207
|
+
fs.writeFileSync(`${docsPath}/model.tsp`, getTypeSpecModel(moduleName));
|
|
208
|
+
trackFile(`docs/src/modules/${moduleName}/model.tsp`, 'typespec');
|
|
209
|
+
fs.writeFileSync(`${docsPath}/exception.tsp`, getTypeSpecException(moduleName));
|
|
210
|
+
trackFile(`docs/src/modules/${moduleName}/exception.tsp`, 'typespec');
|
|
211
|
+
|
|
212
|
+
if (fs.existsSync(mainTspPath)) {
|
|
213
|
+
let mainContent = fs.readFileSync(mainTspPath, 'utf-8');
|
|
214
|
+
const importStatement = `import "./modules/${moduleName}/controller.tsp";`;
|
|
215
|
+
|
|
216
|
+
if (!mainContent.includes(importStatement)) {
|
|
217
|
+
const lastImportRegex = /import "\.\/modules\/[^"]+\/controller\.tsp";/g;
|
|
218
|
+
const matches = [...mainContent.matchAll(lastImportRegex)];
|
|
219
|
+
|
|
220
|
+
if (matches.length > 0) {
|
|
221
|
+
const lastMatch = matches[matches.length - 1];
|
|
222
|
+
const insertPosition = lastMatch.index + lastMatch[0].length;
|
|
223
|
+
mainContent = mainContent.slice(0, insertPosition) + '\n' + importStatement + mainContent.slice(insertPosition);
|
|
224
|
+
fs.writeFileSync(mainTspPath, mainContent, 'utf-8');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log(bold(green(`✓ TypeSpec documentation created at docs/src/modules/${moduleName}/`)));
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.log(bold(red(`Error creating TypeSpec docs: ${error.message}`)));
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
95
235
|
import { getCoreUsecaseCreateTest } from './templates/core/use-cases/__tests__/create.spec.mjs';
|
|
96
236
|
import { getCoreUsecaseUpdateTest } from './templates/core/use-cases/__tests__/update.spec.mjs';
|
|
97
237
|
import { getCoreUsecaseDeleteTest } from './templates/core/use-cases/__tests__/delete.spec.mjs';
|
|
@@ -122,7 +262,6 @@ const createModule = async (name) => {
|
|
|
122
262
|
name = getName(name)
|
|
123
263
|
const dirRoot = `${__dirname}/scafold/module/${name}`
|
|
124
264
|
|
|
125
|
-
console.log("dirRoot", dirRoot)
|
|
126
265
|
try {
|
|
127
266
|
if (fs.existsSync(dirRoot)) {
|
|
128
267
|
fs.rmSync(dirRoot, { recursive: true });
|
|
@@ -131,7 +270,9 @@ const createModule = async (name) => {
|
|
|
131
270
|
fs.mkdirSync(dirRoot)
|
|
132
271
|
|
|
133
272
|
fs.writeFileSync(`${dirRoot}/controller.ts`, getModuleControllerModule(name))
|
|
273
|
+
trackFile(`modules/${name}/controller.ts`, 'modules');
|
|
134
274
|
fs.writeFileSync(`${dirRoot}/module.ts`, getModuleModule(name))
|
|
275
|
+
trackFile(`modules/${name}/module.ts`, 'modules');
|
|
135
276
|
|
|
136
277
|
return `${name}`
|
|
137
278
|
} catch (error) {
|
|
@@ -148,7 +289,6 @@ const createInfra = async (name) => {
|
|
|
148
289
|
name = getName(name)
|
|
149
290
|
const dirRoot = `${__dirname}/scafold/infra/${name}`
|
|
150
291
|
|
|
151
|
-
console.log("dirRoot", dirRoot)
|
|
152
292
|
try {
|
|
153
293
|
if (fs.existsSync(dirRoot)) {
|
|
154
294
|
fs.rmSync(dirRoot, { recursive: true });
|
|
@@ -157,9 +297,13 @@ const createInfra = async (name) => {
|
|
|
157
297
|
fs.mkdirSync(dirRoot)
|
|
158
298
|
|
|
159
299
|
fs.writeFileSync(`${dirRoot}/adapter.ts`, getAdapterInfra(name))
|
|
300
|
+
trackFile(`infra/${name}/adapter.ts`, 'infra');
|
|
160
301
|
fs.writeFileSync(`${dirRoot}/index.ts`, getIndexInfra(name))
|
|
302
|
+
trackFile(`infra/${name}/index.ts`, 'infra');
|
|
161
303
|
fs.writeFileSync(`${dirRoot}/module.ts`, getModuleInfa(name))
|
|
304
|
+
trackFile(`infra/${name}/module.ts`, 'infra');
|
|
162
305
|
fs.writeFileSync(`${dirRoot}/service.ts`, getServiceInfra(name))
|
|
306
|
+
trackFile(`infra/${name}/service.ts`, 'infra');
|
|
163
307
|
|
|
164
308
|
return `${name}`
|
|
165
309
|
} catch (error) {
|
|
@@ -184,9 +328,13 @@ const createLib = async (name) => {
|
|
|
184
328
|
fs.mkdirSync(dirRoot)
|
|
185
329
|
|
|
186
330
|
fs.writeFileSync(`${dirRoot}/adapter.ts`, getAdapterLib(name))
|
|
331
|
+
trackFile(`libs/${name}/adapter.ts`, 'libs');
|
|
187
332
|
fs.writeFileSync(`${dirRoot}/index.ts`, getIndexLib(name))
|
|
333
|
+
trackFile(`libs/${name}/index.ts`, 'libs');
|
|
188
334
|
fs.writeFileSync(`${dirRoot}/module.ts`, getModuleLib(name))
|
|
335
|
+
trackFile(`libs/${name}/module.ts`, 'libs');
|
|
189
336
|
fs.writeFileSync(`${dirRoot}/service.ts`, getServiceLib(name))
|
|
337
|
+
trackFile(`libs/${name}/service.ts`, 'libs');
|
|
190
338
|
|
|
191
339
|
return `${name}`
|
|
192
340
|
} catch (error) {
|
|
@@ -221,22 +369,34 @@ const createCore = async (name) => {
|
|
|
221
369
|
fs.mkdirSync(useCasesPath)
|
|
222
370
|
|
|
223
371
|
fs.writeFileSync(`${entityPath}/${name}.ts`, getCoreEntity(name))
|
|
372
|
+
trackFile(`${entityPath}/${name}.ts`, 'core/entity');
|
|
373
|
+
|
|
224
374
|
fs.writeFileSync(`${repositoryPath}/${name}.ts`, getCoreRepository(name))
|
|
225
|
-
|
|
375
|
+
trackFile(`${repositoryPath}/${name}.ts`, 'core/repository');
|
|
226
376
|
|
|
227
377
|
fs.writeFileSync(`${useCasesPath}/${name}-create.ts`, getCoreUsecaseCreate(name))
|
|
378
|
+
trackFile(`${useCasesPath}/${name}-create.ts`, 'core/use-cases');
|
|
228
379
|
fs.writeFileSync(`${useCasesPath}/${name}-delete.ts`, getCoreUsecaseDelete(name))
|
|
380
|
+
trackFile(`${useCasesPath}/${name}-delete.ts`, 'core/use-cases');
|
|
229
381
|
fs.writeFileSync(`${useCasesPath}/${name}-get-by-id.ts`, getCoreUsecaseGetById(name))
|
|
382
|
+
trackFile(`${useCasesPath}/${name}-get-by-id.ts`, 'core/use-cases');
|
|
230
383
|
fs.writeFileSync(`${useCasesPath}/${name}-list.ts`, getCoreUsecaseList(name))
|
|
384
|
+
trackFile(`${useCasesPath}/${name}-list.ts`, 'core/use-cases');
|
|
231
385
|
fs.writeFileSync(`${useCasesPath}/${name}-update.ts`, getCoreUsecaseUpdate(name))
|
|
386
|
+
trackFile(`${useCasesPath}/${name}-update.ts`, 'core/use-cases');
|
|
232
387
|
|
|
233
388
|
const useCasesPathTest = `${useCasesPath}/__tests__`
|
|
234
389
|
fs.mkdirSync(useCasesPathTest)
|
|
235
390
|
fs.writeFileSync(`${useCasesPathTest}/${name}-create.spec.ts`, getCoreUsecaseCreateTest(name))
|
|
391
|
+
trackFile(`${useCasesPathTest}/${name}-create.spec.ts`, 'core/tests');
|
|
236
392
|
fs.writeFileSync(`${useCasesPathTest}/${name}-update.spec.ts`, getCoreUsecaseUpdateTest(name))
|
|
393
|
+
trackFile(`${useCasesPathTest}/${name}-update.spec.ts`, 'core/tests');
|
|
237
394
|
fs.writeFileSync(`${useCasesPathTest}/${name}-delete.spec.ts`, getCoreUsecaseDeleteTest(name))
|
|
395
|
+
trackFile(`${useCasesPathTest}/${name}-delete.spec.ts`, 'core/tests');
|
|
238
396
|
fs.writeFileSync(`${useCasesPathTest}/${name}-list.spec.ts`, getCoreUsecaseListTest(name))
|
|
397
|
+
trackFile(`${useCasesPathTest}/${name}-list.spec.ts`, 'core/tests');
|
|
239
398
|
fs.writeFileSync(`${useCasesPathTest}/${name}-get-by-id.spec.ts`, getCoreUsecaseGetByIdTest(name))
|
|
399
|
+
trackFile(`${useCasesPathTest}/${name}-get-by-id.spec.ts`, 'core/tests');
|
|
240
400
|
} catch (error) {
|
|
241
401
|
console.log('error', error)
|
|
242
402
|
if (fs.existsSync(dirRoot)) {
|
|
@@ -305,13 +465,18 @@ const createPostgresCrud = async (name) => {
|
|
|
305
465
|
}
|
|
306
466
|
fs.mkdirSync(schemasPath)
|
|
307
467
|
fs.writeFileSync(`${schemasPath}/${name}.ts`, getModuleSchema(name))
|
|
468
|
+
trackFile(`schemas/${name}.ts`, 'schemas');
|
|
308
469
|
|
|
309
470
|
const modulesPath = `${dirRoot}/modules/${name}`;
|
|
310
471
|
fs.mkdirSync(modulesPath)
|
|
311
472
|
fs.writeFileSync(`${modulesPath}/adapter.ts`, getModuleAdapter(name))
|
|
473
|
+
trackFile(`modules/${name}/adapter.ts`, 'modules');
|
|
312
474
|
fs.writeFileSync(`${modulesPath}/controller.ts`, getModuleController(name))
|
|
475
|
+
trackFile(`modules/${name}/controller.ts`, 'modules');
|
|
313
476
|
fs.writeFileSync(`${modulesPath}/module.ts`, getModule(name))
|
|
477
|
+
trackFile(`modules/${name}/module.ts`, 'modules');
|
|
314
478
|
fs.writeFileSync(`${modulesPath}/repository.ts`, getModuleRepository(name))
|
|
479
|
+
trackFile(`modules/${name}/repository.ts`, 'modules');
|
|
315
480
|
|
|
316
481
|
await createCore(name)
|
|
317
482
|
|
|
@@ -350,14 +515,19 @@ const createMongoCrud = async (name) => {
|
|
|
350
515
|
|
|
351
516
|
fs.mkdirSync(schemasPath)
|
|
352
517
|
fs.writeFileSync(`${schemasPath}/${name}.ts`, getModuleSchemaMongo(name))
|
|
518
|
+
trackFile(`schemas/${name}.ts`, 'schemas');
|
|
353
519
|
|
|
354
520
|
const modulesPath = `${dirRoot}/modules/${name}`;
|
|
355
521
|
fs.mkdirSync(modulesPath)
|
|
356
522
|
|
|
357
523
|
fs.writeFileSync(`${modulesPath}/adapter.ts`, getModuleAdapterMongo(name))
|
|
524
|
+
trackFile(`modules/${name}/adapter.ts`, 'modules');
|
|
358
525
|
fs.writeFileSync(`${modulesPath}/controller.ts`, getModuleControllerMongo(name))
|
|
526
|
+
trackFile(`modules/${name}/controller.ts`, 'modules');
|
|
359
527
|
fs.writeFileSync(`${modulesPath}/module.ts`, getModuleMongo(name))
|
|
528
|
+
trackFile(`modules/${name}/module.ts`, 'modules');
|
|
360
529
|
fs.writeFileSync(`${modulesPath}/repository.ts`, getModuleRepositoryMongo(name))
|
|
530
|
+
trackFile(`modules/${name}/repository.ts`, 'modules');
|
|
361
531
|
|
|
362
532
|
await createCore(name)
|
|
363
533
|
|
|
@@ -450,6 +620,7 @@ export async function cli(args) {
|
|
|
450
620
|
|
|
451
621
|
try {
|
|
452
622
|
|
|
623
|
+
// DESTINATION PATH
|
|
453
624
|
const dest = path.resolve(`${__dirname}/../../../../`)
|
|
454
625
|
const src = paths[0]
|
|
455
626
|
|
|
@@ -566,6 +737,7 @@ export async function cli(args) {
|
|
|
566
737
|
// Add module to the appropriate module file
|
|
567
738
|
if (userInput.type === 'postgres:crud' || userInput.type === 'mongo:crud') {
|
|
568
739
|
addModuleToAppModule(dest, name, `@/modules/${name}/module`, 'app.module.ts', 'Module');
|
|
740
|
+
createTypeSpecDocs(dest, name);
|
|
569
741
|
} else if (userInput.type === 'module') {
|
|
570
742
|
addModuleToAppModule(dest, name, `@/modules/${name}/module`, 'app.module.ts', 'Module');
|
|
571
743
|
} else if (userInput.type === 'lib') {
|
|
@@ -574,7 +746,7 @@ export async function cli(args) {
|
|
|
574
746
|
addModuleToAppModule(dest, name, `./${name}`, 'infra/module.ts', 'Module');
|
|
575
747
|
}
|
|
576
748
|
|
|
577
|
-
|
|
749
|
+
printCreatedFiles(name, userInput.type);
|
|
578
750
|
});
|
|
579
751
|
|
|
580
752
|
} catch (error) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import pluralize from 'pluralize'
|
|
2
|
-
import { dashToPascal
|
|
2
|
+
import { dashToPascal } from '../../../textUtils.mjs'
|
|
3
3
|
|
|
4
4
|
const getModuleController = (name) => `import { Controller, Delete, Get, HttpCode, Post, Put, Req, Version } from '@nestjs/common';
|
|
5
5
|
|
|
@@ -8,7 +8,6 @@ import { ${dashToPascal(name)}DeleteInput, ${dashToPascal(name)}DeleteOutput } f
|
|
|
8
8
|
import { ${dashToPascal(name)}GetByIdInput, ${dashToPascal(name)}GetByIdOutput } from '@/core/${name}/use-cases/${name}-get-by-id';
|
|
9
9
|
import { ${dashToPascal(name)}ListInput, ${dashToPascal(name)}ListOutput } from '@/core/${name}/use-cases/${name}-list';
|
|
10
10
|
import { ${dashToPascal(name)}UpdateInput, ${dashToPascal(name)}UpdateOutput } from '@/core/${name}/use-cases/${name}-update';
|
|
11
|
-
import { Permission } from '@/utils/decorators';
|
|
12
11
|
import { ApiRequest } from '@/utils/request';
|
|
13
12
|
import { SearchHttpSchema } from '@/utils/search';
|
|
14
13
|
import { SortHttpSchema } from '@/utils/sort';
|
|
@@ -27,7 +26,6 @@ export class ${dashToPascal(name)}Controller {
|
|
|
27
26
|
|
|
28
27
|
@Post()
|
|
29
28
|
@Version('1')
|
|
30
|
-
@Permission('${snakeToCamel(name)}:create')
|
|
31
29
|
@HttpCode(201)
|
|
32
30
|
async create(@Req() { body }: ApiRequest): Promise<${dashToPascal(name)}CreateOutput> {
|
|
33
31
|
return await this.createUsecase.execute(body as ${dashToPascal(name)}CreateInput);
|
|
@@ -35,21 +33,18 @@ export class ${dashToPascal(name)}Controller {
|
|
|
35
33
|
|
|
36
34
|
@Put(':id')
|
|
37
35
|
@Version('1')
|
|
38
|
-
@Permission('${snakeToCamel(name)}:update')
|
|
39
36
|
async update(@Req() { body, params }: ApiRequest): Promise<${dashToPascal(name)}UpdateOutput> {
|
|
40
37
|
return await this.updateUsecase.execute({ ...body, id: params.id } as ${dashToPascal(name)}UpdateInput);
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
@Get(':id')
|
|
44
41
|
@Version('1')
|
|
45
|
-
@Permission('${snakeToCamel(name)}:getbyid')
|
|
46
42
|
async getById(@Req() { params }: ApiRequest): Promise<${dashToPascal(name)}GetByIdOutput> {
|
|
47
43
|
return await this.getByIdUsecase.execute(params as ${dashToPascal(name)}GetByIdInput);
|
|
48
44
|
}
|
|
49
45
|
|
|
50
46
|
@Get()
|
|
51
47
|
@Version('1')
|
|
52
|
-
@Permission('${snakeToCamel(name)}:list')
|
|
53
48
|
async list(@Req() { query }: ApiRequest): Promise<${dashToPascal(name)}ListOutput> {
|
|
54
49
|
const input: ${dashToPascal(name)}ListInput = {
|
|
55
50
|
sort: SortHttpSchema.parse(query.sort),
|
|
@@ -63,7 +58,6 @@ export class ${dashToPascal(name)}Controller {
|
|
|
63
58
|
|
|
64
59
|
@Delete(':id')
|
|
65
60
|
@Version('1')
|
|
66
|
-
@Permission('${snakeToCamel(name)}:delete')
|
|
67
61
|
async delete(@Req() { params }: ApiRequest): Promise<${dashToPascal(name)}DeleteOutput> {
|
|
68
62
|
return await this.deleteUsecase.execute(params as ${dashToPascal(name)}DeleteInput);
|
|
69
63
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import pluralize from 'pluralize'
|
|
2
|
-
import { dashToPascal
|
|
2
|
+
import { dashToPascal } from '../../../textUtils.mjs'
|
|
3
3
|
|
|
4
4
|
const getModuleController = (name) => `import { Controller, Delete, Get, HttpCode, Post, Put, Req, Version } from '@nestjs/common';
|
|
5
5
|
|
|
@@ -8,7 +8,6 @@ import { ${dashToPascal(name)}DeleteInput, ${dashToPascal(name)}DeleteOutput } f
|
|
|
8
8
|
import { ${dashToPascal(name)}GetByIdInput, ${dashToPascal(name)}GetByIdOutput } from '@/core/${name}/use-cases/${name}-get-by-id';
|
|
9
9
|
import { ${dashToPascal(name)}ListInput, ${dashToPascal(name)}ListOutput } from '@/core/${name}/use-cases/${name}-list';
|
|
10
10
|
import { ${dashToPascal(name)}UpdateInput, ${dashToPascal(name)}UpdateOutput } from '@/core/${name}/use-cases/${name}-update';
|
|
11
|
-
import { Permission } from '@/utils/decorators';
|
|
12
11
|
import { ApiRequest } from '@/utils/request';
|
|
13
12
|
import { SearchHttpSchema } from '@/utils/search';
|
|
14
13
|
import { SortHttpSchema } from '@/utils/sort';
|
|
@@ -27,7 +26,6 @@ export class ${dashToPascal(name)}Controller {
|
|
|
27
26
|
|
|
28
27
|
@Post()
|
|
29
28
|
@Version('1')
|
|
30
|
-
@Permission('${snakeToCamel(name)}:create')
|
|
31
29
|
@HttpCode(201)
|
|
32
30
|
async create(@Req() { body }: ApiRequest): Promise<${dashToPascal(name)}CreateOutput> {
|
|
33
31
|
return await this.createUsecase.execute(body as ${dashToPascal(name)}CreateInput);
|
|
@@ -35,21 +33,18 @@ export class ${dashToPascal(name)}Controller {
|
|
|
35
33
|
|
|
36
34
|
@Put(':id')
|
|
37
35
|
@Version('1')
|
|
38
|
-
@Permission('${snakeToCamel(name)}:update')
|
|
39
36
|
async update(@Req() { body, params }: ApiRequest): Promise<${dashToPascal(name)}UpdateOutput> {
|
|
40
37
|
return await this.updateUsecase.execute({ ...body, id: params.id } as ${dashToPascal(name)}UpdateInput);
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
@Get(':id')
|
|
44
41
|
@Version('1')
|
|
45
|
-
@Permission('${snakeToCamel(name)}:getbyid')
|
|
46
42
|
async getById(@Req() { params }: ApiRequest): Promise<${dashToPascal(name)}GetByIdOutput> {
|
|
47
43
|
return await this.getByIdUsecase.execute(params as ${dashToPascal(name)}GetByIdInput);
|
|
48
44
|
}
|
|
49
45
|
|
|
50
46
|
@Get()
|
|
51
47
|
@Version('1')
|
|
52
|
-
@Permission('${snakeToCamel(name)}:list')
|
|
53
48
|
async list(@Req() { query }: ApiRequest): Promise<${dashToPascal(name)}ListOutput> {
|
|
54
49
|
const input: ${dashToPascal(name)}ListInput = {
|
|
55
50
|
sort: SortHttpSchema.parse(query.sort),
|
|
@@ -63,7 +58,6 @@ export class ${dashToPascal(name)}Controller {
|
|
|
63
58
|
|
|
64
59
|
@Delete(':id')
|
|
65
60
|
@Version('1')
|
|
66
|
-
@Permission('${snakeToCamel(name)}:delete')
|
|
67
61
|
async delete(@Req() { params }: ApiRequest): Promise<${dashToPascal(name)}DeleteOutput> {
|
|
68
62
|
return await this.deleteUsecase.execute(params as ${dashToPascal(name)}DeleteInput);
|
|
69
63
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import pluralize from 'pluralize';
|
|
2
|
+
|
|
3
|
+
const dashToPascal = (string) =>
|
|
4
|
+
string
|
|
5
|
+
.split("-")
|
|
6
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
7
|
+
.join("");
|
|
8
|
+
|
|
9
|
+
export const getTypeSpecController = (name) => {
|
|
10
|
+
const pascalName = dashToPascal(name);
|
|
11
|
+
|
|
12
|
+
return `import "@typespec/http";
|
|
13
|
+
import "@typespec/rest";
|
|
14
|
+
import "@typespec/openapi3";
|
|
15
|
+
import "@typespec/versioning";
|
|
16
|
+
import "../../utils/model.tsp";
|
|
17
|
+
import "./model.tsp";
|
|
18
|
+
import "./exception.tsp";
|
|
19
|
+
|
|
20
|
+
using TypeSpec.Http;
|
|
21
|
+
using TypeSpec.Versioning;
|
|
22
|
+
using OpenAPI;
|
|
23
|
+
using Utils.Model;
|
|
24
|
+
using Utils.Versioning;
|
|
25
|
+
|
|
26
|
+
@service(#{
|
|
27
|
+
title: "${pascalName}",
|
|
28
|
+
})
|
|
29
|
+
namespace api.${pascalName};
|
|
30
|
+
|
|
31
|
+
@tag("${pascalName}")
|
|
32
|
+
@route("api/{version}/${pluralize(name)}")
|
|
33
|
+
@useAuth(BearerAuth)
|
|
34
|
+
interface ${pascalName}Controller {
|
|
35
|
+
@post
|
|
36
|
+
@doc("create ${name}")
|
|
37
|
+
@returnsDoc("${name} created successfully")
|
|
38
|
+
create(...VersionParams, @body body: CreateInput): CreateOutput | CreateValidationException;
|
|
39
|
+
|
|
40
|
+
@put
|
|
41
|
+
@doc("update ${name}")
|
|
42
|
+
@returnsDoc("${name} updated successfully")
|
|
43
|
+
update(
|
|
44
|
+
...VersionParams,
|
|
45
|
+
...UpdateParamInput,
|
|
46
|
+
@body body: UpdateInput,
|
|
47
|
+
): UpdateOutput | UpdateValidationException | UpdateNotFoundException;
|
|
48
|
+
|
|
49
|
+
@get
|
|
50
|
+
@doc("get ${name} by id")
|
|
51
|
+
@returnsDoc("get ${name} by id successfully")
|
|
52
|
+
getById(
|
|
53
|
+
...VersionParams,
|
|
54
|
+
...GetByIdParamInput,
|
|
55
|
+
): GetByIdOutput | GetByIdValidationException | GetByIdNotFoundException;
|
|
56
|
+
|
|
57
|
+
@get
|
|
58
|
+
@doc("list ${name}")
|
|
59
|
+
@returnsDoc("list ${name} successfully")
|
|
60
|
+
list(...VersionParams, ...ListQueryInput): ListOutput;
|
|
61
|
+
|
|
62
|
+
@delete
|
|
63
|
+
@doc("delete ${name}")
|
|
64
|
+
@returnsDoc("${name} deleted successfully")
|
|
65
|
+
delete(...VersionParams, ...DeleteParamInput): DeleteOutput | DeleteValidationException | DeleteNotFoundException;
|
|
66
|
+
}
|
|
67
|
+
`;
|
|
68
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const dashToPascal = (string) =>
|
|
2
|
+
string
|
|
3
|
+
.split("-")
|
|
4
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
5
|
+
.join("");
|
|
6
|
+
|
|
7
|
+
export const getTypeSpecException = (name) => {
|
|
8
|
+
const pascalName = dashToPascal(name);
|
|
9
|
+
const camelName = name.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
10
|
+
|
|
11
|
+
return `import "../../utils/model.tsp";
|
|
12
|
+
import "../../utils/exceptions.tsp";
|
|
13
|
+
import "@typespec/http";
|
|
14
|
+
import "@typespec/versioning";
|
|
15
|
+
|
|
16
|
+
using TypeSpec.Http;
|
|
17
|
+
using TypeSpec.Versioning;
|
|
18
|
+
using Utils.Model;
|
|
19
|
+
using Utils.Exception;
|
|
20
|
+
using Utils.Versioning;
|
|
21
|
+
|
|
22
|
+
namespace api.${pascalName};
|
|
23
|
+
|
|
24
|
+
// CREATE //
|
|
25
|
+
@doc("When input is invalid")
|
|
26
|
+
@error
|
|
27
|
+
model CreateValidationError extends ApiBadRequestException<"name: Required"> {
|
|
28
|
+
@statusCode statusCode: 400;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
alias CreateValidationException = CreateValidationError;
|
|
32
|
+
|
|
33
|
+
// UPDATE //
|
|
34
|
+
@doc("When input is invalid")
|
|
35
|
+
@error
|
|
36
|
+
model UpdateValidationError extends ApiBadRequestException<"id: Required"> {
|
|
37
|
+
@statusCode statusCode: 400;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
alias UpdateValidationException = UpdateValidationError;
|
|
41
|
+
|
|
42
|
+
@doc("When ${name} not found")
|
|
43
|
+
@error
|
|
44
|
+
model Update${pascalName}NotFoundError extends ApiNotFoundException<"${camelName}NotFound"> {
|
|
45
|
+
@statusCode statusCode: 404;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
alias UpdateNotFoundException = Update${pascalName}NotFoundError;
|
|
49
|
+
|
|
50
|
+
// GET BY ID //
|
|
51
|
+
@doc("When input is invalid")
|
|
52
|
+
@error
|
|
53
|
+
model GetByIdValidationError extends ApiBadRequestException<"id: Required"> {
|
|
54
|
+
@statusCode statusCode: 400;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
alias GetByIdValidationException = GetByIdValidationError;
|
|
58
|
+
|
|
59
|
+
@doc("When ${name} not found")
|
|
60
|
+
@error
|
|
61
|
+
model GetById${pascalName}NotFoundError extends ApiNotFoundException<"${camelName}NotFound"> {
|
|
62
|
+
@statusCode statusCode: 404;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
alias GetByIdNotFoundException = GetById${pascalName}NotFoundError;
|
|
66
|
+
|
|
67
|
+
// DELETE //
|
|
68
|
+
@doc("When input is invalid")
|
|
69
|
+
@error
|
|
70
|
+
model DeleteValidationError extends ApiBadRequestException<"id: Required"> {
|
|
71
|
+
@statusCode statusCode: 400;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
alias DeleteValidationException = DeleteValidationError;
|
|
75
|
+
|
|
76
|
+
@doc("When ${name} not found")
|
|
77
|
+
@error
|
|
78
|
+
model Delete${pascalName}NotFoundError extends ApiNotFoundException<"${camelName}NotFound"> {
|
|
79
|
+
@statusCode statusCode: 404;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
alias DeleteNotFoundException = Delete${pascalName}NotFoundError;
|
|
83
|
+
`;
|
|
84
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const dashToPascal = (string) =>
|
|
2
|
+
string
|
|
3
|
+
.split("-")
|
|
4
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
5
|
+
.join("");
|
|
6
|
+
|
|
7
|
+
export const getTypeSpecModel = (name) => {
|
|
8
|
+
const pascalName = dashToPascal(name);
|
|
9
|
+
|
|
10
|
+
return `import "../../utils/model.tsp";
|
|
11
|
+
import "@typespec/http";
|
|
12
|
+
import "@typespec/versioning";
|
|
13
|
+
|
|
14
|
+
using TypeSpec.Http;
|
|
15
|
+
using TypeSpec.Versioning;
|
|
16
|
+
using Utils.Model;
|
|
17
|
+
using Utils.Versioning;
|
|
18
|
+
|
|
19
|
+
namespace api.${pascalName};
|
|
20
|
+
|
|
21
|
+
@doc("${name} base entity")
|
|
22
|
+
model ${pascalName}Entity {
|
|
23
|
+
name: string;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
updatedAt: string;
|
|
26
|
+
deletedAt: string | null = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// CREATE //
|
|
30
|
+
@doc("${name} create input")
|
|
31
|
+
model CreateInput extends PickProperties<${pascalName}Entity, "name"> {}
|
|
32
|
+
@doc("${name} create output")
|
|
33
|
+
model CreateOutput extends ${pascalName}Entity {}
|
|
34
|
+
|
|
35
|
+
// UPDATE //
|
|
36
|
+
model UpdateParamInput {
|
|
37
|
+
@doc("${name} id")
|
|
38
|
+
@path
|
|
39
|
+
id: string;
|
|
40
|
+
}
|
|
41
|
+
@doc("${name} update input")
|
|
42
|
+
model UpdateInput extends PickProperties<${pascalName}Entity, "name"> {}
|
|
43
|
+
@doc("${name} update output")
|
|
44
|
+
model UpdateOutput extends ${pascalName}Entity {}
|
|
45
|
+
|
|
46
|
+
// GET BY ID //
|
|
47
|
+
model GetByIdParamInput {
|
|
48
|
+
@doc("${name} id")
|
|
49
|
+
@path
|
|
50
|
+
id: string;
|
|
51
|
+
}
|
|
52
|
+
@doc("${name} get by id input")
|
|
53
|
+
model GetByIdOutput extends ${pascalName}Entity {}
|
|
54
|
+
|
|
55
|
+
// LIST //
|
|
56
|
+
model ListQueryInput extends ApiPaginationInput {}
|
|
57
|
+
@doc("${name} list output")
|
|
58
|
+
model ListOutput extends ApiPaginationOutput<${pascalName}Entity> {}
|
|
59
|
+
|
|
60
|
+
// DELETE //
|
|
61
|
+
model DeleteParamInput {
|
|
62
|
+
@doc("${name} id")
|
|
63
|
+
@path
|
|
64
|
+
id: string;
|
|
65
|
+
}
|
|
66
|
+
@doc("${name} delete output")
|
|
67
|
+
model DeleteOutput extends OmitProperties<${pascalName}Entity, "deletedAt"> {
|
|
68
|
+
deletedAt: utcDateTime;
|
|
69
|
+
}
|
|
70
|
+
`;
|
|
71
|
+
};
|