exp-node-server 2.0.0 → 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/dist/cli.js CHANGED
@@ -1,13 +1,18 @@
1
1
  #!/usr/bin/env node
2
- var c=class extends Error{exitCode;constructor(t,r=1){super(t),this.name="CliError",this.exitCode=r}};import T from"chalk";var a={info:e=>console.log(e),success:e=>console.log(`${T.green("\u2714")} ${e}`),warn:e=>console.warn(`${T.yellow("\u26A0")} ${e}`),error:e=>console.error(`${T.red("\u2716")} ${e}`)};import S from"chalk";import{parseArgs as xe}from"util";import de from"@inquirer/select";import g from"chalk";import m from"fs";import h from"path";import pe from"ora";import j from"path";import{fileURLToPath as ce}from"url";var B=j.dirname(ce(import.meta.url)),C=j.basename(B)==="lib"?j.resolve(B,"../../templates"):j.resolve(B,"../templates");var le={_gitignore:".gitignore"},ue=new Set(["node_modules","dist","coverage",".env"]);async function k(e,{template:t,cwd:r=process.cwd()}={}){let s=h.join(r,e);if(m.existsSync(s))throw new c(`Folder '${e}' already exists.`);let n=t??await me(),i=h.join(C,n);if(!m.existsSync(i))throw new c(`Template '${n}' not found at ${i}.`);let o=pe(`Creating ${n} project...`).start();try{await m.promises.cp(i,s,{recursive:!0,filter:d=>!ue.has(h.basename(d))});for(let[d,f]of Object.entries(le)){let p=h.join(s,d);m.existsSync(p)&&await m.promises.rename(p,h.join(s,f))}await fe(s,e)}catch(d){throw o.fail("Failed to create project"),await m.promises.rm(s,{recursive:!0,force:!0}),d}o.succeed(`Project '${e}' created`),a.info(g.green(`
3
- Next steps:`)),a.info(g.cyan(` cd ${e}`)),a.info(g.cyan(" npm install")),a.info(g.cyan(" cp .env.example .env # then fill in the secrets")),a.info(g.cyan(" docker compose up -d # starts the database")),n==="prisma"&&a.info(g.cyan(" npm run db:migrate")),a.info(g.cyan(" npm run dev")),a.info(g.cyan(" npm test"))}async function me(){return de({message:"Select a project template:",choices:[{name:"Mongoose (MongoDB)",value:"mongoose"},{name:"Prisma (PostgreSQL)",value:"prisma"}]})}async function fe(e,t){let r=h.join(e,"package.json");if(!m.existsSync(r))return;let s=JSON.parse(await m.promises.readFile(r,"utf-8"));s.name=t,await m.promises.writeFile(r,`${JSON.stringify(s,null,2)}
4
- `)}import y from"chalk";import $ from"fs";import l from"path";var _=(e,t)=>{let r=`await ${e}_service.${t}(req);`,s=` const result = ${r}`;return s.length<=100?s:` const result =
5
- ${r}`},D=e=>`import catchAsync from '../../utils/catch_async.js';
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';
6
11
  import manageResponse from '../../utils/manage_response.js';
7
12
  import { ${e}_service } from './${e}.service.js';
8
13
 
9
14
  const get_all_${e} = catchAsync(async (req, res) => {
10
- ${_(e,`get_all_${e}_from_db`)}
15
+ ${P(e,`get_all_${e}_from_db`)}
11
16
  manageResponse(res, {
12
17
  success: true,
13
18
  statusCode: 200,
@@ -18,7 +23,7 @@ ${_(e,`get_all_${e}_from_db`)}
18
23
  });
19
24
 
20
25
  const get_single_${e} = catchAsync(async (req, res) => {
21
- ${_(e,`get_single_${e}_from_db`)}
26
+ ${P(e,`get_single_${e}_from_db`)}
22
27
  manageResponse(res, {
23
28
  success: true,
24
29
  statusCode: 200,
@@ -29,7 +34,7 @@ ${_(e,`get_single_${e}_from_db`)}
29
34
  });
30
35
 
31
36
  const create_${e} = catchAsync(async (req, res) => {
32
- ${_(e,`create_${e}_into_db`)}
37
+ ${P(e,`create_${e}_into_db`)}
33
38
  manageResponse(res, {
34
39
  success: true,
35
40
  statusCode: 201,
@@ -40,7 +45,7 @@ ${_(e,`create_${e}_into_db`)}
40
45
  });
41
46
 
42
47
  const update_${e} = catchAsync(async (req, res) => {
43
- ${_(e,`update_${e}_into_db`)}
48
+ ${P(e,`update_${e}_into_db`)}
44
49
  manageResponse(res, {
45
50
  success: true,
46
51
  statusCode: 200,
@@ -51,7 +56,7 @@ ${_(e,`update_${e}_into_db`)}
51
56
  });
52
57
 
53
58
  const delete_${e} = catchAsync(async (req, res) => {
54
- ${_(e,`delete_${e}_from_db`)}
59
+ ${P(e,`delete_${e}_from_db`)}
55
60
  manageResponse(res, {
56
61
  success: true,
57
62
  statusCode: 200,
@@ -68,29 +73,32 @@ export const ${e}_controller = {
68
73
  update_${e},
69
74
  delete_${e},
70
75
  };
71
- `;var x=(e,t)=>{let r=`router.${e}(${t.join(", ")});`;return r.length<=100?r:`router.${e}(
76
+ `;var q=(e,t)=>{let r=`router.${e}(${t.join(", ")});`;return r.length<=100?r:`router.${e}(
72
77
  ${t.map(s=>` ${s},`).join(`
73
78
  `)}
74
- );`},G=e=>`import { Router } from 'express';
75
- import RequestValidator from '../../middlewares/request_validator.js';
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';
76
82
  import { ${e}_controller } from './${e}.controller.js';
77
83
  import { ${e}_validations } from './${e}.validation.js';
78
84
 
79
85
  const router = Router();
80
86
 
81
- ${x("get",["'/'",`${e}_controller.get_all_${e}`])}
82
- ${x("post",["'/'",`RequestValidator(${e}_validations.create_${e})`,`${e}_controller.create_${e}`])}
83
- ${x("get",["'/:id'",`${e}_controller.get_single_${e}`])}
84
- ${x("patch",["'/:id'",`RequestValidator(${e}_validations.update_${e})`,`${e}_controller.update_${e}`])}
85
- ${x("delete",["'/:id'",`${e}_controller.delete_${e}`])}
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")])}
86
94
 
87
95
  export default router;
88
- `;var A=`{
96
+ `};var L=`{
89
97
  name: 'id',
90
98
  in: 'path',
91
99
  required: true,
92
100
  schema: { type: 'string' },
93
- }`,F=e=>`export const ${e}SwaggerDocs = {
101
+ }`,X=e=>`export const ${e}SwaggerDocs = {
94
102
  '/api/${e}': {
95
103
  post: {
96
104
  tags: ['${e}'],
@@ -100,7 +108,7 @@ export default router;
100
108
  required: true,
101
109
  content: {
102
110
  'application/json': {
103
- example: JSON.stringify({}), // put your request body
111
+ example: JSON.stringify({ name: 'Example ${e}' }),
104
112
  },
105
113
  },
106
114
  },
@@ -140,7 +148,7 @@ export default router;
140
148
  summary: 'Get single ${e}',
141
149
  description: '',
142
150
  parameters: [
143
- ${A},
151
+ ${L},
144
152
  ],
145
153
  responses: {
146
154
  200: { description: '${e} fetched successfully' },
@@ -152,13 +160,13 @@ export default router;
152
160
  summary: 'Update ${e}',
153
161
  description: '',
154
162
  parameters: [
155
- ${A},
163
+ ${L},
156
164
  ],
157
165
  requestBody: {
158
166
  required: true,
159
167
  content: {
160
168
  'application/json': {
161
- example: JSON.stringify({}), // put your request body
169
+ example: JSON.stringify({ name: 'Example ${e}' }),
162
170
  },
163
171
  },
164
172
  },
@@ -172,7 +180,7 @@ export default router;
172
180
  summary: 'Delete ${e}',
173
181
  description: '',
174
182
  parameters: [
175
- ${A},
183
+ ${L},
176
184
  ],
177
185
  responses: {
178
186
  200: { description: '${e} deleted successfully' },
@@ -181,56 +189,75 @@ export default router;
181
189
  },
182
190
  },
183
191
  };
184
- `;var O=e=>`import { z } from 'zod';
192
+ `;var Y=e=>`import { z } from 'zod';
185
193
 
186
- const create_${e} = z.object({});
187
- const update_${e} = z.object({});
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();
188
202
 
189
203
  export const ${e}_validations = {
190
204
  create_${e},
191
205
  update_${e},
192
206
  };
193
- `;var N=e=>`// add the fields of a ${e} here
194
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
195
- export type T_${e} = {};
196
- `;var M=e=>`import { Schema, model } from 'mongoose';
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';
197
212
  import type { T_${e} } from './${e}.interface.js';
198
213
 
199
- const schema = new Schema<T_${e}>({}, { timestamps: true, versionKey: false });
214
+ const schema = new Schema<T_${e}>(
215
+ {
216
+ name: { type: String, required: true, trim: true },
217
+ },
218
+ { timestamps: true, versionKey: false },
219
+ );
200
220
 
201
221
  export const ${e}_model = model('${e}', schema);
202
- `;var L=e=>`import type { Request } from 'express';
222
+ `;var re=e=>`import type { Request } from 'express';
223
+ import type { z } from 'zod';
203
224
  import { AppError } from '../../utils/app_error.js';
204
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}`)}
205
232
 
