simpledi-app-generator 0.0.7 → 0.0.9

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.
Files changed (37) hide show
  1. package/README.md +9 -9
  2. package/dist/cli.js +3 -4
  3. package/dist/cli.js.map +1 -1
  4. package/dist/create_module.d.ts.map +1 -1
  5. package/dist/create_module.js +3 -0
  6. package/dist/create_module.js.map +1 -1
  7. package/dist/generate_crud_use_cases.d.ts.map +1 -1
  8. package/dist/generate_crud_use_cases.js +571 -130
  9. package/dist/generate_crud_use_cases.js.map +1 -1
  10. package/dist/generate_skeleton.d.ts.map +1 -1
  11. package/dist/generate_skeleton.js +57 -2
  12. package/dist/generate_skeleton.js.map +1 -1
  13. package/dist/templates/schema.ts +2 -0
  14. package/dist/templates/src/core/user/IUserRepository.ts +8 -0
  15. package/dist/templates/src/core/user/IUserService.ts +8 -0
  16. package/dist/templates/src/core/user/User.ts +71 -0
  17. package/dist/templates/src/core/user/UserModule.ts +7 -0
  18. package/dist/templates/src/core/user/UserRepository.spec.ts +63 -0
  19. package/dist/templates/src/core/user/UserRepository.ts +31 -0
  20. package/dist/templates/src/core/user/UserRepositoryModule.ts +15 -0
  21. package/dist/templates/src/core/user/UserService.ts +34 -0
  22. package/dist/templates/src/core/user/UserServiceModule.ts +14 -0
  23. package/dist/templates/src/core/user/baseZodUserSchema.ts +31 -0
  24. package/dist/templates/src/lib/functions/getContextUser.ts +5 -0
  25. package/dist/templates/src/lib/functions/test-related/createSignedUpUser.ts +44 -0
  26. package/dist/templates/src/lib/functions/test-related/getOneUserSignupData.ts +28 -0
  27. package/dist/templates/src/lib/functions/test-related/getTestServer.ts +28 -0
  28. package/dist/templates/src/lib/types/AdminRoleEnum.ts +6 -0
  29. package/dist/templates/src/lib/types/AnyRoleEnum.ts +5 -0
  30. package/dist/templates/src/lib/types/PhoneNumberTypeEnum.ts +7 -0
  31. package/dist/templates/src/lib/types/UserRoleEnum.ts +4 -0
  32. package/dist/templates/src/lib/types/UserTypeEnum.ts +4 -0
  33. package/dist/templates/src/middlewares/authGuard.ts +46 -0
  34. package/dist/templates/src/middlewares/index.ts +2 -0
  35. package/dist/templates/src/middlewares/roleGuard.ts +16 -0
  36. package/package.json +1 -1
  37. package/user-guide.md +16 -4
@@ -24,14 +24,19 @@ export async function generateCrudUseCases(EntityName, entityName, kebabName, sr
24
24
  await generateDeleteUseCase(useCaseBaseDir, config);
25
25
  // Generate aggregator module
26
26
  await generateUseCaseAggregatorModule(useCaseBaseDir, config);
27
- // Auto-register in UseCaseModule.ts and main.routes.ts
27
+ // Auto-register in UseCaseModule.ts
28
28
  await registerInUseCaseModule(srcDir, config);
29
- await registerRoutes(srcDir, config);
29
+ // Phase 3: Generate Scaffolded E2E Tests
30
+ await generateCreateE2ETest(useCaseBaseDir, config);
31
+ await generateUpdateE2ETest(useCaseBaseDir, config);
32
+ await generateGetE2ETest(useCaseBaseDir, config);
33
+ await generateListE2ETest(useCaseBaseDir, config);
34
+ await generateDeleteE2ETest(useCaseBaseDir, config);
30
35
  console.log(`\n✅ CRUD use cases generated for ${EntityName}!`);
31
36
  }
32
37
  // ============ CREATE USE CASE ============
