exp-node-server 1.1.7 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +162 -20
- package/dist/cli.js +613 -0
- package/package.json +29 -9
- package/templates/mongoose/.dockerignore +10 -0
- package/templates/mongoose/.env.example +56 -7
- package/templates/mongoose/.prettierignore +4 -0
- package/templates/mongoose/.prettierrc +7 -0
- package/templates/mongoose/Dockerfile +33 -0
- package/templates/mongoose/_gitignore +35 -0
- package/templates/mongoose/docker-compose.yml +21 -0
- package/templates/mongoose/eslint.config.js +35 -0
- package/templates/mongoose/package-lock.json +6366 -0
- package/templates/mongoose/package.json +49 -11
- package/templates/mongoose/readme.md +88 -0
- package/templates/mongoose/src/app/configs/index.ts +178 -26
- package/templates/mongoose/src/app/errors/zodError.ts +16 -16
- package/templates/mongoose/src/app/lib/db.ts +18 -0
- package/templates/mongoose/src/app/lib/logger.ts +12 -0
- package/templates/mongoose/src/app/middlewares/auth.ts +48 -44
- package/templates/mongoose/src/app/middlewares/global_error_handler.ts +59 -65
- package/templates/mongoose/src/app/middlewares/not_found_api.ts +7 -7
- package/templates/mongoose/src/app/middlewares/rate_limiter.ts +153 -0
- package/templates/mongoose/src/app/middlewares/request_validator.ts +11 -10
- package/templates/mongoose/src/app/middlewares/uploader.ts +17 -13
- package/templates/mongoose/src/app/modules/account/account.controller.ts +156 -0
- package/templates/mongoose/src/app/modules/account/account.interface.ts +65 -0
- package/templates/mongoose/src/app/modules/account/account.route.ts +101 -0
- package/templates/mongoose/src/app/modules/account/account.schema.ts +97 -0
- package/templates/mongoose/src/app/modules/account/account.service.ts +422 -0
- package/templates/mongoose/src/app/modules/account/account.swagger.ts +278 -0
- package/templates/mongoose/src/app/modules/account/account.validation.ts +63 -0
- package/templates/mongoose/src/app/modules/profile/profile.controller.ts +17 -0
- package/templates/mongoose/src/app/modules/profile/profile.interface.ts +7 -0
- package/templates/mongoose/src/app/modules/profile/profile.route.ts +28 -0
- package/templates/mongoose/src/app/modules/profile/profile.schema.ts +13 -0
- package/templates/mongoose/src/app/modules/profile/profile.service.ts +28 -0
- package/templates/mongoose/src/app/modules/profile/profile.swagger.ts +34 -0
- package/templates/mongoose/src/app/modules/profile/profile.validation.ts +8 -0
- package/templates/mongoose/src/app/passport-js/index.ts +87 -0
- package/templates/mongoose/src/app/passport-js/oauth-state.ts +150 -0
- package/templates/mongoose/src/app/passport-js/providers.ts +258 -0
- package/templates/mongoose/src/app/passport-js/social-account.ts +141 -0
- package/templates/mongoose/src/app/queues/agenda.ts +36 -0
- package/templates/mongoose/src/app/queues/email.queue.ts +81 -0
- package/templates/mongoose/src/app/queues/index.ts +17 -0
- package/templates/mongoose/src/app/templates/otpEmailTemplate.ts +185 -0
- package/templates/mongoose/src/app/templates/resetPasswordEmailTemplate.ts +157 -0
- package/templates/mongoose/src/app/types/error.d.ts +7 -7
- package/templates/mongoose/src/app/types/express.d.ts +11 -0
- package/templates/mongoose/src/app/types/passport-oauth1.d.ts +51 -0
- package/templates/mongoose/src/app/utils/JWT.ts +38 -18
- package/templates/mongoose/src/app/utils/app_error.ts +10 -10
- package/templates/mongoose/src/app/utils/auth_cookie.ts +27 -0
- package/templates/mongoose/src/app/utils/catch_async.ts +8 -8
- package/templates/mongoose/src/app/utils/cloudinary.ts +26 -59
- package/templates/mongoose/src/app/utils/mail_sender.ts +26 -92
- package/templates/mongoose/src/app/utils/manage_response.ts +19 -19
- package/templates/mongoose/src/app/utils/otp.ts +20 -0
- package/templates/mongoose/src/app/utils/session.ts +55 -0
- package/templates/mongoose/src/app.ts +73 -42
- package/templates/mongoose/src/routes.ts +13 -13
- package/templates/mongoose/src/server.ts +38 -18
- package/templates/mongoose/src/swaggerOptions.ts +23 -23
- package/templates/mongoose/tests/app.test.ts +97 -0
- package/templates/mongoose/tests/global-setup.ts +15 -0
- package/templates/mongoose/tests/helpers/fixtures.ts +42 -0
- package/templates/mongoose/tests/queues/email.queue.test.ts +137 -0
- package/templates/mongoose/tests/routes/account.test.ts +614 -0
- package/templates/mongoose/tests/routes/profile.test.ts +95 -0
- package/templates/mongoose/tests/routes/social.test.ts +310 -0
- package/templates/mongoose/tests/setup.ts +25 -0
- package/templates/mongoose/tsconfig.json +16 -20
- package/templates/mongoose/vitest.config.ts +43 -0
- package/templates/prisma/.dockerignore +2 -0
- package/templates/prisma/.env.example +62 -6
- package/templates/prisma/.prettierignore +4 -0
- package/templates/prisma/.prettierrc +7 -0
- package/templates/prisma/.projectrc.json +1 -1
- package/templates/prisma/Dockerfile +21 -7
- package/templates/prisma/_gitignore +38 -0
- package/templates/prisma/docker-compose.yml +8 -7
- package/templates/prisma/eslint.config.js +35 -0
- package/templates/prisma/package-lock.json +7316 -0
- package/templates/prisma/package.json +47 -10
- package/templates/prisma/prisma/schema/account.schema.prisma +92 -10
- package/templates/prisma/prisma/schema/profile.schema.prisma +8 -3
- package/templates/prisma/prisma/schema/schema.prisma +1 -2
- package/templates/prisma/prisma.config.ts +5 -5
- package/templates/prisma/readme.md +90 -0
- package/templates/prisma/src/app/configs/index.ts +175 -26
- package/templates/prisma/src/app/errors/zodError.ts +16 -16
- package/templates/prisma/src/app/lib/logger.ts +12 -0
- package/templates/prisma/src/app/lib/prisma.ts +4 -6
- package/templates/prisma/src/app/middlewares/auth.ts +37 -16
- package/templates/prisma/src/app/middlewares/global_error_handler.ts +42 -35
- package/templates/prisma/src/app/middlewares/not_found_api.ts +7 -7
- package/templates/prisma/src/app/middlewares/rate_limiter.ts +124 -0
- package/templates/prisma/src/app/middlewares/request_validator.ts +4 -3
- package/templates/prisma/src/app/middlewares/uploader.ts +17 -13
- package/templates/prisma/src/app/modules/account/account.controller.ts +74 -34
- package/templates/prisma/src/app/modules/account/account.route.ts +68 -22
- package/templates/prisma/src/app/modules/account/account.service.ts +329 -309
- package/templates/prisma/src/app/modules/account/account.swagger.ts +167 -76
- package/templates/prisma/src/app/modules/account/account.validation.ts +41 -19
- package/templates/prisma/src/app/modules/profile/profile.controller.ts +4 -4
- package/templates/prisma/src/app/modules/profile/profile.route.ts +18 -12
- package/templates/prisma/src/app/modules/profile/profile.service.ts +11 -18
- package/templates/prisma/src/app/modules/profile/profile.swagger.ts +11 -11
- package/templates/prisma/src/app/modules/profile/profile.validation.ts +2 -2
- package/templates/prisma/src/app/passport-js/index.ts +87 -0
- package/templates/prisma/src/app/passport-js/oauth-state.ts +150 -0
- package/templates/prisma/src/app/passport-js/providers.ts +263 -0
- package/templates/prisma/src/app/passport-js/social-account.ts +147 -0
- package/templates/prisma/src/app/queues/boss.ts +33 -0
- package/templates/prisma/src/app/queues/email.queue.ts +71 -0
- package/templates/prisma/src/app/queues/index.ts +23 -0
- package/templates/prisma/src/app/queues/maintenance.queue.ts +30 -0
- package/templates/prisma/src/app/templates/otpEmailTemplate.ts +185 -0
- package/templates/prisma/src/app/templates/resetPasswordEmailTemplate.ts +157 -0
- package/templates/prisma/src/app/types/error.d.ts +7 -7
- package/templates/prisma/src/app/types/express.d.ts +11 -0
- package/templates/prisma/src/app/types/passport-oauth1.d.ts +51 -0
- package/templates/prisma/src/app/utils/JWT.ts +36 -17
- package/templates/prisma/src/app/utils/app_error.ts +10 -10
- package/templates/prisma/src/app/utils/auth_cookie.ts +27 -0
- package/templates/prisma/src/app/utils/catch_async.ts +8 -8
- package/templates/prisma/src/app/utils/cloudinary.ts +26 -59
- package/templates/prisma/src/app/utils/mail_sender.ts +26 -92
- package/templates/prisma/src/app/utils/manage_response.ts +19 -19
- package/templates/prisma/src/app/utils/otp.ts +20 -0
- package/templates/prisma/src/app/utils/session.ts +57 -0
- package/templates/prisma/src/app.ts +73 -42
- package/templates/prisma/src/routes.ts +12 -10
- package/templates/prisma/src/server.ts +37 -19
- package/templates/prisma/src/swaggerOptions.ts +22 -26
- package/templates/prisma/tests/app.test.ts +86 -0
- package/templates/prisma/tests/helpers/fixtures.ts +35 -0
- package/templates/prisma/tests/helpers/prisma.ts +32 -0
- package/templates/prisma/tests/queues/email.queue.test.ts +162 -0
- package/templates/prisma/tests/routes/account.test.ts +627 -0
- package/templates/prisma/tests/routes/profile.test.ts +91 -0
- package/templates/prisma/tests/routes/social.test.ts +317 -0
- package/templates/prisma/tests/setup.ts +10 -0
- package/templates/prisma/tsconfig.json +16 -23
- package/templates/prisma/vitest.config.ts +38 -0
- package/bin/cli.js +0 -41
- package/src/createProject.js +0 -61
- package/src/generateModule.js +0 -93
- package/src/utils/getProjectType.js +0 -13
- package/src/utils/mongoose/controllerGenerator.js +0 -70
- package/src/utils/mongoose/routeGenerator.js +0 -27
- package/src/utils/mongoose/schemaGenerator.js +0 -11
- package/src/utils/mongoose/serviceGenerator.js +0 -49
- package/src/utils/mongoose/swaggerGenerator.js +0 -112
- package/src/utils/mongoose/validationGenerator.js +0 -13
- package/src/utils/prisma/prismaSchemaGenerator.js +0 -10
- package/src/utils/prisma/prismaServiceGenerator.js +0 -47
- package/src/utils/routeInjector.js +0 -40
- package/src/utils/swaggerInjector.js +0 -52
- package/src/utils/validateModuleName.js +0 -17
- package/src/utils/validateProjectName.js +0 -15
- package/src/utils.js +0 -26
- package/templates/mongoose/src/app/errors/castError.ts +0 -23
- package/templates/mongoose/src/app/errors/duplicateError.ts +0 -26
- package/templates/mongoose/src/app/errors/validationError.ts +0 -25
- package/templates/mongoose/src/app/modules/auth/auth.controller.ts +0 -127
- package/templates/mongoose/src/app/modules/auth/auth.interface.ts +0 -24
- package/templates/mongoose/src/app/modules/auth/auth.route.ts +0 -46
- package/templates/mongoose/src/app/modules/auth/auth.schema.ts +0 -19
- package/templates/mongoose/src/app/modules/auth/auth.service.ts +0 -324
- package/templates/mongoose/src/app/modules/auth/auth.swagger.ts +0 -214
- package/templates/mongoose/src/app/modules/auth/auth.validation.ts +0 -37
- package/templates/mongoose/src/app/modules/user/user.controller.ts +0 -17
- package/templates/mongoose/src/app/modules/user/user.interface.ts +0 -15
- package/templates/mongoose/src/app/modules/user/user.route.ts +0 -20
- package/templates/mongoose/src/app/modules/user/user.schema.ts +0 -22
- package/templates/mongoose/src/app/modules/user/user.service.ts +0 -22
- package/templates/mongoose/src/app/modules/user/user.swagger.ts +0 -75
- package/templates/mongoose/src/app/modules/user/user.validation.ts +0 -18
- package/templates/mongoose/src/app/types/index.d.ts +0 -12
- package/templates/mongoose/src/app/utils/isAccountExist.ts +0 -31
- package/templates/prisma/src/app/types/index.d.ts +0 -12
- package/templates/prisma/src/app/utils/otpGenerator.ts +0 -29
package/dist/cli.js
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var p=class extends Error{exitCode;constructor(t,r=1){super(t),this.name="CliError",this.exitCode=r}};import U from"chalk";var d={info:e=>console.log(e),success:e=>console.log(`${U.green("\u2714")} ${e}`),warn:e=>console.warn(`${U.yellow("\u26A0")} ${e}`),error:e=>console.error(`${U.red("\u2716")} ${e}`)};import B from"chalk";import{parseArgs as Ge}from"util";import Se from"@inquirer/select";import x from"chalk";import $ from"fs";import b from"path";import je from"ora";import T from"path";import{fileURLToPath as be}from"url";var M=T.dirname(be(import.meta.url)),J=T.basename(M)==="lib"?T.resolve(M,"../../templates"):T.resolve(M,"../templates");var Ee={_gitignore:".gitignore"},ve=new Set(["node_modules","dist","coverage",".env"]);async function K(e,{template:t,cwd:r=process.cwd()}={}){let s=b.join(r,e);if($.existsSync(s))throw new p(`Folder '${e}' already exists.`);let o=t??await Re(),i=b.join(J,o);if(!$.existsSync(i))throw new p(`Template '${o}' not found at ${i}.`);let n=je(`Creating ${o} project...`).start();try{await $.promises.cp(i,s,{recursive:!0,filter:a=>!ve.has(b.basename(a))});for(let[a,l]of Object.entries(Ee)){let h=b.join(s,a);$.existsSync(h)&&await $.promises.rename(h,b.join(s,l))}await Pe(s,e)}catch(a){throw n.fail("Failed to create project"),await $.promises.rm(s,{recursive:!0,force:!0}),a}n.succeed(`Project '${e}' created`),d.info(x.green(`
|
|
3
|
+
Next steps:`)),d.info(x.cyan(` cd ${e}`)),d.info(x.cyan(" npm install")),d.info(x.cyan(" cp .env.example .env # then fill in the secrets")),d.info(x.cyan(" docker compose up -d # starts the database")),o==="prisma"&&d.info(x.cyan(" npm run db:migrate")),d.info(x.cyan(" npm run dev")),d.info(x.cyan(" npm test"))}async function Re(){return Se({message:"Select a project template:",choices:[{name:"Mongoose (MongoDB)",value:"mongoose"},{name:"Prisma (PostgreSQL)",value:"prisma"}]})}async function Pe(e,t){let r=b.join(e,"package.json");if(!$.existsSync(r))return;let s=JSON.parse(await $.promises.readFile(r,"utf-8"));s.name=t,await $.promises.writeFile(r,`${JSON.stringify(s,null,2)}
|
|
4
|
+
`);let o=b.join(e,"package-lock.json");if(!$.existsSync(o))return;let i=JSON.parse(await $.promises.readFile(o,"utf-8"));i.name=t;let n=i.packages?.[""];n&&(n.name=t),await $.promises.writeFile(o,`${JSON.stringify(i,null,2)}
|
|
5
|
+
`)}import v from"chalk";import w from"fs";import g from"path";var j=(e,t,r)=>{let s=`typeof ${t}_validations.${r}`,o=`type ${e} = z.infer<${s}>;`;return o.length<=100?o:`type ${e} = z.infer<
|
|
6
|
+
${s}
|
|
7
|
+
>;`},R=(e,t)=>{let r=` const result = await ${e}${t.join("")};`;return r.length<=100?r:` const result = await ${e}
|
|
8
|
+
${t.map(s=>` ${s}`).join(`
|
|
9
|
+
`)};`};var P=(e,t)=>{let r=`await ${e}_service.${t}(req);`,s=` const result = ${r}`;return s.length<=100?s:` const result =
|
|
10
|
+
${r}`},Z=e=>`import catchAsync from '../../utils/catch_async.js';
|
|
11
|
+
import manageResponse from '../../utils/manage_response.js';
|
|
12
|
+
import { ${e}_service } from './${e}.service.js';
|
|
13
|
+
|
|
14
|
+
const get_all_${e} = catchAsync(async (req, res) => {
|
|
15
|
+
${P(e,`get_all_${e}_from_db`)}
|
|
16
|
+
manageResponse(res, {
|
|
17
|
+
success: true,
|
|
18
|
+
statusCode: 200,
|
|
19
|
+
message: 'All ${e} fetched successfully.',
|
|
20
|
+
data: result,
|
|
21
|
+
meta: {},
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const get_single_${e} = catchAsync(async (req, res) => {
|
|
26
|
+
${P(e,`get_single_${e}_from_db`)}
|
|
27
|
+
manageResponse(res, {
|
|
28
|
+
success: true,
|
|
29
|
+
statusCode: 200,
|
|
30
|
+
message: 'Single ${e} fetched successfully.',
|
|
31
|
+
data: result,
|
|
32
|
+
meta: {},
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const create_${e} = catchAsync(async (req, res) => {
|
|
37
|
+
${P(e,`create_${e}_into_db`)}
|
|
38
|
+
manageResponse(res, {
|
|
39
|
+
success: true,
|
|
40
|
+
statusCode: 201,
|
|
41
|
+
message: '${e} created successfully.',
|
|
42
|
+
data: result,
|
|
43
|
+
meta: {},
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const update_${e} = catchAsync(async (req, res) => {
|
|
48
|
+
${P(e,`update_${e}_into_db`)}
|
|
49
|
+
manageResponse(res, {
|
|
50
|
+
success: true,
|
|
51
|
+
statusCode: 200,
|
|
52
|
+
message: '${e} updated successfully.',
|
|
53
|
+
data: result,
|
|
54
|
+
meta: {},
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const delete_${e} = catchAsync(async (req, res) => {
|
|
59
|
+
${P(e,`delete_${e}_from_db`)}
|
|
60
|
+
manageResponse(res, {
|
|
61
|
+
success: true,
|
|
62
|
+
statusCode: 200,
|
|
63
|
+
message: '${e} deleted successfully.',
|
|
64
|
+
data: result,
|
|
65
|
+
meta: {},
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export const ${e}_controller = {
|
|
70
|
+
get_all_${e},
|
|
71
|
+
get_single_${e},
|
|
72
|
+
create_${e},
|
|
73
|
+
update_${e},
|
|
74
|
+
delete_${e},
|
|
75
|
+
};
|
|
76
|
+
`;var q=(e,t)=>{let r=`router.${e}(${t.join(", ")});`;return r.length<=100?r:`router.${e}(
|
|
77
|
+
${t.map(s=>` ${s},`).join(`
|
|
78
|
+
`)}
|
|
79
|
+
);`},Q=(e,{auth:t=!1}={})=>{let r=t?["auth()"]:[],s=a=>`RequestValidator(${e}_validations.${a}_${e})`,o=a=>`${e}_controller.${a}_${e}`;return`import { Router } from 'express';
|
|
80
|
+
${t?`import auth from '../../middlewares/auth.js';
|
|
81
|
+
`:""}import RequestValidator from '../../middlewares/request_validator.js';
|
|
82
|
+
import { ${e}_controller } from './${e}.controller.js';
|
|
83
|
+
import { ${e}_validations } from './${e}.validation.js';
|
|
84
|
+
|
|
85
|
+
const router = Router();
|
|
86
|
+
|
|
87
|
+
${t?"// every route needs a signed-in user; use auth('ADMIN') to allow admins only":`// public routes: protect them with auth() from '../../middlewares/auth.js', e.g.
|
|
88
|
+
// router.post('/', auth('ADMIN'), ...), or generate the module with --auth`}
|
|
89
|
+
${q("get",["'/'",...r,o("get_all")])}
|
|
90
|
+
${q("post",["'/'",...r,s("create"),o("create")])}
|
|
91
|
+
${q("get",["'/:id'",...r,o("get_single")])}
|
|
92
|
+
${q("patch",["'/:id'",...r,s("update"),o("update")])}
|
|
93
|
+
${q("delete",["'/:id'",...r,o("delete")])}
|
|
94
|
+
|
|
95
|
+
export default router;
|
|
96
|
+
`};var L=`{
|
|
97
|
+
name: 'id',
|
|
98
|
+
in: 'path',
|
|
99
|
+
required: true,
|
|
100
|
+
schema: { type: 'string' },
|
|
101
|
+
}`,X=e=>`export const ${e}SwaggerDocs = {
|
|
102
|
+
'/api/${e}': {
|
|
103
|
+
post: {
|
|
104
|
+
tags: ['${e}'],
|
|
105
|
+
summary: 'Create new ${e}',
|
|
106
|
+
description: '',
|
|
107
|
+
requestBody: {
|
|
108
|
+
required: true,
|
|
109
|
+
content: {
|
|
110
|
+
'application/json': {
|
|
111
|
+
example: JSON.stringify({ name: 'Example ${e}' }),
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
responses: {
|
|
116
|
+
201: { description: '${e} created successfully' },
|
|
117
|
+
500: { description: 'Validation error or internal server error' },
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
get: {
|
|
121
|
+
tags: ['${e}'],
|
|
122
|
+
summary: 'Get all ${e}',
|
|
123
|
+
description: '',
|
|
124
|
+
parameters: [
|
|
125
|
+
{
|
|
126
|
+
name: 'page',
|
|
127
|
+
in: 'query',
|
|
128
|
+
required: false,
|
|
129
|
+
schema: { type: 'number' },
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'limit',
|
|
133
|
+
in: 'query',
|
|
134
|
+
required: false,
|
|
135
|
+
schema: { type: 'number' },
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
responses: {
|
|
139
|
+
200: { description: '${e} fetched successfully' },
|
|
140
|
+
401: { description: 'unauthorized' },
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
'/api/${e}/{id}': {
|
|
146
|
+
get: {
|
|
147
|
+
tags: ['${e}'],
|
|
148
|
+
summary: 'Get single ${e}',
|
|
149
|
+
description: '',
|
|
150
|
+
parameters: [
|
|
151
|
+
${L},
|
|
152
|
+
],
|
|
153
|
+
responses: {
|
|
154
|
+
200: { description: '${e} fetched successfully' },
|
|
155
|
+
401: { description: 'unauthorized' },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
patch: {
|
|
159
|
+
tags: ['${e}'],
|
|
160
|
+
summary: 'Update ${e}',
|
|
161
|
+
description: '',
|
|
162
|
+
parameters: [
|
|
163
|
+
${L},
|
|
164
|
+
],
|
|
165
|
+
requestBody: {
|
|
166
|
+
required: true,
|
|
167
|
+
content: {
|
|
168
|
+
'application/json': {
|
|
169
|
+
example: JSON.stringify({ name: 'Example ${e}' }),
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
responses: {
|
|
174
|
+
200: { description: '${e} updated successfully' },
|
|
175
|
+
500: { description: 'Validation error or internal server error' },
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
delete: {
|
|
179
|
+
tags: ['${e}'],
|
|
180
|
+
summary: 'Delete ${e}',
|
|
181
|
+
description: '',
|
|
182
|
+
parameters: [
|
|
183
|
+
${L},
|
|
184
|
+
],
|
|
185
|
+
responses: {
|
|
186
|
+
200: { description: '${e} deleted successfully' },
|
|
187
|
+
401: { description: 'unauthorized' },
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
`;var Y=e=>`import { z } from 'zod';
|
|
193
|
+
|
|
194
|
+
// \`name\` is an example field: rename it or add yours here, in the database schema
|
|
195
|
+
// and in tests/routes/${e}.test.ts. Fields that aren't listed here are removed
|
|
196
|
+
// from req.body before it reaches the service.
|
|
197
|
+
const create_${e} = z.object({
|
|
198
|
+
name: z.string('Name is required').trim().min(1).max(200),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const update_${e} = create_${e}.partial();
|
|
202
|
+
|
|
203
|
+
export const ${e}_validations = {
|
|
204
|
+
create_${e},
|
|
205
|
+
update_${e},
|
|
206
|
+
};
|
|
207
|
+
`;var ee=e=>`// the fields of a ${e}; \`name\` is an example, keep it in sync with ${e}.validation.ts
|
|
208
|
+
export type T_${e} = {
|
|
209
|
+
name: string;
|
|
210
|
+
};
|
|
211
|
+
`;var te=e=>`import { Schema, model } from 'mongoose';
|
|
212
|
+
import type { T_${e} } from './${e}.interface.js';
|
|
213
|
+
|
|
214
|
+
const schema = new Schema<T_${e}>(
|
|
215
|
+
{
|
|
216
|
+
name: { type: String, required: true, trim: true },
|
|
217
|
+
},
|
|
218
|
+
{ timestamps: true, versionKey: false },
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
export const ${e}_model = model('${e}', schema);
|
|
222
|
+
`;var re=e=>`import type { Request } from 'express';
|
|
223
|
+
import type { z } from 'zod';
|
|
224
|
+
import { AppError } from '../../utils/app_error.js';
|
|
225
|
+
import { ${e}_model } from './${e}.schema.js';
|
|
226
|
+
import type { ${e}_validations } from './${e}.validation.js';
|
|
227
|
+
|
|
228
|
+
// req.body was already parsed by RequestValidator (see ${e}.route.ts), so it only
|
|
229
|
+
// holds the fields of these schemas: add a field there before the service can use it
|
|
230
|
+
${j("CreatePayload",e,`create_${e}`)}
|
|
231
|
+
${j("UpdatePayload",e,`update_${e}`)}
|
|
232
|
+
|
|
233
|
+
const notFound = () => new AppError('${e} not found', 404);
|
|
234
|
+
|
|
235
|
+
const get_all_${e}_from_db = async (_req: Request) => {
|
|
236
|
+
// define your own logic here
|
|
237
|
+
${R(`${e}_model`,[".find()",".lean()"])}
|
|
238
|
+
return result;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const get_single_${e}_from_db = async (req: Request) => {
|
|
242
|
+
// define your own logic here
|
|
243
|
+
const { id } = req.params;
|
|
244
|
+
${R(`${e}_model`,[".findById(id)",".lean()"])}
|
|
245
|
+
if (!result) throw notFound();
|
|
246
|
+
return result;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const create_${e}_into_db = async (req: Request) => {
|
|
250
|
+
// define your own logic here
|
|
251
|
+
const payload: CreatePayload = req.body;
|
|
252
|
+
const result = await ${e}_model.create(payload);
|
|
253
|
+
return result;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const update_${e}_into_db = async (req: Request) => {
|
|
257
|
+
// define your own logic here
|
|
258
|
+
const { id } = req.params;
|
|
259
|
+
const payload: UpdatePayload = req.body;
|
|
260
|
+
${R(`${e}_model`,[".findByIdAndUpdate(id, payload, { returnDocument: 'after' })",".lean()"])}
|
|
261
|
+
if (!result) throw notFound();
|
|
262
|
+
return result;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const delete_${e}_from_db = async (req: Request) => {
|
|
266
|
+
// define your own logic here
|
|
267
|
+
const { id } = req.params;
|
|
268
|
+
${R(`${e}_model`,[".findByIdAndDelete(id)",".lean()"])}
|
|
269
|
+
if (!result) throw notFound();
|
|
270
|
+
return result;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
export const ${e}_service = {
|
|
274
|
+
get_all_${e}_from_db,
|
|
275
|
+
get_single_${e}_from_db,
|
|
276
|
+
create_${e}_into_db,
|
|
277
|
+
update_${e}_into_db,
|
|
278
|
+
delete_${e}_from_db,
|
|
279
|
+
};
|
|
280
|
+
`;var se=(e,{auth:t=!1}={})=>`import { Types } from 'mongoose';
|
|
281
|
+
import request from 'supertest';
|
|
282
|
+
import { ${t?"beforeEach, describe, expect, it":"describe, expect, it"} } from 'vitest';
|
|
283
|
+
import app from '../../src/app.js';
|
|
284
|
+
import { ${e}_model } from '../../src/app/modules/${e}/${e}.schema.js';
|
|
285
|
+
${t?`import { bearer, createAccount } from '../helpers/fixtures.js';
|
|
286
|
+
`:""}
|
|
287
|
+
// Starter tests for the generated CRUD routes: extend them with your own rules.
|
|
288
|
+
const BASE = '/api/${e}';
|
|
289
|
+
const missingId = new Types.ObjectId().toString();
|
|
290
|
+
|
|
291
|
+
// Valid request bodies, matching ${e}.validation.ts. Update them with your fields.
|
|
292
|
+
const createPayload = { name: 'Example ${e}' };
|
|
293
|
+
const updatePayload = { name: 'Updated ${e}' };
|
|
294
|
+
|
|
295
|
+
${t?`let token = '';
|
|
296
|
+
|
|
297
|
+
beforeEach(async () => {
|
|
298
|
+
token = bearer(await createAccount());
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
/** Requests as a signed-in user. */
|
|
302
|
+
const api = () => request.agent(app).set('Authorization', token);`:"const api = () => request(app);"}
|
|
303
|
+
|
|
304
|
+
/** Inserts one directly in the database, for the read / update / delete tests. */
|
|
305
|
+
const insert = () => ${e}_model.create(createPayload);
|
|
306
|
+
|
|
307
|
+
describe(BASE, () => {${t?"\n it.each([\n ['get', BASE],\n ['post', BASE],\n ['get', `${BASE}/${missingId}`],\n ['patch', `${BASE}/${missingId}`],\n ['delete', `${BASE}/${missingId}`],\n ] as const)('%s %s needs a signed-in user', async (method, url) => {\n const res = await request(app)[method](url);\n\n expect(res.status).toBe(401);\n });\n":""}
|
|
308
|
+
it('POST / creates one', async () => {
|
|
309
|
+
const res = await api().post(BASE).send(createPayload);
|
|
310
|
+
|
|
311
|
+
expect(res.status).toBe(201);
|
|
312
|
+
expect(res.body.data).toMatchObject(createPayload);
|
|
313
|
+
expect(await ${e}_model.countDocuments()).toBe(1);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('POST / rejects an invalid body', async () => {
|
|
317
|
+
const res = await api().post(BASE).send({});
|
|
318
|
+
|
|
319
|
+
expect(res.status).toBe(400);
|
|
320
|
+
expect(await ${e}_model.countDocuments()).toBe(0);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('GET / lists them', async () => {
|
|
324
|
+
await insert();
|
|
325
|
+
|
|
326
|
+
const res = await api().get(BASE);
|
|
327
|
+
|
|
328
|
+
expect(res.status).toBe(200);
|
|
329
|
+
expect(res.body.data).toHaveLength(1);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('GET /:id returns one', async () => {
|
|
333
|
+
const doc = await insert();
|
|
334
|
+
|
|
335
|
+
const res = await api().get(\`\${BASE}/\${doc._id}\`);
|
|
336
|
+
|
|
337
|
+
expect(res.status).toBe(200);
|
|
338
|
+
expect(res.body.data).toMatchObject({ _id: String(doc._id), ...createPayload });
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('GET /:id is 404 when it does not exist', async () => {
|
|
342
|
+
const res = await api().get(\`\${BASE}/\${missingId}\`);
|
|
343
|
+
|
|
344
|
+
expect(res.status).toBe(404);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('GET /:id is 400 for a malformed id', async () => {
|
|
348
|
+
const res = await api().get(\`\${BASE}/not-an-id\`);
|
|
349
|
+
|
|
350
|
+
expect(res.status).toBe(400);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('PATCH /:id updates it', async () => {
|
|
354
|
+
const doc = await insert();
|
|
355
|
+
|
|
356
|
+
const res = await api().patch(\`\${BASE}/\${doc._id}\`).send(updatePayload);
|
|
357
|
+
|
|
358
|
+
expect(res.status).toBe(200);
|
|
359
|
+
const stored = await ${e}_model.findById(doc._id).lean();
|
|
360
|
+
expect(stored).toMatchObject(updatePayload);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('PATCH /:id is 404 when it does not exist', async () => {
|
|
364
|
+
const res = await api().patch(\`\${BASE}/\${missingId}\`).send(updatePayload);
|
|
365
|
+
|
|
366
|
+
expect(res.status).toBe(404);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('DELETE /:id deletes it', async () => {
|
|
370
|
+
const doc = await insert();
|
|
371
|
+
|
|
372
|
+
const res = await api().delete(\`\${BASE}/\${doc._id}\`);
|
|
373
|
+
|
|
374
|
+
expect(res.status).toBe(200);
|
|
375
|
+
const stillThere = await ${e}_model.exists({ _id: doc._id });
|
|
376
|
+
expect(stillThere).toBeNull();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('DELETE /:id is 404 when it does not exist', async () => {
|
|
380
|
+
const res = await api().delete(\`\${BASE}/\${missingId}\`);
|
|
381
|
+
|
|
382
|
+
expect(res.status).toBe(404);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
`;var oe=e=>e.charAt(0).toUpperCase()+e.slice(1);var ne=e=>`model ${oe(e)} {
|
|
386
|
+
id String @id @default(uuid())
|
|
387
|
+
// example field, keep it in sync with ${e}.validation.ts
|
|
388
|
+
name String
|
|
389
|
+
createdAt DateTime @default(now())
|
|
390
|
+
updatedAt DateTime @updatedAt
|
|
391
|
+
}
|
|
392
|
+
`;var ie=e=>`import type { Request } from 'express';
|
|
393
|
+
import type { z } from 'zod';
|
|
394
|
+
import { prisma } from '../../lib/prisma.js';
|
|
395
|
+
import { AppError } from '../../utils/app_error.js';
|
|
396
|
+
import type { ${e}_validations } from './${e}.validation.js';
|
|
397
|
+
|
|
398
|
+
// req.body was already parsed by RequestValidator (see ${e}.route.ts), so it only
|
|
399
|
+
// holds the fields of these schemas: add a field there before the service can use it
|
|
400
|
+
${j("CreatePayload",e,`create_${e}`)}
|
|
401
|
+
${j("UpdatePayload",e,`update_${e}`)}
|
|
402
|
+
|
|
403
|
+
const get_all_${e}_from_db = async (_req: Request) => {
|
|
404
|
+
// define your own logic here
|
|
405
|
+
const result = await prisma.${e}.findMany();
|
|
406
|
+
return result;
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const get_single_${e}_from_db = async (req: Request) => {
|
|
410
|
+
// define your own logic here
|
|
411
|
+
const { id } = req.params as { id: string };
|
|
412
|
+
const result = await prisma.${e}.findUnique({
|
|
413
|
+
where: { id },
|
|
414
|
+
});
|
|
415
|
+
if (!result) throw new AppError('${e} not found', 404);
|
|
416
|
+
return result;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const create_${e}_into_db = async (req: Request) => {
|
|
420
|
+
// define your own logic here
|
|
421
|
+
const payload: CreatePayload = req.body;
|
|
422
|
+
const result = await prisma.${e}.create({
|
|
423
|
+
data: payload,
|
|
424
|
+
});
|
|
425
|
+
return result;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
// update / delete of a missing record throw Prisma's P2025, which the global
|
|
429
|
+
// error handler turns into a 404
|
|
430
|
+
const update_${e}_into_db = async (req: Request) => {
|
|
431
|
+
// define your own logic here
|
|
432
|
+
const { id } = req.params as { id: string };
|
|
433
|
+
const payload: UpdatePayload = req.body;
|
|
434
|
+
const result = await prisma.${e}.update({
|
|
435
|
+
where: { id },
|
|
436
|
+
data: payload,
|
|
437
|
+
});
|
|
438
|
+
return result;
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const delete_${e}_from_db = async (req: Request) => {
|
|
442
|
+
// define your own logic here
|
|
443
|
+
const { id } = req.params as { id: string };
|
|
444
|
+
const result = await prisma.${e}.delete({
|
|
445
|
+
where: { id },
|
|
446
|
+
});
|
|
447
|
+
return result;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
export const ${e}_service = {
|
|
451
|
+
get_all_${e}_from_db,
|
|
452
|
+
get_single_${e}_from_db,
|
|
453
|
+
create_${e}_into_db,
|
|
454
|
+
update_${e}_into_db,
|
|
455
|
+
delete_${e}_from_db,
|
|
456
|
+
};
|
|
457
|
+
`;var ae=(e,{auth:t=!1}={})=>`import { Prisma } from '@prisma/client';
|
|
458
|
+
import request from 'supertest';
|
|
459
|
+
import { ${t?"beforeEach, describe, expect, it":"describe, expect, it"} } from 'vitest';
|
|
460
|
+
import app from '../../src/app.js';
|
|
461
|
+
${t?`import { bearer, makeAccount } from '../helpers/fixtures.js';
|
|
462
|
+
`:""}import { prismaMock } from '../helpers/prisma.js';
|
|
463
|
+
|
|
464
|
+
// Starter tests for the generated CRUD routes: extend them with your own rules.
|
|
465
|
+
const BASE = '/api/${e}';
|
|
466
|
+
const db = prismaMock.${e};
|
|
467
|
+
|
|
468
|
+
// Valid request bodies, matching ${e}.validation.ts. Update them with your fields.
|
|
469
|
+
const createPayload = { name: 'Example ${e}' };
|
|
470
|
+
const updatePayload = { name: 'Updated ${e}' };
|
|
471
|
+
|
|
472
|
+
// a row as the database would return it
|
|
473
|
+
const row = {
|
|
474
|
+
id: '9b2f3c1e-7a41-4c8e-9d6b-2f4e8a1c5b70',
|
|
475
|
+
...createPayload,
|
|
476
|
+
createdAt: new Date('2025-01-01'),
|
|
477
|
+
updatedAt: new Date('2025-01-01'),
|
|
478
|
+
};
|
|
479
|
+
// ...and as it looks in a JSON response
|
|
480
|
+
const json = JSON.parse(JSON.stringify(row));
|
|
481
|
+
|
|
482
|
+
const recordNotFound = () =>
|
|
483
|
+
new Prisma.PrismaClientKnownRequestError('Record not found', {
|
|
484
|
+
code: 'P2025',
|
|
485
|
+
clientVersion: 'test',
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
${t?`const account = makeAccount();
|
|
489
|
+
|
|
490
|
+
beforeEach(() => {
|
|
491
|
+
// the auth middleware looks the signed-in account up
|
|
492
|
+
prismaMock.account.findUnique.mockResolvedValue(account);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
/** Requests as a signed-in user. */
|
|
496
|
+
const api = () => request.agent(app).set('Authorization', bearer(account));`:"const api = () => request(app);"}
|
|
497
|
+
|
|
498
|
+
describe(BASE, () => {${t?"\n it.each([\n ['get', BASE],\n ['post', BASE],\n ['get', `${BASE}/${row.id}`],\n ['patch', `${BASE}/${row.id}`],\n ['delete', `${BASE}/${row.id}`],\n ] as const)('%s %s needs a signed-in user', async (method, url) => {\n const res = await request(app)[method](url);\n\n expect(res.status).toBe(401);\n });\n":""}
|
|
499
|
+
it('POST / creates one', async () => {
|
|
500
|
+
db.create.mockResolvedValue(row);
|
|
501
|
+
|
|
502
|
+
const res = await api().post(BASE).send(createPayload);
|
|
503
|
+
|
|
504
|
+
expect(res.status).toBe(201);
|
|
505
|
+
expect(res.body.data).toEqual(json);
|
|
506
|
+
expect(db.create).toHaveBeenCalledWith({ data: createPayload });
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('POST / rejects an invalid body', async () => {
|
|
510
|
+
const res = await api().post(BASE).send({});
|
|
511
|
+
|
|
512
|
+
expect(res.status).toBe(400);
|
|
513
|
+
expect(db.create).not.toHaveBeenCalled();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('GET / lists them', async () => {
|
|
517
|
+
db.findMany.mockResolvedValue([row]);
|
|
518
|
+
|
|
519
|
+
const res = await api().get(BASE);
|
|
520
|
+
|
|
521
|
+
expect(res.status).toBe(200);
|
|
522
|
+
expect(res.body.data).toEqual([json]);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('GET /:id returns one', async () => {
|
|
526
|
+
db.findUnique.mockResolvedValue(row);
|
|
527
|
+
|
|
528
|
+
const res = await api().get(\`\${BASE}/\${row.id}\`);
|
|
529
|
+
|
|
530
|
+
expect(res.status).toBe(200);
|
|
531
|
+
expect(res.body.data).toEqual(json);
|
|
532
|
+
expect(db.findUnique).toHaveBeenCalledWith({ where: { id: row.id } });
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('GET /:id is 404 when it does not exist', async () => {
|
|
536
|
+
db.findUnique.mockResolvedValue(null);
|
|
537
|
+
|
|
538
|
+
const res = await api().get(\`\${BASE}/\${row.id}\`);
|
|
539
|
+
|
|
540
|
+
expect(res.status).toBe(404);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('PATCH /:id updates it', async () => {
|
|
544
|
+
db.update.mockResolvedValue({ ...row, ...updatePayload });
|
|
545
|
+
|
|
546
|
+
const res = await api().patch(\`\${BASE}/\${row.id}\`).send(updatePayload);
|
|
547
|
+
|
|
548
|
+
expect(res.status).toBe(200);
|
|
549
|
+
expect(db.update).toHaveBeenCalledWith({ where: { id: row.id }, data: updatePayload });
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it('PATCH /:id is 404 when it does not exist', async () => {
|
|
553
|
+
db.update.mockRejectedValue(recordNotFound());
|
|
554
|
+
|
|
555
|
+
const res = await api().patch(\`\${BASE}/\${row.id}\`).send(updatePayload);
|
|
556
|
+
|
|
557
|
+
expect(res.status).toBe(404);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it('DELETE /:id deletes it', async () => {
|
|
561
|
+
db.delete.mockResolvedValue(row);
|
|
562
|
+
|
|
563
|
+
const res = await api().delete(\`\${BASE}/\${row.id}\`);
|
|
564
|
+
|
|
565
|
+
expect(res.status).toBe(200);
|
|
566
|
+
expect(db.delete).toHaveBeenCalledWith({ where: { id: row.id } });
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('DELETE /:id is 404 when it does not exist', async () => {
|
|
570
|
+
db.delete.mockRejectedValue(recordNotFound());
|
|
571
|
+
|
|
572
|
+
const res = await api().delete(\`\${BASE}/\${row.id}\`);
|
|
573
|
+
|
|
574
|
+
expect(res.status).toBe(404);
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
`;var qe=/^import\s[\s\S]*?from\s*(['"])[^'"]+\1;?[ \t]*$|^import\s*(['"])[^'"]+\2;?[ \t]*$/gm;function I(e){return{quote:e.match(/^import[\s\S]*?(['"])/m)?.[1]==="'"?"'":'"',jsExtension:/from\s*['"]\.{1,2}\/[^'"]+\.js['"]/.test(e)}}function C(e,t){let r=-1;for(let s of e.matchAll(qe))r=s.index+s[0].length;return r===-1?`${t}
|
|
578
|
+
${e}`:`${e.slice(0,r)}
|
|
579
|
+
${t}${e.slice(r)}`}var k={"[":"]","{":"}","(":")"};function ce(e,t){let r=e[t],s=e[t+1];if(r==="/"&&s==="/"){let o=e.indexOf(`
|
|
580
|
+
`,t);return o===-1?e.length-1:o}if(r==="/"&&s==="*"){let o=e.indexOf("*/",t+2);return o===-1?-1:o+1}if(r==="'"||r==='"'){let o=t+1;for(;o<e.length&&e[o]!==r&&e[o]!==`
|
|
581
|
+
`;)o+=e[o]==="\\"?2:1;return e[o]===r?o:-1}if(r==="`"){let o=t+1;for(;o<e.length;)if(e[o]==="\\")o+=2;else{if(e[o]==="`")return o;if(e[o]==="$"&&e[o+1]==="{"){let i=D(e,o+2,["}"],!0);if(i===-1)return-1;o=i+1}else o++}return-1}return null}function D(e,t,r,s){for(let o=t;o<e.length;o++){let i=ce(e,o);if(i!==null){if(i===-1)return-1;o=i;continue}let n=e[o];if(n in k)r.push(k[n]);else if(n==="]"||n==="}"||n===")"){if(r.pop()!==n)return-1;if(s&&r.length===0)return o}}return!s&&r.length===0?e.length:-1}function V(e,t,r){return D(e,t,[r],!0)}function E(e){return D(e,0,[],!1)===e.length}function de(e,t,r){let s=[];for(let o=t;o<r;o++){let i=ce(e,o);if(i!==null){if(i===-1)return s;o=i;continue}let n=e[o];if(n in k){let a=D(e,o+1,[k[n]],!0);if(a===-1)return s;n==="{"&&s.push(e.slice(o,a+1)),o=a}}return s}function O(e,t,r,s){let o=e.match(t);if(o?.index===void 0)return null;let i=o.index+o[0].length,n=V(e,i,r);if(n===-1)return null;let a=e.slice(i,n),l=a.match(/\n([ \t]*)$/)?.[1]??"",h=a.match(/\n([ \t]+)\S/)?.[1]??`${l} `,c=a.trimEnd(),_=c.length>0&&!/[,[{]$/.test(c),f=`${c}${_?",":""}
|
|
582
|
+
${h}${s}
|
|
583
|
+
${l}`;return e.slice(0,i)+f+e.slice(n)}function F(e,t){let r=e.includes(`\r
|
|
584
|
+
`),s=t(r?e.replace(/\r\n/g,`
|
|
585
|
+
`):e);return!r||s.status!=="updated"?s:{status:"updated",content:s.content.replace(/\n/g,`\r
|
|
586
|
+
`)}}var pe=/const moduleRoutes\s*=\s*\[/;function H(e){let t=[],r=e.match(pe);if(r?.index!==void 0){let s=r.index+r[0].length,o=V(e,s,"]");if(o!==-1)for(let i of de(e,s,o)){let n=/\bpath\s*:\s*(['"`])([^'"`]*)\1/.exec(i)?.[2];if(n===void 0)continue;let a=/\broute\s*:\s*([A-Za-z_$][\w$]*)\s*[,}]/.exec(i)?.[1]??null;t.push({path:n,route:a})}}for(let s of e.matchAll(/\.use\(\s*(['"`])(\/[^'"`]*)\1\s*,\s*([A-Za-z_$][\w$]*)?/g))t.push({path:s[2],route:s[3]??null});return t}function le(e,t){return F(e,r=>Ae(r,t))}function Ae(e,t){let{quote:r,jsExtension:s}=I(e),o=`./app/modules/${t}/${t}.route`,i=`${t}Route`,n=l=>H(l).filter(h=>h.route===i).length,a=e;if(a.includes(o)||(a=C(a,`import ${i} from ${r}${o}${s?".js":""}${r};`)),n(a)===0){let l=O(a,pe,"]",`{ path: ${r}/${t}${r}, route: ${i} },`);if(l===null)return{status:"failed",reason:"could not find `const moduleRoutes = [...]`"};a=l}return a===e?{status:"unchanged"}:E(e)&&!E(a)||n(a)!==1?{status:"failed",reason:"the edited file didn't look right"}:{status:"updated",content:a}}function ue(e,t){return F(e,r=>Be(r,t))}function Be(e,t){let{quote:r,jsExtension:s}=I(e),o=`./app/modules/${t}/${t}.swagger`,i=`${t}SwaggerDocs`,n=e;if(n.includes(o)||(n=C(n,`import { ${i} } from ${r}${o}${s?".js":""}${r};`)),!n.includes(`...${i}`)){let l=O(n,/paths:\s*\{/,"}",`...${i},`);if(l===null)return{status:"failed",reason:"could not find `paths: { ... }`"};n=l}if(n===e)return{status:"unchanged"};let a=n.match(new RegExp(`\\.\\.\\.${i}\\b`,"g"))?.length??0;return E(e)&&!E(n)||a!==1?{status:"failed",reason:"the edited file didn't look right"}:{status:"updated",content:n}}import{randomBytes as Te}from"crypto";import u from"fs";import S from"path";function me(e,t,r={}){let s=new Map,o=[],i=[],n=e.filter(c=>!u.existsSync(c.path)||u.readFileSync(c.path,"utf-8")!==c.content);for(let c of n)u.existsSync(c.path)&&s.set(c.path,u.readFileSync(c.path,"utf-8"));let a=ke(s,t,r.backupDir),l=[];try{for(let c of n)s.has(c.path)||(i.push(...Ie(S.dirname(c.path))),u.mkdirSync(S.dirname(c.path),{recursive:!0}),o.push(c.path)),fe(c.path,c.content),s.has(c.path)&&l.push(c.path)}catch(c){let _=f=>{try{f()}catch{}};for(let f of l)_(()=>fe(f,s.get(f)));for(let f of o)_(()=>u.rmSync(f,{force:!0}));for(let f of[...new Set(i)].sort((m,N)=>N.length-m.length))_(()=>{u.existsSync(f)&&u.readdirSync(f).length===0&&u.rmdirSync(f)});throw c}let h=c=>S.relative(t,c);for(let c of o)d.success(`${h(c)} created`);for(let c of s.keys())d.success(`${h(c)} updated`);return{created:o,updated:[...s.keys()],backupDir:a}}function fe(e,t){let r=u.existsSync(e)?u.statSync(e):null;r&&u.accessSync(e,u.constants.W_OK);let s=`${e}.${Te(6).toString("hex")}.tmp`;try{u.writeFileSync(s,t,r?{mode:r.mode}:{}),u.renameSync(s,e)}catch(o){throw u.rmSync(s,{force:!0}),o}}function ke(e,t,r){if(!r||e.size===0)return null;for(let[s,o]of e){let i=S.join(r,S.relative(t,s));u.mkdirSync(S.dirname(i),{recursive:!0}),u.writeFileSync(i,o)}return r}function Ie(e){let t=[],r=e;for(;!u.existsSync(r);){t.unshift(r);let s=S.dirname(r);if(s===r)break;r=s}return t}import ge from"fs";import Ce from"path";var A=["mongoose","prisma"],G=".projectrc.json",W=e=>typeof e=="string"&&A.includes(e);function he(e){let t=Ce.join(e,G);if(!ge.existsSync(t))throw new p(`${G} not found. Run this command from the root of a project created with exp-node-server.`);let r;try{r=JSON.parse(ge.readFileSync(t,"utf-8"))}catch{throw new p(`${G} is not valid JSON.`)}let s=r?.database;if(!W(s))throw new p(`Invalid "database" in ${G}. Expected one of: ${A.join(", ")}.`);return s}async function ye(e,{cwd:t=process.cwd(),force:r=!1,auth:s=!1}={}){let o=he(t),i=g.join(t,"src/app/modules",e);if(w.existsSync(i)&&!r)throw new p(`Module '${e}' already exists at ${g.relative(t,i)}. Use --force to overwrite it.`);Fe(t,e);let n=(m,N)=>({path:g.join(i,`${e}.${m}`),content:N}),a=[n("controller.ts",Z(e)),n("route.ts",Q(e,{auth:s})),n("validation.ts",Y(e)),n("swagger.ts",X(e))];o==="mongoose"?a.push(n("interface.ts",ee(e)),n("schema.ts",te(e)),n("service.ts",re(e))):a.push(n("service.ts",ie(e)),{path:g.join(t,"prisma/schema",`${e}.prisma`),content:ne(e)});let l=g.join(t,"tests/routes",`${e}.test.ts`),h=De.some(m=>w.existsSync(g.join(t,m)));h&&a.push({path:l,content:o==="mongoose"?se(e,{auth:s}):ae(e,{auth:s})});let c=[$e(g.join(t,"src/routes.ts"),m=>le(m,e),"Route registered"),$e(g.join(t,"src/swaggerOptions.ts"),m=>ue(m,e),"Swagger docs registered")];for(let{file:m}of c)m&&a.push(m);let _=r?g.join(t,Oe,new Date().toISOString().replace(/[:.]/g,"-")):void 0,f=me(a,t,{backupDir:_});for(let{report:m}of c)m();h||d.info(v.dim(" no vitest config found, skipped tests/routes test")),f.backupDir&&d.warn(`${f.updated.length} existing file(s) were overwritten. Previous versions: ${g.relative(t,f.backupDir)}`),o==="prisma"&&(d.info(v.blue(`
|
|
587
|
+
Next, run:`)),d.info(v.yellow(" npx prisma generate && npx prisma migrate dev"))),h&&d.info(v.blue(`
|
|
588
|
+
Test it with: npm test -- tests/routes/${e}.test.ts`)),d.info(v.green(`
|
|
589
|
+
Module '${e}' created successfully`))}var De=["vitest.config.ts","vitest.config.mts","vitest.config.js"],Oe=".exp-node-server/backups";function Fe(e,t){let r=g.join(e,"src/routes.ts");if(w.existsSync(r)){let i=H(w.readFileSync(r,"utf-8")).find(n=>n.path===`/${t}`&&n.route!==`${t}Route`);if(i)throw new p(`The path '/${t}' is already used by ${i.route??"another router"} in src/routes.ts. Pick another module name.`)}let s=g.join(e,"src/app/modules");if(!w.existsSync(s))return;let o=new RegExp(`['"\`]/api/${t}(/[^'"\`]*)?['"\`]\\s*:`);for(let i of w.readdirSync(s)){if(i===t)continue;let n=g.join(s,i,`${i}.swagger.ts`);if(w.existsSync(n)&&o.test(w.readFileSync(n,"utf-8")))throw new p(`The path '/api/${t}' is already documented by the '${i}' module. Pick another module name.`)}}function $e(e,t,r){let s=g.basename(e);if(!w.existsSync(e))return{file:null,report:()=>d.warn(`${s} not found, skipping registration`)};let o=t(w.readFileSync(e,"utf-8"));switch(o.status){case"updated":return{file:{path:e,content:o.content},report:()=>d.success(`${r} in ${s}`)};case"unchanged":return{file:null,report:()=>d.info(v.dim(` already registered in ${s}`))};case"failed":return{file:null,report:()=>d.warn(`Couldn't update ${s} (${o.reason}). Register it manually.`)}}}function we(e){if(!e)return null;let t=e.trim().toLowerCase().replace(/[-\s]+/g,"_");return/^[a-z][a-z_]*$/.test(t)?t:null}function xe(e){if(!e)return null;let t=e.trim().toLowerCase().replace(/\s+/g,"-");return/^[a-z0-9][a-z0-9-]*$/.test(t)?t:null}var y="exp-node-server",Ne=`
|
|
590
|
+
${B.bold(y)} - scaffold Express + TypeScript APIs with Mongoose or Prisma
|
|
591
|
+
|
|
592
|
+
${B.bold("Usage")}
|
|
593
|
+
npx ${y} create <project-name> [--template <mongoose|prisma>]
|
|
594
|
+
npx ${y} generate <module-name> [--auth] [--force]
|
|
595
|
+
|
|
596
|
+
${B.bold("Commands")}
|
|
597
|
+
create, -c <name> Create a new project from a template
|
|
598
|
+
generate, -g <name> Generate a module in the current project
|
|
599
|
+
|
|
600
|
+
${B.bold("Options")}
|
|
601
|
+
-t, --template <name> Template to use with create (${A.join(" | ")})
|
|
602
|
+
-a, --auth Generated routes require a signed-in user (default: public)
|
|
603
|
+
-f, --force Overwrite an existing module when generating
|
|
604
|
+
-h, --help Show this help
|
|
605
|
+
-v, --version Show version
|
|
606
|
+
|
|
607
|
+
${B.bold("Examples")}
|
|
608
|
+
npx ${y} -c my-api
|
|
609
|
+
npx ${y} create my-api --template prisma
|
|
610
|
+
npx ${y} -g order
|
|
611
|
+
npx ${y} -g order --auth
|
|
612
|
+
`;async function _e(e){let{values:t,positionals:r}=Ue(e);if(t.version){console.log("2.1.0");return}let s=Me(t,r);if(t.help||!s){console.log(Ne);return}if(s.kind==="create"){if(t.auth||t.force)throw new p("--auth and --force only apply to generate.");let i=xe(s.name);if(!i)throw new p(s.name?`Invalid project name '${s.name}'. Use letters, numbers and dashes.`:"Project name required. Example: exp-node-server -c my-api");let{template:n}=t;if(n!==void 0&&!W(n))throw new p(`Unknown template '${n}'. Use one of: ${A.join(", ")}.`);await K(i,{template:n});return}let o=we(s.name);if(!o)throw new p(s.name?`Invalid module name '${s.name}'. Use letters and underscores, starting with a letter.`:"Module name required. Example: exp-node-server -g order");await ye(o,{force:t.force??!1,auth:t.auth??!1})}function Ue(e){try{return Ge({args:e,allowPositionals:!0,strict:!0,options:{create:{type:"string",short:"c"},generate:{type:"string",short:"g"},template:{type:"string",short:"t"},force:{type:"boolean",short:"f"},auth:{type:"boolean",short:"a"},help:{type:"boolean",short:"h"},version:{type:"boolean",short:"v"}}})}catch(t){let r=t instanceof Error?t.message:String(t);throw new p(`${r}
|
|
613
|
+
Run '${y} --help' for usage.`)}}function Me(e,t){if(e.create!==void 0&&e.generate!==void 0)throw new p("Use either -c (create) or -g (generate), not both.");if((e.create!==void 0||e.generate!==void 0)&&t.length>0)throw new p(`Unexpected argument '${t[0]}'. Run '${y} --help' for usage.`);if(e.create!==void 0)return{kind:"create",name:e.create};if(e.generate!==void 0)return{kind:"generate",name:e.generate};let[r,s,...o]=t;if(r===void 0)return null;if(o.length>0)throw new p(`Unexpected argument '${o[0]}'. Run '${y} --help' for usage.`);switch(r){case"create":case"c":return{kind:"create",name:s};case"generate":case"g":return{kind:"generate",name:s};default:throw new p(`Unknown command '${r}'. Run '${y} --help' for usage.`)}}try{await _e(process.argv.slice(2))}catch(e){e instanceof p?(d.error(e.message),process.exitCode=e.exitCode):e instanceof Error&&e.name==="ExitPromptError"?process.exitCode=130:(d.error("Unexpected error:"),console.error(e),process.exitCode=1)}
|
package/package.json
CHANGED
|
@@ -1,19 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "exp-node-server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "CLI to scaffold Express + TypeScript APIs with Mongoose or Prisma, Zod validation and Swagger docs",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"bin": {
|
|
6
|
-
"exp-node-server": "./
|
|
7
|
+
"exp-node-server": "./dist/cli.js"
|
|
7
8
|
},
|
|
8
9
|
"files": [
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"templates"
|
|
10
|
+
"dist",
|
|
11
|
+
"templates",
|
|
12
|
+
"!templates/*/node_modules",
|
|
13
|
+
"!templates/*/dist",
|
|
14
|
+
"!templates/*/coverage",
|
|
15
|
+
"!templates/*/.env"
|
|
12
16
|
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"dev": "tsup --watch",
|
|
20
|
+
"typecheck": "tsc -p .",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
24
|
+
},
|
|
13
25
|
"dependencies": {
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
26
|
+
"@inquirer/select": "^5.2.6",
|
|
27
|
+
"chalk": "^5.6.2",
|
|
28
|
+
"ora": "^9.4.1"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^20.19.43",
|
|
32
|
+
"tsup": "^8.5.1",
|
|
33
|
+
"typescript": "^7.0.2",
|
|
34
|
+
"vitest": "^3.2.7"
|
|
17
35
|
},
|
|
18
36
|
"keywords": [
|
|
19
37
|
"express",
|
|
@@ -28,7 +46,9 @@
|
|
|
28
46
|
"exp-node",
|
|
29
47
|
"server"
|
|
30
48
|
],
|
|
49
|
+
"author": "Abumahid (https://abumahid.me)",
|
|
50
|
+
"homepage": "https://www.npmjs.com/package/exp-node-server",
|
|
31
51
|
"engines": {
|
|
32
|
-
"node": ">=20.
|
|
52
|
+
"node": ">=20.17.0"
|
|
33
53
|
}
|
|
34
54
|
}
|