206
233
  const notFound = () => new AppError('${e} not found', 404);
207
234
 
208
235
  const get_all_${e}_from_db = async (_req: Request) => {
209
236
  // define your own logic here
210
- const result = await ${e}_model.find().lean();
237
+ ${R(`${e}_model`,[".find()",".lean()"])}
211
238
  return result;
212
239
  };
213
240
 
214
241
  const get_single_${e}_from_db = async (req: Request) => {
215
242
  // define your own logic here
216
243
  const { id } = req.params;
217
- const result = await ${e}_model.findById(id).lean();
244
+ ${R(`${e}_model`,[".findById(id)",".lean()"])}
218
245
  if (!result) throw notFound();
219
246
  return result;
220
247
  };
221
248
 
222
249
  const create_${e}_into_db = async (req: Request) => {
223
250
  // define your own logic here
224
- const result = await ${e}_model.create(req.body);
251
+ const payload: CreatePayload = req.body;
252
+ const result = await ${e}_model.create(payload);
225
253
  return result;
226
254
  };
227
255
 
228
256
  const update_${e}_into_db = async (req: Request) => {
229
257
  // define your own logic here
230
258
  const { id } = req.params;
231
- const result = await ${e}_model
232
- .findByIdAndUpdate(id, req.body, { returnDocument: 'after' })
233
- .lean();
259
+ const payload: UpdatePayload = req.body;
260
+ ${R(`${e}_model`,[".findByIdAndUpdate(id, payload, { returnDocument: 'after' })",".lean()"])}
234
261
  if (!result) throw notFound();
235
262
  return result;
236
263
  };
