alexis-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.
@@ -0,0 +1,439 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /* ======================================================
5
+ Utils
6
+ ====================================================== */
7
+
8
+ const mapSwaggerType = (column) => {
9
+ const type = column.type;
10
+
11
+ if (typeof type === 'string') {
12
+ switch (type) {
13
+ case 'varchar':
14
+ case 'text':
15
+ case 'uuid':
16
+ return { type: 'string' };
17
+
18
+ case 'int':
19
+ case 'integer':
20
+ case 'float':
21
+ case 'double':
22
+ case 'decimal':
23
+ return { type: 'number' };
24
+
25
+ case 'boolean':
26
+ case 'bool':
27
+ return { type: 'boolean' };
28
+
29
+ case 'timestamp':
30
+ case 'datetime':
31
+ case 'date':
32
+ return { type: 'string', format: 'date-time' };
33
+
34
+ default:
35
+ return { type: 'string' };
36
+ }
37
+ }
38
+
39
+ return { type: 'string' };
40
+ };
41
+
42
+ const mapExampleValue = (column) => {
43
+ const type = column.type;
44
+
45
+ if (typeof type === 'string') {
46
+ switch (type) {
47
+ case 'varchar':
48
+ case 'text':
49
+ case 'uuid':
50
+ return 'string';
51
+
52
+ case 'int':
53
+ case 'integer':
54
+ case 'float':
55
+ case 'double':
56
+ case 'decimal':
57
+ return 0;
58
+
59
+ case 'boolean':
60
+ case 'bool':
61
+ return true;
62
+
63
+ case 'timestamp':
64
+ case 'datetime':
65
+ case 'date':
66
+ return '2024-01-01T00:00:00Z';
67
+
68
+ default:
69
+ return 'string';
70
+ }
71
+ }
72
+
73
+ return null;
74
+ };
75
+
76
+ const buildSchemaFromEntity = (entity) => {
77
+ const properties = {};
78
+ const required = [];
79
+
80
+ for (const [field, column] of Object.entries(entity.options.columns)) {
81
+ if (
82
+ column.primary ||
83
+ column.generated ||
84
+ column.createDate ||
85
+ column.updateDate ||
86
+ field === 'password'
87
+ ) continue;
88
+
89
+ properties[field] = mapSwaggerType(column);
90
+
91
+ if (!column.nullable) {
92
+ required.push(field);
93
+ }
94
+ }
95
+
96
+ return {
97
+ type: 'object',
98
+ properties,
99
+ required: required.length ? required : undefined
100
+ };
101
+ };
102
+
103
+ const buildExampleFromEntity = (entity) => {
104
+ const example = {};
105
+
106
+ for (const [field, column] of Object.entries(entity.options.columns)) {
107
+ if (
108
+ column.primary ||
109
+ column.generated ||
110
+ column.createDate ||
111
+ column.updateDate ||
112
+ field === 'password'
113
+ ) continue;
114
+
115
+ example[field] = mapExampleValue(column);
116
+ }
117
+
118
+ return example;
119
+ };
120
+
121
+ /* ======================================================
122
+ Main
123
+ ====================================================== */
124
+
125
+ module.exports = () => {
126
+ const projectRoot = path.join(__dirname, '../..');
127
+ const srcDir = path.join(projectRoot, 'src');
128
+ const swaggerDir = path.join(srcDir, 'swagger');
129
+
130
+ if (!fs.existsSync(swaggerDir)) {
131
+ fs.mkdirSync(swaggerDir, { recursive: true });
132
+ }
133
+
134
+ const swagger = {
135
+ openapi: '3.0.0',
136
+ info: {
137
+ title: 'Express API',
138
+ version: '1.0.0',
139
+ description: 'API générée automatiquement'
140
+ },
141
+ servers: [{ url: 'http://localhost:3000/api/v1' }],
142
+ paths: {},
143
+ components: {
144
+ securitySchemes: {
145
+ bearerAuth: {
146
+ type: 'http',
147
+ scheme: 'bearer',
148
+ bearerFormat: 'JWT'
149
+ }
150
+ },
151
+ schemas: {}
152
+ }
153
+ };
154
+
155
+ /* ========= ENTITIES → SCHEMAS ========= */
156
+
157
+ const entitiesDir = path.join(srcDir, 'entities');
158
+
159
+ fs.readdirSync(entitiesDir).forEach((file) => {
160
+ if (!file.endsWith('.entity.js')) return;
161
+
162
+ let entity;
163
+ try {
164
+ entity = require(path.join(entitiesDir, file));
165
+ } catch {
166
+ console.warn(`⚠️ Cannot load entity ${file}`);
167
+ return;
168
+ }
169
+
170
+ if (entity.options?.swagger === false) return;
171
+
172
+ const entityName = entity.options.name;
173
+ swagger.components.schemas[entityName] = buildSchemaFromEntity(entity);
174
+ });
175
+
176
+ /* ========= MODULES / ROUTES ========= */
177
+
178
+ const generateAuthSwagger = (swagger) => {
179
+ swagger.paths['/auth/register'] = {
180
+ post: {
181
+ tags: ['Auth'],
182
+ summary: 'Register',
183
+ description: 'Create a new user account',
184
+ requestBody: {
185
+ required: true,
186
+ content: {
187
+ 'application/json': {
188
+ schema: {
189
+ type: 'object',
190
+ required: ['email', 'password'],
191
+ properties: {
192
+ email: { type: 'string', example: 'user@example.com' },
193
+ password: { type: 'string', example: 'password123' }
194
+ }
195
+ }
196
+ }
197
+ }
198
+ },
199
+ responses: {
200
+ 201: { description: 'User created' },
201
+ 400: { description: 'User already exists' }
202
+ }
203
+ }
204
+ };
205
+
206
+ swagger.paths['/auth/login'] = {
207
+ post: {
208
+ tags: ['Auth'],
209
+ summary: 'Login',
210
+ description: 'Authenticate user and return JWT token',
211
+ requestBody: {
212
+ required: true,
213
+ content: {
214
+ 'application/json': {
215
+ schema: {
216
+ type: 'object',
217
+ required: ['email', 'password'],
218
+ properties: {
219
+ email: { type: 'string', example: 'user@example.com' },
220
+ password: { type: 'string', example: 'password123' }
221
+ }
222
+ }
223
+ }
224
+ }
225
+ },
226
+ responses: {
227
+ 200: { description: 'JWT token returned' },
228
+ 401: { description: 'Invalid credentials' }
229
+ }
230
+ }
231
+ };
232
+
233
+ swagger.paths['/auth/profile'] = {
234
+ get: {
235
+ tags: ['Auth'],
236
+ summary: 'Profile',
237
+ description: 'Get current authenticated user',
238
+ security: [{ bearerAuth: [] }],
239
+ responses: {
240
+ 200: { description: 'User profile' },
241
+ 401: { description: 'Unauthorized' }
242
+ }
243
+ }
244
+ };
245
+ };
246
+ generateAuthSwagger(swagger);
247
+
248
+ const modulesDir = path.join(srcDir, 'modules/v1');
249
+
250
+ fs.readdirSync(modulesDir).forEach((moduleName) => {
251
+ if (moduleName === 'auth') return;
252
+ const modulePath = path.join(modulesDir, moduleName);
253
+ if (!fs.statSync(modulePath).isDirectory()) return;
254
+
255
+ /* ----- AUTH ----- */
256
+ if (moduleName === 'auth') {
257
+ swagger.paths['/auth/register'] = {
258
+ post: {
259
+ tags: ['Auth'],
260
+ summary: 'Register',
261
+ description: 'Create a new user account',
262
+ requestBody: {
263
+ required: true,
264
+ content: {
265
+ 'application/json': {
266
+ schema: {
267
+ type: 'object',
268
+ properties: {
269
+ email: { type: 'string' },
270
+ password: { type: 'string' }
271
+ }
272
+ },
273
+ example: {
274
+ email: 'user@example.com',
275
+ password: 'password123'
276
+ }
277
+ }
278
+ }
279
+ },
280
+ responses: { 201: { description: 'User created' } }
281
+ }
282
+ };
283
+ return;
284
+ }
285
+
286
+ /* ----- CRUD ----- */
287
+
288
+ const entityName =
289
+ moduleName.charAt(0).toUpperCase() + moduleName.slice(1);
290
+
291
+ const schemaRef = {
292
+ $ref: `#/components/schemas/${entityName}`
293
+ };
294
+
295
+ const entityPath = path.join(srcDir, 'entities', `${entityName}.entity.js`);
296
+ const entity = fs.existsSync(entityPath)
297
+ ? require(entityPath)
298
+ : null;
299
+
300
+ const exampleBody = entity ? buildExampleFromEntity(entity) : {};
301
+
302
+
303
+ const basePath = `/${moduleName}`;
304
+
305
+ swagger.paths[basePath] = {
306
+ get: {
307
+ tags: [entityName],
308
+ summary: `List ${entityName}`,
309
+ description: `Retrieve all ${entityName}`,
310
+ security: [{ bearerAuth: [] }],
311
+ responses: {
312
+ 200: {
313
+ description: 'List retrieved',
314
+ content: {
315
+ 'application/json': {
316
+ schema: {
317
+ type: 'array',
318
+ items: schemaRef
319
+ }
320
+ }
321
+ }
322
+ }
323
+ }
324
+ },
325
+
326
+ post: {
327
+ tags: [entityName],
328
+ summary: `Create ${entityName}`,
329
+ description: `Create a new ${entityName}`,
330
+ security: [{ bearerAuth: [] }],
331
+ requestBody: {
332
+ required: true,
333
+ content: {
334
+ 'application/json': {
335
+ schema: schemaRef,
336
+ example: exampleBody
337
+ }
338
+ }
339
+ },
340
+ responses: {
341
+ 201: {
342
+ description: `${entityName} created`,
343
+ content: {
344
+ 'application/json': {
345
+ schema: schemaRef
346
+ }
347
+ }
348
+ }
349
+ }
350
+ }
351
+ };
352
+ swagger.paths[`${basePath}/{id}`] = {
353
+ get: {
354
+ tags: [entityName],
355
+ summary: `Get ${entityName}`,
356
+ description: `Retrieve a ${entityName} by ID`,
357
+ security: [{ bearerAuth: [] }],
358
+ parameters: [
359
+ {
360
+ name: 'id',
361
+ in: 'path',
362
+ required: true,
363
+ schema: { type: 'number' }
364
+ }
365
+ ],
366
+ responses: {
367
+ 200: {
368
+ description: 'Entity found',
369
+ content: {
370
+ 'application/json': {
371
+ schema: schemaRef
372
+ }
373
+ }
374
+ },
375
+ 404: { description: 'Not found' }
376
+ }
377
+ },
378
+
379
+ put: {
380
+ tags: [entityName],
381
+ summary: `Update ${entityName}`,
382
+ description: `Update an existing ${entityName}`,
383
+ security: [{ bearerAuth: [] }],
384
+ parameters: [
385
+ {
386
+ name: 'id',
387
+ in: 'path',
388
+ required: true,
389
+ schema: { type: 'number' }
390
+ }
391
+ ],
392
+ requestBody: {
393
+ required: true,
394
+ content: {
395
+ 'application/json': {
396
+ schema: schemaRef,
397
+ example: exampleBody
398
+ }
399
+ }
400
+ },
401
+ responses: {
402
+ 200: {
403
+ description: `${entityName} updated`,
404
+ content: {
405
+ 'application/json': {
406
+ schema: schemaRef
407
+ }
408
+ }
409
+ }
410
+ }
411
+ },
412
+
413
+ delete: {
414
+ tags: [entityName],
415
+ summary: `Delete ${entityName}`,
416
+ description: `Delete a ${entityName}`,
417
+ security: [{ bearerAuth: [] }],
418
+ parameters: [
419
+ {
420
+ name: 'id',
421
+ in: 'path',
422
+ required: true,
423
+ schema: { type: 'number' }
424
+ }
425
+ ],
426
+ responses: {
427
+ 204: { description: `${entityName} deleted` }
428
+ }
429
+ }
430
+ };
431
+ });
432
+
433
+ fs.writeFileSync(
434
+ path.join(swaggerDir, 'openapi.json'),
435
+ JSON.stringify(swagger, null, 2)
436
+ );
437
+
438
+ console.log('✅ Swagger generated at src/swagger/openapi.json');
439
+ };
@@ -0,0 +1,15 @@
1
+ const { execSync } = require('child_process');
2
+
3
+ exports.run = () => {
4
+ execSync(
5
+ `npx typeorm migration:run -d src/config/orm.js`,
6
+ { stdio: 'inherit' }
7
+ );
8
+ };
9
+
10
+ exports.rollback = () => {
11
+ execSync(
12
+ `npx typeorm migration:revert -d src/config/orm.js`,
13
+ { stdio: 'inherit' }
14
+ );
15
+ };
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "alexis-cli",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "bin": {
6
+ "alexis-cli": "bin/alexis.js"
7
+ },
8
+ "keywords": ["express", "cli", "swagger", "crud", "typeorm"],
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "author": "Alexis",
13
+ "license": "MIT",
14
+ "description": "CLI pour générer des APIs Express (modules, CRUD, auth, swagger)"
15
+ }