33
38
  async function generateCreateUseCase(baseDir, config) {
34
- const { EntityName, entityName, kebabName, TOKEN_BASE } = config;
39
+ const { EntityName, entityName, kebabName, TOKEN_BASE, pluralKebab } = config;
35
40
  const useCaseName = `Create${EntityName}`;
36
41
  const useCaseDir = join(baseDir, `create-${kebabName}`);
37
42
  const inputsDir = join(useCaseDir, 'inputs');
@@ -111,28 +116,42 @@ import { StatusCodes } from 'http-status-codes';
111
116
  import { inject } from '@kanian77/simple-di';
112
117
  import { ${useCaseName}, ${toUpperSnakeCase(useCaseName)}_USE_CASE_TOKEN } from './${useCaseName}';
113
118
  import { ${useCaseName}Failure } from './outputs/${useCaseName}Failure';
114
- import type { ${useCaseName}Input } from './inputs/${useCaseName}Input';
119
+ import { ${EntityName}InsertSchema } from '@root/core/${kebabName}/${EntityName}';
120
+ import { getContextUser } from '@root/lib/functions/getContextUser';
121
+ import { authGuard } from '@root/middlewares/authGuard';
122
+ import { roleGuard } from '@root/middlewares/roleGuard';
123
+ import { AdminRoleEnum } from '@root/lib';
115
124
 
116
125
  const ${toCamelCase(useCaseName)}Routes = new Hono();
126
+ const ${toCamelCase(useCaseName)}RoutesPath = '/${pluralKebab}';
117
127
 
118
- ${toCamelCase(useCaseName)}Routes.post('/', async (c) => {
119
- try {
120
- const input = await c.req.json<${useCaseName}Input>();
121
- const useCase = inject<${useCaseName}>(${toUpperSnakeCase(useCaseName)}_USE_CASE_TOKEN);
122
- const result = await useCase.execute(input);
123
- return c.json(result, StatusCodes.CREATED);
124
- } catch (e) {
125
- console.error('Error in ${toCamelCase(useCaseName)}Routes:', e);
126
- return c.json(
127
- new ${useCaseName}Failure('Internal Server Error'),
128
- StatusCodes.INTERNAL_SERVER_ERROR,
129
- );
130
- }
131
- });
128
+ ${toCamelCase(useCaseName)}Routes.post(
129
+ ${toCamelCase(useCaseName)}RoutesPath,
130
+ authGuard(),
131
+ roleGuard([AdminRoleEnum.ADMIN]),
132
+ async (c) => {
133
+ try {
134
+ const rawInput = await c.req.json();
135
+ const user = getContextUser(c);
136
+ if (user) {
137
+ rawInput.createdBy = user.id;
138
+ }
132
139
 
133
- const ${toCamelCase(useCaseName)}RoutesPath = '/';
140
+ const input = ${EntityName}InsertSchema.parse(rawInput);
141
+ const useCase = inject<${useCaseName}>(${toUpperSnakeCase(useCaseName)}_USE_CASE_TOKEN);
142
+ const result = await useCase.execute(input);
143
+ return c.json(result, StatusCodes.CREATED);
144
+ } catch (e) {
145
+ console.error('Error in ${toCamelCase(useCaseName)}Routes:', e);
146
+ return c.json(
147
+ new ${useCaseName}Failure('Internal Server Error'),
148
+ StatusCodes.INTERNAL_SERVER_ERROR,
149
+ );
150
+ }
151
+ }
152
+ );
134
153
 
135
- export { ${toCamelCase(useCaseName)}Routes, ${toCamelCase(useCaseName)}RoutesPath };
154
+ export { ${toCamelCase(useCaseName)}Routes as Route, ${toCamelCase(useCaseName)}RoutesPath as Path };
136
155
  `;
137
156
  await writeFile(join(inputsDir, `${useCaseName}Input.ts`), inputContent);
138
157
  await writeFile(join(outputsDir, `${useCaseName}Success.ts`), successContent);
@@ -143,7 +162,7 @@ export { ${toCamelCase(useCaseName)}Routes, ${toCamelCase(useCaseName)}RoutesPat
143
162
  }
144
163
  // ============ UPDATE USE CASE ============
145
164
  async function generateUpdateUseCase(baseDir, config) {
146
- const { EntityName, entityName, kebabName, TOKEN_BASE } = config;
165
+ const { EntityName, entityName, kebabName, TOKEN_BASE, pluralKebab } = config;
147
166
  const useCaseName = `Update${EntityName}`;
148
167
  const useCaseDir = join(baseDir, `update-${kebabName}`);
149
168
  const inputsDir = join(useCaseDir, 'inputs');
@@ -227,29 +246,45 @@ import { inject } from '@kanian77/simple-di';
227
246
  import { ${useCaseName}, ${toUpperSnakeCase(useCaseName)}_USE_CASE_TOKEN } from './${useCaseName}';
228
247
  import { ${useCaseName}Failure } from './outputs/${useCaseName}Failure';
229
248
  import type { ${useCaseName}Input } from './inputs/${useCaseName}Input';
249
+ import { ${EntityName}UpdateSchema } from '@root/core/${kebabName}/${EntityName}';
250
+ import { getContextUser } from '@root/lib/functions/getContextUser';
251
+ import { authGuard } from '@root/middlewares/authGuard';
252
+ import { roleGuard } from '@root/middlewares/roleGuard';
253
+ import { AdminRoleEnum } from '@root/lib';
230
254
 
231
255
  const ${toCamelCase(useCaseName)}Routes = new Hono();
256
+ const ${toCamelCase(useCaseName)}RoutesPath = '/${pluralKebab}/:id';
232
257
 
233
- ${toCamelCase(useCaseName)}Routes.put('/:id', async (c) => {
234
- try {
235
- const id = c.req.param('id');
236
- const data = await c.req.json();
237
- const input: ${useCaseName}Input = { id, data };
238
- const useCase = inject<${useCaseName}>(${toUpperSnakeCase(useCaseName)}_USE_CASE_TOKEN);
239
- const result = await useCase.execute(input);
240
- return c.json(result, StatusCodes.OK);
241
- } catch (e) {
242
- console.error('Error in ${toCamelCase(useCaseName)}Routes:', e);
243
- return c.json(
244
- new ${useCaseName}Failure('Internal Server Error'),
245
- StatusCodes.INTERNAL_SERVER_ERROR,
246
- );
247
- }
248
- });
258
+ ${toCamelCase(useCaseName)}Routes.put(
259
+ ${toCamelCase(useCaseName)}RoutesPath,
260
+ authGuard(),
261
+ roleGuard([AdminRoleEnum.ADMIN]),
262
+ async (c) => {
263
+ try {
264
+ const id = c.req.param('id');
265
+ const rawBody = await c.req.json();
249
266
 
250
- const ${toCamelCase(useCaseName)}RoutesPath = '/';
267
+ const user = getContextUser(c);
268
+ if (user) {
269
+ rawBody.updatedBy = user.id;
270
+ }
251
271
 
252
- export { ${toCamelCase(useCaseName)}Routes, ${toCamelCase(useCaseName)}RoutesPath };
272
+ const data = ${EntityName}UpdateSchema.parse(rawBody);
273
+ const input: ${useCaseName}Input = { id, data };
274
+ const useCase = inject<${useCaseName}>(${toUpperSnakeCase(useCaseName)}_USE_CASE_TOKEN);
275
+ const result = await useCase.execute(input);
276
+ return c.json(result, StatusCodes.OK);
277
+ } catch (e) {
278
+ console.error('Error in ${toCamelCase(useCaseName)}Routes:', e);
279
+ return c.json(
280
+ new ${useCaseName}Failure('Internal Server Error'),
281
+ StatusCodes.INTERNAL_SERVER_ERROR,
282
+ );
283
+ }
284
+ }
285
+ );
286
+
287
+ export { ${toCamelCase(useCaseName)}Routes as Route, ${toCamelCase(useCaseName)}RoutesPath as Path };
253
288
  `;
254
289
  await writeFile(join(inputsDir, `${useCaseName}Input.ts`), inputContent);
255
290
  await writeFile(join(outputsDir, `${useCaseName}Success.ts`), successContent);
@@ -260,7 +295,7 @@ export { ${toCamelCase(useCaseName)}Routes, ${toCamelCase(useCaseName)}RoutesPat
260
295
  }
261
296
  // ============ GET USE CASE ============
262
297
  async function generateGetUseCase(baseDir, config) {
263
- const { EntityName, entityName, kebabName, TOKEN_BASE } = config;
298
+ const { EntityName, entityName, kebabName, TOKEN_BASE, pluralKebab } = config;
264
299
  const useCaseName = `Get${EntityName}`;
265
300
  const useCaseDir = join(baseDir, `get-${kebabName}`);
266
301
  const outputsDir = join(useCaseDir, 'outputs');
@@ -334,8 +369,9 @@ import { ${useCaseName}, ${toUpperSnakeCase(useCaseName)}_USE_CASE_TOKEN } from
334
369
  import { ${useCaseName}Failure } from './outputs/${useCaseName}Failure';
335
370
 
336
371
  const ${toCamelCase(useCaseName)}Routes = new Hono();
372
+ const ${toCamelCase(useCaseName)}RoutesPath = '/${pluralKebab}/:id';
337
373
 
338
- ${toCamelCase(useCaseName)}Routes.get('/:id', async (c) => {
374
+ ${toCamelCase(useCaseName)}Routes.get(${toCamelCase(useCaseName)}RoutesPath, async (c) => {
339
375
  try {
340
376
  const id = c.req.param('id');
341
377
  const useCase = inject<${useCaseName}>(${toUpperSnakeCase(useCaseName)}_USE_CASE_TOKEN);
@@ -350,9 +386,7 @@ ${toCamelCase(useCaseName)}Routes.get('/:id', async (c) => {
350
386
  }
351
387
  });
352
388
 
353
- const ${toCamelCase(useCaseName)}RoutesPath = '/';
354
-
355
- export { ${toCamelCase(useCaseName)}Routes, ${toCamelCase(useCaseName)}RoutesPath };
389
+ export { ${toCamelCase(useCaseName)}Routes as Route, ${toCamelCase(useCaseName)}RoutesPath as Path };
356
390
  `;
357
391
  await writeFile(join(outputsDir, `${useCaseName}Success.ts`), successContent);
358
392
  await writeFile(join(outputsDir, `${useCaseName}Failure.ts`), failureContent);
@@ -362,7 +396,7 @@ export { ${toCamelCase(useCaseName)}Routes, ${toCamelCase(useCaseName)}RoutesPat
362
396
  }
363
397
  // ============ LIST USE CASE ============
364
398
  async function generateListUseCase(baseDir, config) {
365
- const { EntityName, entityName, kebabName, TOKEN_BASE, pluralPascal } = config;
399
+ const { EntityName, entityName, kebabName, TOKEN_BASE, pluralPascal, pluralKebab, } = config;
366
400
  const useCaseName = `List${pluralPascal}`;
367
401
  const useCaseDir = join(baseDir, `list-${config.pluralKebab}`);
368
402
  const outputsDir = join(useCaseDir, 'outputs');
@@ -436,8 +470,9 @@ import { ${useCaseName}, ${toUpperSnakeCase(useCaseName)}_USE_CASE_TOKEN } from
436
470
  import { ${useCaseName}Failure } from './outputs/${useCaseName}Failure';
437
471
 
438
472
  const ${toCamelCase(useCaseName)}Routes = new Hono();
473
+ const ${toCamelCase(useCaseName)}RoutesPath = '/${pluralKebab}';
439
474
 
440
- ${toCamelCase(useCaseName)}Routes.get('/', async (c) => {
475
+ ${toCamelCase(useCaseName)}Routes.get(${toCamelCase(useCaseName)}RoutesPath, async (c) => {
441
476
  try {
442
477
  const useCase = inject<${useCaseName}>(${toUpperSnakeCase(useCaseName)}_USE_CASE_TOKEN);
443
478
  const result = await useCase.execute();
@@ -451,9 +486,7 @@ ${toCamelCase(useCaseName)}Routes.get('/', async (c) => {
451
486
  }
452
487
  });
453
488
 
454
- const ${toCamelCase(useCaseName)}RoutesPath = '/';
455
-
456
- export { ${toCamelCase(useCaseName)}Routes, ${toCamelCase(useCaseName)}RoutesPath };
489
+ export { ${toCamelCase(useCaseName)}Routes as Route, ${toCamelCase(useCaseName)}RoutesPath as Path };
457
490
  `;
458
491
  await writeFile(join(outputsDir, `${useCaseName}Success.ts`), successContent);
459
492
  await writeFile(join(outputsDir, `${useCaseName}Failure.ts`), failureContent);
@@ -463,7 +496,7 @@ export { ${toCamelCase(useCaseName)}Routes, ${toCamelCase(useCaseName)}RoutesPat
463
496
  }
464
497
  // ============ DELETE USE CASE ============
465
498
  async function generateDeleteUseCase(baseDir, config) {
466
- const { EntityName, entityName, kebabName, TOKEN_BASE } = config;
499
+ const { EntityName, entityName, kebabName, TOKEN_BASE, pluralKebab } = config;
467
500
  const useCaseName = `Delete${EntityName}`;
468
501
  const useCaseDir = join(baseDir, `delete-${kebabName}`);
469
502
  const outputsDir = join(useCaseDir, 'outputs');
@@ -472,7 +505,7 @@ async function generateDeleteUseCase(baseDir, config) {
472
505
  // Success Output
473
506
  const successContent = `import { SuccessfullOperation } from '@root/lib';
474
507
 
475
- export type ${useCaseName}Payload = { id: string; deleted: boolean };
508
+ export type ${useCaseName}Payload = { id: string; deleted: boolean; softDelete: boolean };
476
509
 
477
510
  export class ${useCaseName}Success extends SuccessfullOperation {
478
511
  constructor(
@@ -512,9 +545,9 @@ export class ${useCaseName} implements IUseCase {
512
545
  private readonly ${entityName}Service: I${EntityName}Service,
513
546
  ) {}
514
547
 
515
- async execute(id: string): Promise<${useCaseName}Success> {
516
- await this.${entityName}Service.delete(id);
517
- const result: ${useCaseName}Payload = { id, deleted: true };
548
+ async execute(id: string, softDelete: boolean = true): Promise<${useCaseName}Success> {
549
+ await this.${entityName}Service.delete(id, softDelete);
550
+ const result: ${useCaseName}Payload = { id, deleted: true, softDelete };
518
551
  return new ${useCaseName}Success(result);
519
552
  }
520
553
  }
@@ -535,27 +568,45 @@ import { StatusCodes } from 'http-status-codes';
535
568
  import { inject } from '@kanian77/simple-di';
536
569
  import { ${useCaseName}, ${toUpperSnakeCase(useCaseName)}_USE_CASE_TOKEN } from './${useCaseName}';
537
570
  import { ${useCaseName}Failure } from './outputs/${useCaseName}Failure';
571
+ import { authGuard } from '@root/middlewares/authGuard';
572
+ import { roleGuard } from '@root/middlewares/roleGuard';
573
+ import { AdminRoleEnum } from '@root/lib';
538
574
 
539
575
  const ${toCamelCase(useCaseName)}Routes = new Hono();
576
+ const ${toCamelCase(useCaseName)}RoutesPath = '/${pluralKebab}/:id';
540
577
 
541
- ${toCamelCase(useCaseName)}Routes.delete('/:id', async (c) => {
542
- try {
543
- const id = c.req.param('id');
544
- const useCase = inject<${useCaseName}>(${toUpperSnakeCase(useCaseName)}_USE_CASE_TOKEN);
545
- const result = await useCase.execute(id);
546
- return c.json(result, StatusCodes.OK);
547
- } catch (e) {
548
- console.error('Error in ${toCamelCase(useCaseName)}Routes:', e);
549
- return c.json(
550
- new ${useCaseName}Failure('Internal Server Error'),
551
- StatusCodes.INTERNAL_SERVER_ERROR,
552
- );
553
- }
554
- });
578
+ ${toCamelCase(useCaseName)}Routes.delete(
579
+ ${toCamelCase(useCaseName)}RoutesPath,
580
+ authGuard(),
581
+ roleGuard([AdminRoleEnum.ADMIN]),
582
+ async (c) => {
583
+ try {
584
+ const id = c.req.param('id');
585
+
586
+ let softDelete = true;
587
+ try {
588
+ const body = await c.req.json();
589
+ if (typeof body.softDelete === 'boolean') {
590
+ softDelete = body.softDelete;
591
+ }
592
+ } catch (e) {
593
+ // Body is likely empty, default to softDelete = true
594
+ }
555
595
 
556
- const ${toCamelCase(useCaseName)}RoutesPath = '/';
596
+ const useCase = inject<${useCaseName}>(${toUpperSnakeCase(useCaseName)}_USE_CASE_TOKEN);
597
+ const result = await useCase.execute(id, softDelete);
598
+ return c.json(result, StatusCodes.OK);
599
+ } catch (e) {
600
+ console.error('Error in ${toCamelCase(useCaseName)}Routes:', e);
601
+ return c.json(
602
+ new ${useCaseName}Failure('Internal Server Error'),
603
+ StatusCodes.INTERNAL_SERVER_ERROR,
604
+ );
605
+ }
606
+ }
607
+ );
557
608
 
558
- export { ${toCamelCase(useCaseName)}Routes, ${toCamelCase(useCaseName)}RoutesPath };
609
+ export { ${toCamelCase(useCaseName)}Routes as Route, ${toCamelCase(useCaseName)}RoutesPath as Path };
559
610
  `;
560
611
  await writeFile(join(outputsDir, `${useCaseName}Success.ts`), successContent);
561
612
  await writeFile(join(outputsDir, `${useCaseName}Failure.ts`), failureContent);
@@ -644,66 +695,456 @@ async function registerInUseCaseModule(srcDir, config) {
644
695
  await writeFile(useCaseModulePath, content);
645
696
  console.log(`Updated: src/use-case/UseCaseModule.ts`);
646
697
  }
647
- // ============ REGISTER ROUTES ============
648
- async function registerRoutes(srcDir, config) {
649
- const { EntityName, entityName, kebabName, pluralPascal, pluralKebab } = config;
650
- const mainRoutesPath = join(srcDir, 'main.routes.ts');
651
- if (!existsSync(mainRoutesPath)) {
652
- console.warn('Warning: src/main.routes.ts not found');
653
- return;
654
- }
655
- let content = await readFile(mainRoutesPath, 'utf8');
656
- // Import all route handlers
657
- const routeImports = `import { create${EntityName}Routes } from './use-case/${kebabName}/create-${kebabName}/create${EntityName}Routes';
658
- import { update${EntityName}Routes } from './use-case/${kebabName}/update-${kebabName}/update${EntityName}Routes';
659
- import { get${EntityName}Routes } from './use-case/${kebabName}/get-${kebabName}/get${EntityName}Routes';
660
- import { list${pluralPascal}Routes } from './use-case/${kebabName}/list-${pluralKebab}/list${pluralPascal}Routes';
661
- import { delete${EntityName}Routes } from './use-case/${kebabName}/delete-${kebabName}/delete${EntityName}Routes';`;
662
- if (content.includes(`create${EntityName}Routes`)) {
663
- return; // Already registered
664
- }
665
- // Add imports after last import
666
- const lastImportIndex = content.lastIndexOf('import ');
667
- if (lastImportIndex !== -1) {
668
- let searchPos = lastImportIndex;
669
- while (searchPos < content.length) {
670
- if (content[searchPos] === ';') {
671
- content =
672
- content.slice(0, searchPos + 1) +
673
- '\n' +
674
- routeImports +
675
- content.slice(searchPos + 1);
676
- break;
677
- }
678
- searchPos++;
679
- }
680
- }
681
- // Add route registrations before export
682
- const routeRegistrations = `
683
- // ${EntityName} CRUD routes
684
- const ${entityName}Routes = new Hono();
685
- ${entityName}Routes.route('/', create${EntityName}Routes);
686
- ${entityName}Routes.route('/', update${EntityName}Routes);
687
- ${entityName}Routes.route('/', get${EntityName}Routes);
688
- ${entityName}Routes.route('/', list${pluralPascal}Routes);
689
- ${entityName}Routes.route('/', delete${EntityName}Routes);
690
- mainRoutes.route('/${pluralKebab}', ${entityName}Routes);
698
+ // ============ E2E TESTS GENERATION ============
699
+ async function generateCreateE2ETest(baseDir, config) {
700
+ const { EntityName, entityName, kebabName, pluralKebab } = config;
701
+ const useCaseDir = join(baseDir, `create-${kebabName}`);
702
+ const testContent = `import { afterAll, beforeAll, describe, expect, it } from 'bun:test';
703
+ import { EnvFileNames, UserRoleEnum, UserTypeEnum } from '@root/lib';
704
+ import { bootstrap, inject, Module } from '@kanian77/simple-di';
705
+ import { UseCaseModule } from '@root/use-case/UseCaseModule';
706
+ import { getConfigModule } from 'config/getConfigModule';
707
+ import { getDbModule } from 'db/getDbModule';
708
+ import { CoreModule } from '@root/core/CoreModule';
709
+ import { DB_SERVICE, type DbService } from 'db/DbService';
710
+ import { APP, AppModule } from '@root/AppModule';
711
+ import type { Hono } from 'hono';
712
+ import * as schema from '@root/schema';
713
+ import { eq } from 'drizzle-orm';
714
+ import { getTestServer, type TestServer } from '@root/lib/functions/test-related/getTestServer';
715
+ import { createOneSignedUpUser, deleteCreatedSignedUsers } from '@root/lib/functions/test-related/createSignedUpUser';
716
+ import { USER_REPOSITORY_INTERFACE, type UserRepository } from '@root/core/user/IUserRepository';
717
+ import { UserModule } from '@root/core/user/UserModule';
718
+
719
+ describe('Create${EntityName} e2e', () => {
720
+ let dbService: DbService;
721
+ let server: TestServer;
722
+ let app: Hono;
723
+ let userRepository: UserRepository;
724
+
725
+ beforeAll(async () => {
726
+ const TestModule = new Module({
727
+ imports: [
728
+ AppModule,
729
+ getConfigModule(EnvFileNames.TESTING),
730
+ getDbModule(),
731
+ CoreModule,
732
+ UserModule,
733
+ UseCaseModule,
734
+ ],
735
+ });
736
+ bootstrap(TestModule);
737
+ dbService = inject<DbService>(DB_SERVICE);
738
+ app = inject<Hono>(APP);
739
+ userRepository = inject(USER_REPOSITORY_INTERFACE);
740
+ server = await getTestServer(app);
741
+ });
742
+
743
+ afterAll(async () => {
744
+ await dbService.getDb().delete(schema.${entityName}Schema).execute();
745
+ await deleteCreatedSignedUsers(userRepository);
746
+ });
747
+
748
+ it('fails if unauthenticated', async () => {
749
+ const req = await server.client.request('/${pluralKebab}', {
750
+ method: 'POST',
751
+ body: JSON.stringify({ /* TODO: Add valid payload */ }),
752
+ headers: { 'Content-Type': 'application/json' },
753
+ });
754
+ expect(req.status).toBe(401);
755
+ });
756
+
757
+ it('fails with fake generated ID for createdBy', async () => {
758
+ const { token } = await createOneSignedUpUser(userRepository, {
759
+ userType: UserTypeEnum.ADMIN,
760
+ role: UserRoleEnum.AUTHENTICATED,
761
+ });
762
+
763
+ // Attempting to forge createdBy should be overwritten by the backend Context Injection
764
+ const maliciousPayload = {
765
+ // TODO: Add required payload properties
766
+ createdBy: 'fake-malicious-id'
767
+ };
768
+
769
+ const req = await server.client.request('/${pluralKebab}', {
770
+ method: 'POST',
771
+ body: JSON.stringify(maliciousPayload),
772
+ headers: {
773
+ 'Content-Type': 'application/json',
774
+ Authorization: \`Bearer \${token}\`,
775
+ },
776
+ });
777
+
778
+ expect(req.status).toBe(201);
779
+ const body: any = await req.json();
780
+
781
+ // Verify creation succeeded but createdBy was securely overwritten
782
+ expect(body.result.createdBy).not.toBe('fake-malicious-id');
783
+
784
+ // Direct DB check
785
+ const row = await dbService.getDb().query.${entityName}Schema.findFirst({
786
+ where: eq(schema.${entityName}Schema.id, body.result.id)
787
+ });
788
+ expect(row).toBeDefined();
789
+ expect(row!.createdBy).not.toBe('fake-malicious-id');
790
+ });
791
+ });
691
792
  `;
692
- const exportIndex = content.indexOf('export { mainRoutes }');
693
- if (exportIndex !== -1) {
694
- content =
695
- content.slice(0, exportIndex) +
696
- routeRegistrations +
697
- '\n' +
698
- content.slice(exportIndex);
699
- }
700
- // Check if Hono is imported
701
- if (!content.includes('import { Hono }') &&
702
- !content.includes('import {Hono}')) {
703
- // Add Hono import at top if not present
704
- content = "import { Hono } from 'hono';\n" + content;
705
- }
706
- await writeFile(mainRoutesPath, content);
707
- console.log(`Updated: src/main.routes.ts with ${EntityName} CRUD routes`);
793
+ await writeFile(join(useCaseDir, `Create${EntityName}.e2e.spec.ts`), testContent);
794
+ }
795
+ async function generateUpdateE2ETest(baseDir, config) {
796
+ const { EntityName, entityName, kebabName, pluralKebab } = config;
797
+ const useCaseDir = join(baseDir, `update-${kebabName}`);
798
+ const testContent = `import { afterAll, beforeAll, describe, expect, it } from 'bun:test';
799
+ import { EnvFileNames, UserRoleEnum, UserTypeEnum } from '@root/lib';
800
+ import { bootstrap, inject, Module } from '@kanian77/simple-di';
801
+ import { UseCaseModule } from '@root/use-case/UseCaseModule';
802
+ import { getConfigModule } from 'config/getConfigModule';
803
+ import { getDbModule } from 'db/getDbModule';
804
+ import { CoreModule } from '@root/core/CoreModule';
805
+ import { DB_SERVICE, type DbService } from 'db/DbService';
806
+ import { APP, AppModule } from '@root/AppModule';
807
+ import type { Hono } from 'hono';
808
+ import * as schema from '@root/schema';
809
+ import { eq } from 'drizzle-orm';
810
+ import { getTestServer, type TestServer } from '@root/lib/functions/test-related/getTestServer';
811
+ import { createOneSignedUpUser, deleteCreatedSignedUsers } from '@root/lib/functions/test-related/createSignedUpUser';
812
+ import { USER_REPOSITORY_INTERFACE, type UserRepository } from '@root/core/user/IUserRepository';
813
+ import { UserModule } from '@root/core/user/UserModule';
814
+
815
+ describe('Update${EntityName} e2e', () => {
816
+ let dbService: DbService;
817
+ let server: TestServer;
818
+ let app: Hono;
819
+ let targetId: string;
820
+ let userRepository: UserRepository;
821
+
822
+ beforeAll(async () => {
823
+ const TestModule = new Module({
824
+ imports: [
825
+ AppModule,
826
+ getConfigModule(EnvFileNames.TESTING),
827
+ getDbModule(),
828
+ CoreModule,
829
+ UserModule,
830
+ UseCaseModule,
831
+ ],
832
+ });
833
+ bootstrap(TestModule);
834
+ dbService = inject<DbService>(DB_SERVICE);
835
+ app = inject<Hono>(APP);
836
+ userRepository = inject(USER_REPOSITORY_INTERFACE);
837
+ server = await getTestServer(app);
838
+
839
+ // TODO: Seed a target entity here and save targetId
840
+ });
841
+
842
+ afterAll(async () => {
843
+ await dbService.getDb().delete(schema.${entityName}Schema).execute();
844
+ await deleteCreatedSignedUsers(userRepository);
845
+ });
846
+
847
+ it('fails if unauthenticated', async () => {
848
+ const req = await server.client.request(\`/\${pluralKebab}/\${targetId}\`, {
849
+ method: 'PUT',
850
+ body: JSON.stringify({ /* TODO: valid update payload */ }),
851
+ headers: { 'Content-Type': 'application/json' },
852
+ });
853
+ expect(req.status).toBe(401);
854
+ });
855
+
856
+ it('successfully updates and overrides updatedBy context', async () => {
857
+ const { token } = await createOneSignedUpUser(userRepository, {
858
+ userType: UserTypeEnum.ADMIN,
859
+ role: UserRoleEnum.AUTHENTICATED,
860
+ });
861
+
862
+ const payload = {
863
+ // TODO: Add properties to update
864
+ updatedBy: 'hacker-id' // Should be discarded
865
+ };
866
+
867
+ const req = await server.client.request(\`/\${pluralKebab}/\${targetId}\`, {
868
+ method: 'PUT',
869
+ body: JSON.stringify(payload),
870
+ headers: {
871
+ 'Content-Type': 'application/json',
872
+ Authorization: \`Bearer \${token}\`,
873
+ },
874
+ });
875
+
876
+ expect(req.status).toBe(200);
877
+ const body: any = await req.json();
878
+
879
+ expect(body.result.updatedBy).not.toBe('hacker-id');
880
+
881
+ const row = await dbService.getDb().query.${entityName}Schema.findFirst({
882
+ where: eq(schema.${entityName}Schema.id, targetId)
883
+ });
884
+ expect(row!.updatedBy).not.toBe('hacker-id');
885
+ });
886
+ });
887
+ `;
888
+ await writeFile(join(useCaseDir, `Update${EntityName}.e2e.spec.ts`), testContent);
889
+ }
890
+ async function generateGetE2ETest(baseDir, config) {
891
+ const { EntityName, entityName, kebabName, pluralKebab } = config;
892
+ const useCaseDir = join(baseDir, `get-${kebabName}`);
893
+ const testContent = `import { afterAll, beforeAll, describe, expect, it } from 'bun:test';
894
+ import { EnvFileNames, UserRoleEnum, UserTypeEnum } from '@root/lib';
895
+ import { bootstrap, inject, Module } from '@kanian77/simple-di';
896
+ import { UseCaseModule } from '@root/use-case/UseCaseModule';
897
+ import { getConfigModule } from 'config/getConfigModule';
898
+ import { getDbModule } from 'db/getDbModule';
899
+ import { CoreModule } from '@root/core/CoreModule';
900
+ import { DB_SERVICE, type DbService } from 'db/DbService';
901
+ import { APP, AppModule } from '@root/AppModule';
902
+ import type { Hono } from 'hono';
903
+ import * as schema from '@root/schema';
904
+ import { getTestServer, type TestServer } from '@root/lib/functions/test-related/getTestServer';
905
+ import { createOneSignedUpUser, deleteCreatedSignedUsers } from '@root/lib/functions/test-related/createSignedUpUser';
906
+ import { USER_REPOSITORY_INTERFACE, type UserRepository } from '@root/core/user/IUserRepository';
907
+ import { UserModule } from '@root/core/user/UserModule';
908
+
909
+ describe('Get${EntityName} e2e', () => {
910
+ let dbService: DbService;
911
+ let server: TestServer;
912
+ let app: Hono;
913
+ let targetId: string;
914
+ let userRepository: UserRepository;
915
+
916
+ beforeAll(async () => {
917
+ const TestModule = new Module({
918
+ imports: [
919
+ AppModule,
920
+ getConfigModule(EnvFileNames.TESTING),
921
+ getDbModule(),
922
+ CoreModule,
923
+ UserModule,
924
+ UseCaseModule,
925
+ ],
926
+ });
927
+ bootstrap(TestModule);
928
+ dbService = inject<DbService>(DB_SERVICE);
929
+ app = inject<Hono>(APP);
930
+ userRepository = inject(USER_REPOSITORY_INTERFACE);
931
+ server = await getTestServer(app);
932
+
933
+ // TODO: Seed an entity here and assign targetId
934
+ });
935
+
936
+ afterAll(async () => {
937
+ await dbService.getDb().delete(schema.${entityName}Schema).execute();
938
+ await deleteCreatedSignedUsers(userRepository);
939
+ });
940
+
941
+ it('fails if unauthenticated (assuming route requires auth)', async () => {
942
+ // Note: Adjust depending on whether the GET route should be public or private
943
+ // const req = await server.client.request(\`/\${pluralKebab}/\${targetId}\`);
944
+ // expect(req.status).toBe(401);
945
+ });
946
+
947
+ it('retrieves the entity', async () => {
948
+ const { token } = await createOneSignedUpUser(userRepository, {
949
+ userType: UserTypeEnum.ADMIN,
950
+ role: UserRoleEnum.AUTHENTICATED,
951
+ });
952
+
953
+ const req = await server.client.request(\`/\${pluralKebab}/\${targetId}\`, {
954
+ method: 'GET',
955
+ headers: {
956
+ Authorization: \`Bearer \${token}\`,
957
+ },
958
+ });
959
+
960
+ expect(req.status).toBe(200);
961
+ const body: any = await req.json();
962
+ expect(body.result.id).toBe(targetId);
963
+ });
964
+ });
965
+ `;
966
+ await writeFile(join(useCaseDir, `Get${EntityName}.e2e.spec.ts`), testContent);
967
+ }
968
+ async function generateListE2ETest(baseDir, config) {
969
+ const { EntityName, entityName, kebabName, pluralKebab, pluralPascal } = config;
970
+ const useCaseDir = join(baseDir, `list-${pluralKebab}`);
971
+ const testContent = `import { afterAll, beforeAll, describe, expect, it } from 'bun:test';
972
+ import { EnvFileNames, UserRoleEnum, UserTypeEnum } from '@root/lib';
973
+ import { bootstrap, inject, Module } from '@kanian77/simple-di';
974
+ import { UseCaseModule } from '@root/use-case/UseCaseModule';
975
+ import { getConfigModule } from 'config/getConfigModule';
976
+ import { getDbModule } from 'db/getDbModule';
977
+ import { CoreModule } from '@root/core/CoreModule';
978
+ import { DB_SERVICE, type DbService } from 'db/DbService';
979
+ import { APP, AppModule } from '@root/AppModule';
980
+ import type { Hono } from 'hono';
981
+ import * as schema from '@root/schema';
982
+ import { getTestServer, type TestServer } from '@root/lib/functions/test-related/getTestServer';
983
+ import { createOneSignedUpUser, deleteCreatedSignedUsers } from '@root/lib/functions/test-related/createSignedUpUser';
984
+ import { USER_REPOSITORY_INTERFACE, type UserRepository } from '@root/core/user/IUserRepository';
985
+ import { UserModule } from '@root/core/user/UserModule';
986
+
987
+ describe('List${pluralPascal} e2e', () => {
988
+ let dbService: DbService;
989
+ let server: TestServer;
990
+ let app: Hono;
991
+ let userRepository: UserRepository;
992
+
993
+ beforeAll(async () => {
994
+ const TestModule = new Module({
995
+ imports: [
996
+ AppModule,
997
+ getConfigModule(EnvFileNames.TESTING),
998
+ getDbModule(),
999
+ CoreModule,
1000
+ UserModule,
1001
+ UseCaseModule,
1002
+ ],
1003
+ });
1004
+ bootstrap(TestModule);
1005
+ dbService = inject<DbService>(DB_SERVICE);
1006
+ app = inject<Hono>(APP);
1007
+ userRepository = inject(USER_REPOSITORY_INTERFACE);
1008
+ server = await getTestServer(app);
1009
+
1010
+ // TODO: Seed multiple entities here
1011
+ });
1012
+
1013
+ afterAll(async () => {
1014
+ await dbService.getDb().delete(schema.${entityName}Schema).execute();
1015
+ await deleteCreatedSignedUsers(userRepository);
1016
+ });
1017
+
1018
+ it('lists entities successfully', async () => {
1019
+ const { token } = await createOneSignedUpUser(userRepository, {
1020
+ userType: UserTypeEnum.ADMIN,
1021
+ role: UserRoleEnum.AUTHENTICATED,
1022
+ });
1023
+
1024
+ const req = await server.client.request('/${pluralKebab}', {
1025
+ method: 'GET',
1026
+ headers: {
1027
+ Authorization: \`Bearer \${token}\`,
1028
+ },
1029
+ });
1030
+
1031
+ expect(req.status).toBe(200);
1032
+ const body: any = await req.json();
1033
+ expect(Array.isArray(body.result)).toBe(true);
1034
+ });
1035
+ });
1036
+ `;
1037
+ await writeFile(join(useCaseDir, `List${pluralPascal}.e2e.spec.ts`), testContent);
1038
+ }
1039
+ async function generateDeleteE2ETest(baseDir, config) {
1040
+ const { EntityName, entityName, kebabName, pluralKebab } = config;
1041
+ const useCaseDir = join(baseDir, `delete-${kebabName}`);
1042
+ const testContent = `import { afterAll, beforeAll, describe, expect, it } from 'bun:test';
1043
+ import { EnvFileNames, UserRoleEnum, UserTypeEnum } from '@root/lib';
1044
+ import { bootstrap, inject, Module } from '@kanian77/simple-di';
1045
+ import { UseCaseModule } from '@root/use-case/UseCaseModule';
1046
+ import { getConfigModule } from 'config/getConfigModule';
1047
+ import { getDbModule } from 'db/getDbModule';
1048
+ import { CoreModule } from '@root/core/CoreModule';
1049
+ import { DB_SERVICE, type DbService } from 'db/DbService';
1050
+ import { APP, AppModule } from '@root/AppModule';
1051
+ import type { Hono } from 'hono';
1052
+ import * as schema from '@root/schema';
1053
+ import { eq } from 'drizzle-orm';
1054
+ import { getTestServer, type TestServer } from '@root/lib/functions/test-related/getTestServer';
1055
+ import { createOneSignedUpUser, deleteCreatedSignedUsers } from '@root/lib/functions/test-related/createSignedUpUser';
1056
+ import { USER_REPOSITORY_INTERFACE, type UserRepository } from '@root/core/user/IUserRepository';
1057
+ import { UserModule } from '@root/core/user/UserModule';
1058
+
1059
+ describe('Delete${EntityName} e2e', () => {
1060
+ let dbService: DbService;
1061
+ let server: TestServer;
1062
+ let app: Hono;
1063
+ let targetIdSoft: string;
1064
+ let targetIdHard: string;
1065
+ let userRepository: UserRepository;
1066
+
1067
+ beforeAll(async () => {
1068
+ const TestModule = new Module({
1069
+ imports: [
1070
+ AppModule,
1071
+ getConfigModule(EnvFileNames.TESTING),
1072
+ getDbModule(),
1073
+ CoreModule,
1074
+ UserModule,
1075
+ UseCaseModule,
1076
+ ],
1077
+ });
1078
+ bootstrap(TestModule);
1079
+ dbService = inject<DbService>(DB_SERVICE);
1080
+ app = inject<Hono>(APP);
1081
+ userRepository = inject(USER_REPOSITORY_INTERFACE);
1082
+ server = await getTestServer(app);
1083
+
1084
+ // TODO: Seed two entities, assign to targetIdSoft and targetIdHard
1085
+ });
1086
+
1087
+ afterAll(async () => {
1088
+ await dbService.getDb().delete(schema.${entityName}Schema).execute();
1089
+ });
1090
+
1091
+ it('fails if unauthenticated', async () => {
1092
+ const req = await server.client.request(\`/\${pluralKebab}/\${targetIdSoft}\`, {
1093
+ method: 'DELETE',
1094
+ });
1095
+ expect(req.status).toBe(401);
1096
+ });
1097
+
1098
+ it('soft deletes by default', async () => {
1099
+ const { token } = await createOneSignedUpUser(userRepository, {
1100
+ userType: UserTypeEnum.ADMIN,
1101
+ role: UserRoleEnum.AUTHENTICATED,
1102
+ });
1103
+
1104
+ const req = await server.client.request(\`/\${pluralKebab}/\${targetIdSoft}\`, {
1105
+ method: 'DELETE',
1106
+ headers: {
1107
+ Authorization: \`Bearer \${token}\`,
1108
+ },
1109
+ });
1110
+
1111
+ expect(req.status).toBe(200);
1112
+
1113
+ // Direct DB check for soft delete
1114
+ const row = await dbService.getDb().query.${entityName}Schema.findFirst({
1115
+ where: eq(schema.${entityName}Schema.id, targetIdSoft)
1116
+ });
1117
+ expect(row).toBeDefined();
1118
+ expect(row!.deleted).toBe(true);
1119
+ expect(row!.deletedAt).not.toBeNull();
1120
+ });
1121
+
1122
+ it('hard deletes when flag is false', async () => {
1123
+ const { token } = await createOneSignedUpUser(userRepository, {
1124
+ userType: UserTypeEnum.ADMIN,
1125
+ role: UserRoleEnum.AUTHENTICATED,
1126
+ });
1127
+
1128
+ const req = await server.client.request(\`/\${pluralKebab}/\${targetIdHard}\`, {
1129
+ method: 'DELETE',
1130
+ body: JSON.stringify({ softDelete: false }),
1131
+ headers: {
1132
+ 'Content-Type': 'application/json',
1133
+ Authorization: \`Bearer \${token}\`,
1134
+ },
1135
+ });
1136
+
1137
+ expect(req.status).toBe(200);
1138
+
1139
+ // Direct DB check for physical row missing
1140
+ const row = await dbService.getDb().query.${entityName}Schema.findFirst({
1141
+ where: eq(schema.${entityName}Schema.id, targetIdHard)
1142
+ });
1143
+ expect(row).toBeUndefined();
1144
+ });
1145
+ });
1146
+ `;
1147
+ await writeFile(join(useCaseDir, `Delete${EntityName}.e2e.spec.ts`), testContent);
708
1148
  }
1149
+ // END ROUTES
709
1150
  //# sourceMappingURL=generate_crud_use_cases.js.map