@@ -238,7 +265,7 @@ const update_${e}_into_db = async (req: Request) => {
238
265
  const delete_${e}_from_db = async (req: Request) => {
239
266
  // define your own logic here
240
267
  const { id } = req.params;
241
- const result = await ${e}_model.findByIdAndDelete(id).lean();
268
+ ${R(`${e}_model`,[".findByIdAndDelete(id)",".lean()"])}
242
269
  if (!result) throw notFound();
243
270
  return result;
244
271
  };
@@ -250,34 +277,53 @@ export const ${e}_service = {
250
277
  update_${e}_into_db,
251
278
  delete_${e}_from_db,
252
279
  };
253
- `;var U=e=>`import { Types } from 'mongoose';
280
+ `;var se=(e,{auth:t=!1}={})=>`import { Types } from 'mongoose';
254
281
  import request from 'supertest';
255
- import { describe, expect, it } from 'vitest';
282
+ import { ${t?"beforeEach, describe, expect, it":"describe, expect, it"} } from 'vitest';
256
283
  import app from '../../src/app.js';
257
284
  import { ${e}_model } from '../../src/app/modules/${e}/${e}.schema.js';
258
-
285
+ ${t?`import { bearer, createAccount } from '../helpers/fixtures.js';
286
+ `:""}
287
+ // Starter tests for the generated CRUD routes: extend them with your own rules.
259
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}' };
260
294
 
261
- // Valid request bodies. Fill them in as you add fields to ${e}.schema.ts and
262
- // ${e}.validation.ts (and protect the routes with auth() once they need it).
263
- const createPayload = {};
264
- const updatePayload = {};
295
+ ${t?`let token = '';
265
296
 
266
- /** Inserts a ${e} directly in the database, for the read / update / delete tests. */
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. */
267
305
  const insert = () => ${e}_model.create(createPayload);
268
306
 
269
- describe(BASE, () => {
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":""}
270
308
  it('POST / creates one', async () => {
271
- const res = await request(app).post(BASE).send(createPayload);
309
+ const res = await api().post(BASE).send(createPayload);
272
310
 
273
311
  expect(res.status).toBe(201);
312
+ expect(res.body.data).toMatchObject(createPayload);
274
313
  expect(await ${e}_model.countDocuments()).toBe(1);
275
314
  });
276
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
+
277
323
  it('GET / lists them', async () => {
278
324
  await insert();
279
325
 
280
- const res = await request(app).get(BASE);
326
+ const res = await api().get(BASE);
281
327
 
282
328
  expect(res.status).toBe(200);
283
329
  expect(res.body.data).toHaveLength(1);
@@ -286,20 +332,20 @@ describe(BASE, () => {
286
332
  it('GET /:id returns one', async () => {
287
333
  const doc = await insert();
288
334
 
289
- const res = await request(app).get(\`\${BASE}/\${doc._id}\`);
335
+ const res = await api().get(\`\${BASE}/\${doc._id}\`);
290
336
 
291
337
  expect(res.status).toBe(200);
292
- expect(res.body.data._id).toBe(String(doc._id));
338
+ expect(res.body.data).toMatchObject({ _id: String(doc._id), ...createPayload });
293
339
  });
294
340
 
295
341
  it('GET /:id is 404 when it does not exist', async () => {
296
- const res = await request(app).get(\`\${BASE}/\${new Types.ObjectId()}\`);
342
+ const res = await api().get(\`\${BASE}/\${missingId}\`);
297
343
 
298
344
  expect(res.status).toBe(404);
299
345
  });
300
346
 
301
347
  it('GET /:id is 400 for a malformed id', async () => {
302
- const res = await request(app).get(\`\${BASE}/not-an-id\`);
348
+ const res = await api().get(\`\${BASE}/not-an-id\`);
303
349
 
304
350
  expect(res.status).toBe(400);
305
351
  });
@@ -307,7 +353,7 @@ describe(BASE, () => {
307
353
  it('PATCH /:id updates it', async () => {
308
354
  const doc = await insert();
309
355
 
310
- const res = await request(app).patch(\`\${BASE}/\${doc._id}\`).send(updatePayload);
356
+ const res = await api().patch(\`\${BASE}/\${doc._id}\`).send(updatePayload);
311
357
 
312
358
  expect(res.status).toBe(200);
313
359
  const stored = await ${e}_model.findById(doc._id).lean();
@@ -315,7 +361,7 @@ describe(BASE, () => {
315
361
  });
316
362
 
317
363
  it('PATCH /:id is 404 when it does not exist', async () => {
318
- const res = await request(app).patch(\`\${BASE}/\${new Types.ObjectId()}\`).send(updatePayload);
364
+ const res = await api().patch(\`\${BASE}/\${missingId}\`).send(updatePayload);
319
365
 
320
366
  expect(res.status).toBe(404);
321
367
  });
@@ -323,7 +369,7 @@ describe(BASE, () => {
323
369
  it('DELETE /:id deletes it', async () => {
324
370
  const doc = await insert();
325
371
 
326
- const res = await request(app).delete(\`\${BASE}/\${doc._id}\`);
372
+ const res = await api().delete(\`\${BASE}/\${doc._id}\`);
327
373
 
328
374
  expect(res.status).toBe(200);
329
375
  const stillThere = await ${e}_model.exists({ _id: doc._id });
@@ -331,19 +377,28 @@ describe(BASE, () => {
331
377
  });
332
378
 
333
379
  it('DELETE /:id is 404 when it does not exist', async () => {
334
- const res = await request(app).delete(\`\${BASE}/\${new Types.ObjectId()}\`);
380
+ const res = await api().delete(\`\${BASE}/\${missingId}\`);
335
381
 
336
382
  expect(res.status).toBe(404);
337
383
  });
338
384
  });
339
- `;var V=e=>e.charAt(0).toUpperCase()+e.slice(1);var H=e=>`model ${V(e)} {
385
+ `;var oe=e=>e.charAt(0).toUpperCase()+e.slice(1);var ne=e=>`model ${oe(e)} {
340
386
  id String @id @default(uuid())
387
+ // example field, keep it in sync with ${e}.validation.ts
388
+ name String
341
389
  createdAt DateTime @default(now())
342
390
  updatedAt DateTime @updatedAt
343
391
  }
344
- `;var z=e=>`import type { Request } from 'express';
392
+ `;var ie=e=>`import type { Request } from 'express';
393
+ import type { z } from 'zod';
345
394
  import { prisma } from '../../lib/prisma.js';
346
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}`)}
347
402
 
348
403
  const get_all_${e}_from_db = async (_req: Request) => {
349
404
  // define your own logic here
@@ -354,14 +409,19 @@ const get_all_${e}_from_db = async (_req: Request) => {
354
409
  const get_single_${e}_from_db = async (req: Request) => {
355
410
  // define your own logic here
356
411
  const { id } = req.params as { id: string };
357
- const result = await prisma.${e}.findUnique({ where: { id } });
412
+ const result = await prisma.${e}.findUnique({
413
+ where: { id },
414
+ });
358
415
  if (!result) throw new AppError('${e} not found', 404);
359
416
  return result;
360
417
  };
361
418
 
362
419
  const create_${e}_into_db = async (req: Request) => {
363
420
  // define your own logic here
364
- const result = await prisma.${e}.create({ data: req.body });
421
+ const payload: CreatePayload = req.body;
422
+ const result = await prisma.${e}.create({
423
+ data: payload,
424
+ });
365
425
  return result;
366
426
  };
367
427
 
@@ -370,14 +430,20 @@ const create_${e}_into_db = async (req: Request) => {
370
430
  const update_${e}_into_db = async (req: Request) => {
371
431
  // define your own logic here
372
432
  const { id } = req.params as { id: string };
373
- const result = await prisma.${e}.update({ where: { id }, data: req.body });
433
+ const payload: UpdatePayload = req.body;
434
+ const result = await prisma.${e}.update({
435
+ where: { id },
436
+ data: payload,
437
+ });
374
438
  return result;
375
439
  };
376
440
 
377
441
  const delete_${e}_from_db = async (req: Request) => {
378
442
  // define your own logic here
379
443
  const { id } = req.params as { id: string };
380
- const result = await prisma.${e}.delete({ where: { id } });
444
+ const result = await prisma.${e}.delete({
445
+ where: { id },
446
+ });
381
447
  return result;
382
448
  };
383
449
 
@@ -388,19 +454,20 @@ export const ${e}_service = {
388
454
  update_${e}_into_db,
389
455
  delete_${e}_from_db,
390
456
  };
391
- `;var J=e=>`import { Prisma } from '@prisma/client';
457
+ `;var ae=(e,{auth:t=!1}={})=>`import { Prisma } from '@prisma/client';
392
458
  import request from 'supertest';
393
- import { describe, expect, it } from 'vitest';
459
+ import { ${t?"beforeEach, describe, expect, it":"describe, expect, it"} } from 'vitest';
394
460
  import app from '../../src/app.js';
395
- import { prismaMock } from '../helpers/prisma.js';
461
+ ${t?`import { bearer, makeAccount } from '../helpers/fixtures.js';
462
+ `:""}import { prismaMock } from '../helpers/prisma.js';
396
463
 
464
+ // Starter tests for the generated CRUD routes: extend them with your own rules.
397
465
  const BASE = '/api/${e}';
398
466
  const db = prismaMock.${e};
399
467
 
400
- // Valid request bodies. Fill them in as you add fields to prisma/schema/${e}.prisma
401
- // and ${e}.validation.ts (and protect the routes with auth() once they need it).
402
- const createPayload = {};
403
- const updatePayload = {};
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}' };
404
471
 
405
472
  // a row as the database would return it
406
473
  const row = {
@@ -418,21 +485,38 @@ const recordNotFound = () =>
418
485
  clientVersion: 'test',
419
486
  });
420
487
 
421
- describe(BASE, () => {
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":""}
422
499
  it('POST / creates one', async () => {
423
500
  db.create.mockResolvedValue(row);
424
501
 
425
- const res = await request(app).post(BASE).send(createPayload);
502
+ const res = await api().post(BASE).send(createPayload);
426
503
 
427
504
  expect(res.status).toBe(201);
428
505
  expect(res.body.data).toEqual(json);
429
506
  expect(db.create).toHaveBeenCalledWith({ data: createPayload });
430
507
  });
431
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
+
432
516
  it('GET / lists them', async () => {
433
517
  db.findMany.mockResolvedValue([row]);
434
518
 
435
- const res = await request(app).get(BASE);
519
+ const res = await api().get(BASE);
436
520
 
437
521
  expect(res.status).toBe(200);
438
522
  expect(res.body.data).toEqual([json]);
@@ -441,7 +525,7 @@ describe(BASE, () => {
441
525
  it('GET /:id returns one', async () => {
442
526
  db.findUnique.mockResolvedValue(row);
443
527
 
444
- const res = await request(app).get(\`\${BASE}/\${row.id}\`);
528
+ const res = await api().get(\`\${BASE}/\${row.id}\`);
445
529
 
446
530
  expect(res.status).toBe(200);
447
531
  expect(res.body.data).toEqual(json);
@@ -451,15 +535,15 @@ describe(BASE, () => {
451
535
  it('GET /:id is 404 when it does not exist', async () => {
452
536
  db.findUnique.mockResolvedValue(null);
453
537
 
454
- const res = await request(app).get(\`\${BASE}/\${row.id}\`);
538
+ const res = await api().get(\`\${BASE}/\${row.id}\`);
455
539
 
456
540
  expect(res.status).toBe(404);
457
541
  });
458
542
 
459
543
  it('PATCH /:id updates it', async () => {
460
- db.update.mockResolvedValue(row);
544
+ db.update.mockResolvedValue({ ...row, ...updatePayload });
461
545
 
462
- const res = await request(app).patch(\`\${BASE}/\${row.id}\`).send(updatePayload);
546
+ const res = await api().patch(\`\${BASE}/\${row.id}\`).send(updatePayload);
463
547
 
464
548
  expect(res.status).toBe(200);
465
549
  expect(db.update).toHaveBeenCalledWith({ where: { id: row.id }, data: updatePayload });
@@ -468,7 +552,7 @@ describe(BASE, () => {
468
552
  it('PATCH /:id is 404 when it does not exist', async () => {
469
553
  db.update.mockRejectedValue(recordNotFound());
470
554
 
471
- const res = await request(app).patch(\`\${BASE}/\${row.id}\`).send(updatePayload);
555
+ const res = await api().patch(\`\${BASE}/\${row.id}\`).send(updatePayload);
472
556
 
473
557
  expect(res.status).toBe(404);
474
558
  });
@@ -476,7 +560,7 @@ describe(BASE, () => {
476
560
  it('DELETE /:id deletes it', async () => {
477
561
  db.delete.mockResolvedValue(row);
478
562
 
479
- const res = await request(app).delete(\`\${BASE}/\${row.id}\`);
563
+ const res = await api().delete(\`\${BASE}/\${row.id}\`);
480
564
 
481
565
  expect(res.status).toBe(200);
482
566
  expect(db.delete).toHaveBeenCalledWith({ where: { id: row.id } });
@@ -485,41 +569,45 @@ describe(BASE, () => {
485
569
  it('DELETE /:id is 404 when it does not exist', async () => {
486
570
  db.delete.mockRejectedValue(recordNotFound());
487
571
 
488
- const res = await request(app).delete(\`\${BASE}/\${row.id}\`);
572
+ const res = await api().delete(\`\${BASE}/\${row.id}\`);
489
573
 
490
574
  expect(res.status).toBe(404);
491
575
  });
492
576
  });
493
- `;var ge=/^import\s[\s\S]*?from\s*(['"])[^'"]+\1;?[ \t]*$|^import\s*(['"])[^'"]+\2;?[ \t]*$/gm;function E(e){return{quote:e.match(/^import[\s\S]*?(['"])/m)?.[1]==="'"?"'":'"',jsExtension:/from\s*['"]\.{1,2}\/[^'"]+\.js['"]/.test(e)}}function q(e,t){let r=-1;for(let s of e.matchAll(ge))r=s.index+s[0].length;return r===-1?`${t}
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}
494
578
  ${e}`:`${e.slice(0,r)}
495
- ${t}${e.slice(r)}`}function v(e,t,r,s){let n=e.match(t);if(n?.index===void 0)return null;let i=n.index+n[0].length,o=e.indexOf(r,i);if(o===-1)return null;let d=e.slice(i,o),f=d.match(/\n([ \t]*)$/)?.[1]??"",p=d.match(/\n([ \t]+)\S/)?.[1]??`${f} `,w=d.trimEnd(),ie=w.length>0&&!/[,[{]$/.test(w),ae=`${w}${ie?",":""}
496
- ${p}${s}
497
- ${f}`;return e.slice(0,i)+ae+e.slice(o)}function R(e,t){let r=e.includes(`\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
498
584
  `),s=t(r?e.replace(/\r\n/g,`
499
585
  `):e);return!r||s.status!=="updated"?s:{status:"updated",content:s.content.replace(/\n/g,`\r
500
- `)}}function W(e,t){return R(e,r=>$e(r,t))}function $e(e,t){let{quote:r,jsExtension:s}=E(e),n=`./app/modules/${t}/${t}.route`,i=`${t}Route`,o=e;if(o.includes(n)||(o=q(o,`import ${i} from ${r}${n}${s?".js":""}${r};`)),!new RegExp(`route:\\s*${i}\\b`).test(o)){let d=v(o,/const moduleRoutes\s*=\s*\[/,"]",`{ path: ${r}/${t}${r}, route: ${i} },`);if(d===null)return{status:"failed",reason:"could not find `const moduleRoutes = [...]`"};o=d}return o===e?{status:"unchanged"}:{status:"updated",content:o}}function K(e,t){return R(e,r=>he(r,t))}function he(e,t){let{quote:r,jsExtension:s}=E(e),n=`./app/modules/${t}/${t}.swagger`,i=`${t}SwaggerDocs`,o=e;if(o.includes(n)||(o=q(o,`import { ${i} } from ${r}${n}${s?".js":""}${r};`)),!o.includes(`...${i}`)){let d=v(o,/paths:\s*\{/,"}",`...${i},`);if(d===null)return{status:"failed",reason:"could not find `paths: { ... }`"};o=d}return o===e?{status:"unchanged"}:{status:"updated",content:o}}import Q from"fs";import X from"path";function Y(e,t){for(let r of e)Q.mkdirSync(X.dirname(r.path),{recursive:!0}),Q.writeFileSync(r.path,r.content),a.success(`${X.relative(t,r.path)} created`)}import Z from"fs";import ye from"path";var b=["mongoose","prisma"],P=".projectrc.json",I=e=>typeof e=="string"&&b.includes(e);function ee(e){let t=ye.join(e,P);if(!Z.existsSync(t))throw new c(`${P} not found. Run this command from the root of a project created with exp-node-server.`);let r;try{r=JSON.parse(Z.readFileSync(t,"utf-8"))}catch{throw new c(`${P} is not valid JSON.`)}let s=r?.database;if(!I(s))throw new c(`Invalid "database" in ${P}. Expected one of: ${b.join(", ")}.`);return s}async function re(e,{cwd:t=process.cwd(),force:r=!1}={}){let s=ee(t),n=l.join(t,"src/app/modules",e);if($.existsSync(n)&&!r)throw new c(`Module '${e}' already exists at ${l.relative(t,n)}. Use --force to overwrite it.`);_e(l.join(t,"src/routes.ts"),e);let i=(p,w)=>({path:l.join(n,`${e}.${p}`),content:w}),o=[i("controller.ts",D(e)),i("route.ts",G(e)),i("validation.ts",O(e)),i("swagger.ts",F(e))];s==="mongoose"?o.push(i("interface.ts",N(e)),i("schema.ts",M(e)),i("service.ts",L(e))):o.push(i("service.ts",z(e)),{path:l.join(t,"prisma/schema",`${e}.prisma`),content:H(e)});let d=l.join(t,"tests/routes",`${e}.test.ts`),f=we.some(p=>$.existsSync(l.join(t,p)));f&&o.push({path:d,content:s==="mongoose"?U(e):J(e)}),Y(o,t),f||a.info(y.dim(" no vitest config found, skipped tests/routes test")),te(l.join(t,"src/routes.ts"),p=>W(p,e),"Route registered"),te(l.join(t,"src/swaggerOptions.ts"),p=>K(p,e),"Swagger docs registered"),s==="prisma"&&(a.info(y.blue(`
501
- Next, run:`)),a.info(y.yellow(" npx prisma generate && npx prisma migrate dev"))),f&&a.info(y.blue(`
502
- Test it with: npm test -- tests/routes/${e}.test.ts`)),a.info(y.green(`
503
- Module '${e}' created successfully`))}var we=["vitest.config.ts","vitest.config.mts","vitest.config.js"];function _e(e,t){if(!$.existsSync(e))return;let r=$.readFileSync(e,"utf-8"),s=new RegExp(`path:\\s*['"]/${t}['"]\\s*,\\s*route:\\s*(\\w+)`).exec(r);if(s&&s[1]!==`${t}Route`)throw new c(`The path '/${t}' is already used by ${s[1]} in src/routes.ts. Pick another module name.`)}function te(e,t,r){let s=l.basename(e);if(!$.existsSync(e)){a.warn(`${s} not found, skipping registration`);return}let n=t($.readFileSync(e,"utf-8"));switch(n.status){case"updated":$.writeFileSync(e,n.content),a.success(`${r} in ${s}`);break;case"unchanged":a.info(y.dim(` already registered in ${s}`));break;case"failed":a.warn(`Couldn't update ${s} (${n.reason}). Register it manually.`);break}}function se(e){if(!e)return null;let t=e.trim().toLowerCase().replace(/[-\s]+/g,"_");return/^[a-z][a-z_]*$/.test(t)?t:null}function oe(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 u="exp-node-server",be=`
504
- ${S.bold(u)} - scaffold Express + TypeScript APIs with Mongoose or Prisma
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
505
591
 
506
- ${S.bold("Usage")}
507
- npx ${u} create <project-name> [--template <mongoose|prisma>]
508
- npx ${u} generate <module-name> [--force]
592
+ ${B.bold("Usage")}
593
+ npx ${y} create <project-name> [--template <mongoose|prisma>]
594
+ npx ${y} generate <module-name> [--auth] [--force]
509
595
 
510
- ${S.bold("Commands")}
596
+ ${B.bold("Commands")}
511
597
  create, -c <name> Create a new project from a template
512
598
  generate, -g <name> Generate a module in the current project
513
599
 
514
- ${S.bold("Options")}
515
- -t, --template <name> Template to use with create (${b.join(" | ")})
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)
516
603
  -f, --force Overwrite an existing module when generating
517
604
  -h, --help Show this help
518
605
  -v, --version Show version
519
606
 
520
- ${S.bold("Examples")}
521
- npx ${u} -c my-api
522
- npx ${u} create my-api --template prisma
523
- npx ${u} -g order
524
- `;async function ne(e){let{values:t,positionals:r}=Se(e);if(t.version){console.log("2.0.0");return}let s=je(t,r);if(t.help||!s){console.log(be);return}if(s.kind==="create"){let i=oe(s.name);if(!i)throw new c(s.name?`Invalid project name '${s.name}'. Use letters, numbers and dashes.`:"Project name required. Example: exp-node-server -c my-api");let{template:o}=t;if(o!==void 0&&!I(o))throw new c(`Unknown template '${o}'. Use one of: ${b.join(", ")}.`);await k(i,{template:o});return}let n=se(s.name);if(!n)throw new c(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 re(n,{force:t.force??!1})}function Se(e){try{return xe({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"},help:{type:"boolean",short:"h"},version:{type:"boolean",short:"v"}}})}catch(t){let r=t instanceof Error?t.message:String(t);throw new c(`${r}
525
- Run '${u} --help' for usage.`)}}function je(e,t){if(e.create!==void 0&&e.generate!==void 0)throw new c("Use either -c (create) or -g (generate), not both.");if((e.create!==void 0||e.generate!==void 0)&&t.length>0)throw new c(`Unexpected argument '${t[0]}'. Run '${u} --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,...n]=t;if(r===void 0)return null;if(n.length>0)throw new c(`Unexpected argument '${n[0]}'. Run '${u} --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 c(`Unknown command '${r}'. Run '${u} --help' for usage.`)}}try{await ne(process.argv.slice(2))}catch(e){e instanceof c?(a.error(e.message),process.exitCode=e.exitCode):e instanceof Error&&e.name==="ExitPromptError"?process.exitCode=130:(a.error("Unexpected error:"),console.error(e),process.exitCode=1)}
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)}