@nest-arch/tui 0.2.7 → 0.4.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.
Files changed (71) hide show
  1. package/README.md +20 -42
  2. package/dist/cli.js +1 -1
  3. package/dist/constants-C3xmDWeK.js +1 -0
  4. package/dist/index.js +295 -82
  5. package/dist/templates/base/.env.example.hbs +4 -2
  6. package/dist/templates/base/.env.hbs +4 -2
  7. package/dist/templates/base/.gitignore.hbs +2 -1
  8. package/dist/templates/base/.yarnrc.yml.hbs +0 -5
  9. package/dist/templates/base/README.md.hbs +33 -5
  10. package/dist/templates/base/nest-cli.json.hbs +2 -1
  11. package/dist/templates/base/pnpm-workspace.yaml.hbs +9 -0
  12. package/dist/templates/base/prisma.config.ts.hbs +32 -6
  13. package/dist/templates/base/src/app.controller.spec.ts.hbs +3 -11
  14. package/dist/templates/base/src/app.controller.ts.hbs +2 -2
  15. package/dist/templates/base/src/app.module.ts.hbs +19 -1
  16. package/dist/templates/base/src/app.router.ts.hbs +2 -2
  17. package/dist/templates/base/src/database/adapters/interfaces/db-adapter.interface.ts.hbs +7 -0
  18. package/dist/templates/base/src/database/adapters/mongo/prisma-mongo.adapter.ts.hbs +25 -1
  19. package/dist/templates/base/src/database/adapters/mongo/raw-mongo.adapter.ts.hbs +17 -12
  20. package/dist/templates/base/src/database/adapters/sql/drizzle-sql.adapter.ts.hbs +9 -0
  21. package/dist/templates/base/src/database/adapters/sql/prisma-sql.adapter.ts.hbs +7 -4
  22. package/dist/templates/base/src/database/adapters/sql/raw-sql.adapter.ts.hbs +37 -0
  23. package/dist/templates/base/src/database/adapters/sql/typeorm-sql.adapter.ts.hbs +8 -3
  24. package/dist/templates/base/src/database/db.module.ts.hbs +32 -35
  25. package/dist/templates/base/src/database/db.service.ts.hbs +40 -6
  26. package/dist/templates/base/src/database/entities/todo.entity.ts.hbs +12 -7
  27. package/dist/templates/base/src/database/index.ts.hbs +1 -0
  28. package/dist/templates/base/src/database/orm/drizzle/schema.ts.hbs +47 -5
  29. package/dist/templates/base/src/database/orm/typeorm/data-source.ts.hbs +12 -4
  30. package/dist/templates/base/src/libs/trpc/error.handler.ts.hbs +1 -1
  31. package/dist/templates/base/src/main.ts.hbs +21 -6
  32. package/dist/templates/base/test/app.e2e-spec.ts.hbs +208 -6
  33. package/dist/templates/base/test/bootstrap.e2e-spec.ts.hbs +74 -0
  34. package/dist/templates/base/tsconfig.build.json.hbs +2 -0
  35. package/dist/templates/base/tsconfig.json.hbs +1 -1
  36. package/dist/templates/base/vitest.config.e2e.ts.hbs +4 -3
  37. package/dist/templates/base/vitest.config.ts.hbs +4 -3
  38. package/dist/templates/base/yarn.lock.hbs +1 -0
  39. package/dist/templates/extras/docker/Dockerfile.hbs +4 -1
  40. package/dist/templates/extras/swagger/create-docs.ts.hbs +47 -119
  41. package/dist/templates/feature/graphql/src/graphql/graphql.module.ts.hbs +8 -1
  42. package/dist/templates/feature/graphql/src/graphql/models/todo.model.ts.hbs +4 -0
  43. package/dist/templates/feature/graphql/src/graphql/orm/drizzle/drizzle-graphql.module.ts.hbs +4 -3
  44. package/dist/templates/feature/graphql/src/graphql/resolvers/todo.resolver.ts.hbs +69 -24
  45. package/dist/templates/feature/rest/todo-example/src/api/todo/todo.controller.ts.hbs +3 -1
  46. package/dist/templates/feature/rest/todo-example/src/api/todo/todo.module.ts.hbs +1 -3
  47. package/dist/templates/feature/rest/todo-example/src/api/todo/todo.service.ts.hbs +98 -32
  48. package/dist/templates/monorepo/apps/api-gateway/package.json.hbs +0 -1
  49. package/dist/templates/monorepo/apps/api-gateway/src/app.controller.spec.ts.hbs +1 -1
  50. package/dist/templates/monorepo/apps/api-gateway/test/app.e2e-spec.ts.hbs +20 -1
  51. package/dist/templates/monorepo/apps/api-gateway/tsconfig.build.json.hbs +2 -1
  52. package/dist/templates/monorepo/apps/api-gateway/vitest.config.e2e.ts.hbs +3 -2
  53. package/dist/templates/monorepo/apps/api-gateway/vitest.config.ts.hbs +3 -2
  54. package/dist/templates/monorepo/feature/graphql/src/graphql/resolvers/todo.resolver.ts.hbs +24 -21
  55. package/dist/templates/monorepo/packages/env/src/api-gateway.ts.hbs +9 -3
  56. package/dist/templates/monorepo/root/.gitignore.hbs +1 -0
  57. package/dist/templates/monorepo/root/pnpm-workspace.yaml.hbs +9 -0
  58. package/dist/templates/monorepo/root/tsconfig.json.hbs +1 -1
  59. package/dist/templates/monorepo/root/turbo.json.hbs +2 -0
  60. package/dist/templates/single-project/architecture/nest-api/.env.hbs +3 -1
  61. package/dist/templates/single-project/architecture/nest-api/package.json.hbs +0 -1
  62. package/dist/templates/single-project/architecture/nest-api/src/main.ts.hbs +20 -5
  63. package/dist/templates/single-project/architecture/nest-microservice/.env.hbs +18 -0
  64. package/dist/templates/single-project/architecture/nest-microservice/proto/app.proto.hbs +13 -0
  65. package/dist/templates/single-project/architecture/nest-microservice/src/app.controller.ts.hbs +14 -1
  66. package/dist/templates/single-project/architecture/nest-microservice/src/app.service.ts.hbs +4 -0
  67. package/dist/templates/single-project/architecture/nest-microservice/src/main.ts.hbs +13 -2
  68. package/dist/templates/single-project/architecture/nest-microservice/test/microservice.e2e-spec.ts.hbs +64 -0
  69. package/dist/templates/single-project/shared/config/env.ts.hbs +27 -3
  70. package/package.json +7 -4
  71. package/dist/constants-Cgtoxhmd.js +0 -1
@@ -1,3 +1,22 @@
1
+ {{#if isPrisma8}}
2
+ import { Injectable } from "@nestjs/common";
3
+ {{#if isMonorepo}}
4
+ import { db as prismaDb } from "../prisma/db.js";
5
+ {{else}}
6
+ import { db as prismaDb } from "../prisma/db.js";
7
+ {{/if}}
8
+
9
+ @Injectable()
10
+ export class DbService {
11
+ get prisma() {
12
+ return prismaDb;
13
+ }
14
+
15
+ get raw() {
16
+ return { sql: prismaDb };
17
+ }
18
+ }
19
+ {{else}}
1
20
  import { Injectable, Inject } from '@nestjs/common';
2
21
  {{#if (or hasPostgres hasMysql hasSqlite)}}
3
22
  import type { ISqlAdapter } from './adapters/interfaces/db-adapter.interface.js';
@@ -16,7 +35,7 @@ import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
16
35
  import type { MySql2Database } from 'drizzle-orm/mysql2';
17
36
  {{/if}}
18
37
  {{#if hasSqlite}}
19
- import type { BetterSqlite3Database } from 'drizzle-orm/better-sqlite3';
38
+ import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
20
39
  {{/if}}
21
40
  import type * as schema from './orm/drizzle/schema.js';
22
41
  {{/if}}
@@ -29,8 +48,10 @@ type SqlClient = PrismaClient;
29
48
  {{else if useTypeorm}}
30
49
  type SqlClient = DataSource;
31
50
  {{else if useDrizzle}}
32
- type DrizzleClient = {{#if hasPostgres}}NodePgDatabase{{/if}}{{#if hasMysql}}MySql2Database{{/if}}{{#if hasSqlite}}BetterSqlite3Database{{/if}}<typeof schema>;
51
+ type DrizzleClient = {{#if hasPostgres}}NodePgDatabase{{/if}}{{#if hasMysql}}MySql2Database{{/if}}{{#if hasSqlite}}BetterSQLite3Database{{/if}}<typeof schema>;
33
52
  type SqlClient = DrizzleClient;
53
+ {{else if hasPostgres}}
54
+ type SqlClient = import('pg').Pool;
34
55
  {{else}}
35
56
  type SqlClient = unknown;
36
57
  {{/if}}
@@ -39,18 +60,26 @@ type SqlClient = unknown;
39
60
  {{#if usePrisma}}
40
61
  type MongoClient = PrismaClient;
41
62
  {{else}}
42
- type MongoClient = unknown;
63
+ type MongoClient = import('mongodb').Db;
43
64
  {{/if}}
44
65
  {{/if}}
45
66
 
67
+ {{#if isPrisma8}}
68
+ {{#if isMonorepo}}
69
+ import { db as prismaDb } from '../prisma/db.js';
70
+ {{else}}
71
+ import { db as prismaDb } from './prisma/db.js';
72
+ {{/if}}
73
+ {{/if}}
74
+
46
75
  @Injectable()
47
76
  export class DbService {
48
77
  constructor(
49
78
  {{#if (or hasPostgres hasMysql hasSqlite)}}
50
- @Inject('SQL_ADAPTER') public readonly sql: ISqlAdapter<SqlClient>{{#if hasMongodb}},{{/if}}
79
+ @Inject('SQL_ADAPTER') readonly sql: ISqlAdapter<SqlClient>{{#if hasMongodb}},{{/if}}
51
80
  {{/if}}
52
81
  {{#if hasMongodb}}
53
- @Inject('MONGO_ADAPTER') public readonly mongo: IMongoAdapter<MongoClient>
82
+ @Inject('MONGO_ADAPTER') readonly mongo: IMongoAdapter<MongoClient>
54
83
  {{/if}}
55
84
  ) {}
56
85
 
@@ -64,7 +93,11 @@ export class DbService {
64
93
  {{/if}}
65
94
  };
66
95
  }
67
- {{#if usePrisma}}
96
+ {{#if isPrisma8}}
97
+ get prisma() {
98
+ return prismaDb;
99
+ }
100
+ {{else if usePrisma}}
68
101
  {{#if (or hasPostgres hasMysql hasSqlite)}}
69
102
  get prisma(): PrismaClient {
70
103
  return this.sql.getClient();
@@ -86,3 +119,4 @@ export class DbService {
86
119
  {{/if}}
87
120
  {{/if}}
88
121
  }
122
+ {{/if}}
@@ -15,18 +15,23 @@ export class Todo {
15
15
  @PrimaryGeneratedColumn("uuid")
16
16
  id: string;
17
17
 
18
- @Column({ length: 255 })
19
- description: string;
18
+ @Column({ type: "varchar", length: 255, nullable: true })
19
+ description?: string;
20
20
 
21
- @Column({ length: 100, nullable: true })
22
- name: string;
21
+ @Column({ type: "varchar", length: 255 })
22
+ title: string;
23
23
 
24
24
  @Column({ default: false, name: "is_completed", type: "boolean" })
25
- isCompleted: boolean;
25
+ completed: boolean;
26
26
 
27
- @CreateDateColumn({ name: "created_at", type: "timestamptz" })
27
+ {{#if hasAuth}}
28
+ @Column({ type: "varchar", nullable: true })
29
+ authorId?: string;
30
+ {{/if}}
31
+
32
+ @CreateDateColumn({ name: "created_at" })
28
33
  createdAt: Date;
29
34
 
30
- @UpdateDateColumn({ name: "updated_at", type: "timestamptz" })
35
+ @UpdateDateColumn({ name: "updated_at" })
31
36
  updatedAt: Date;
32
37
  }
@@ -1,4 +1,5 @@
1
1
  // oxlint-disable oxc/no-barrel-file
2
+ // biome-ignore-all lint/performance/noBarrelFile: Public entry point shared with the monorepo database package.
2
3
  export * from './db.module.js';
3
4
  export * from './db.service.js';
4
5
  export * from './adapters/interfaces/db-adapter.interface.js';
@@ -1,30 +1,72 @@
1
+ {{#if isBetterAuth}}
2
+ export * from "./auth-schema.js";
3
+ {{/if}}
1
4
  // oxlint-disable typescript/no-explicit-any
2
5
  {{#if hasPostgres}}
3
- import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
6
+ import { boolean, pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
4
7
 
8
+ {{#unless isBetterAuth}}
5
9
  export const users = pgTable('users', {
10
+ email: text('email').unique(),
6
11
  id: serial('id').primaryKey(),
7
12
  name: text('name'),
8
- email: text('email').unique(),
9
13
  createdAt: timestamp('created_at').defaultNow(),
10
14
  });
15
+ {{/unless}}
16
+ {{#if hasTodoExample}}
17
+ export const todos = pgTable('todos', {
18
+ {{#if hasAuth}}
19
+ authorId: text('author_id'),
20
+ {{/if}}
21
+ completed: boolean('completed').notNull().default(false),
22
+ description: text('description'),
23
+ id: serial('id').primaryKey(),
24
+ title: text('title').notNull(),
25
+ });
26
+ {{/if}}
11
27
  {{/if}}
12
28
  {{#if hasMysql}}
13
- import { mysqlTable, serial, text, timestamp } from 'drizzle-orm/mysql-core';
29
+ import { boolean, mysqlTable, serial, text, timestamp } from 'drizzle-orm/mysql-core';
14
30
 
31
+ {{#unless isBetterAuth}}
15
32
  export const users = mysqlTable('users', {
33
+ email: text('email').unique(),
16
34
  id: serial('id').primaryKey(),
17
35
  name: text('name'),
18
- email: text('email').unique(),
19
36
  createdAt: timestamp('created_at').defaultNow(),
20
37
  });
38
+ {{/unless}}
39
+ {{#if hasTodoExample}}
40
+ export const todos = mysqlTable('todos', {
41
+ {{#if hasAuth}}
42
+ authorId: text('author_id'),
43
+ {{/if}}
44
+ completed: boolean('completed').notNull().default(false),
45
+ description: text('description'),
46
+ id: serial('id').primaryKey(),
47
+ title: text('title').notNull(),
48
+ });
49
+ {{/if}}
21
50
  {{/if}}
22
51
  {{#if hasSqlite}}
23
52
  import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
24
53
 
54
+ {{#unless isBetterAuth}}
25
55
  export const users = sqliteTable('users', {
56
+ email: text('email').unique(),
26
57
  id: integer('id').primaryKey({ autoIncrement: true }),
27
58
  name: text('name'),
28
- email: text('email').unique(),
29
59
  });
60
+ {{/unless}}
61
+ {{#if hasTodoExample}}
62
+ export const todos = sqliteTable('todos', {
63
+ {{#if hasAuth}}
64
+ authorId: text('author_id'),
65
+ {{/if}}
66
+ completed: integer('completed', { mode: 'boolean' }).notNull().default(false),
67
+ description: text('description'),
68
+ id: integer('id').primaryKey({ autoIncrement: true }),
69
+ title: text('title').notNull(),
70
+ });
71
+ {{/if}}
30
72
  {{/if}}
@@ -1,10 +1,17 @@
1
1
  // oxlint-disable typescript/no-non-null-assertion
2
- import { join } from "node:path";
3
2
  import "reflect-metadata";
3
+ {{#if isMonorepo}}
4
+ import { env } from "@repo/env/api-gateway";
5
+ {{else}}
6
+ import { env } from "../../../config/env.js";
7
+ {{/if}}
4
8
  import { DataSource } from "typeorm";
9
+ {{#if hasTodoExample}}
10
+ import { Todo } from "../../entities/todo.entity.js";
11
+ {{/if}}
5
12
 
6
13
  const databaseUrl =
7
- process.env.{{#if hasPostgres}}POSTGRES_URL{{/if}}{{#if hasMysql}}MYSQL_URL{{/if}}{{#if hasSqlite}}SQLITE_PATH{{/if}}!;
14
+ env.{{#if hasPostgres}}POSTGRES_URL{{/if}}{{#if hasMysql}}MYSQL_URL{{/if}}{{#if hasSqlite}}SQLITE_PATH{{/if}};
8
15
 
9
16
  {{#unless hasSqlite}}
10
17
  const parsedUrl = new URL(databaseUrl);
@@ -12,9 +19,10 @@ const parsedUrl = new URL(databaseUrl);
12
19
 
13
20
 
14
21
  export const AppDataSource = new DataSource({
15
- entities: [join(import.meta.dirname, "..", "..", "entities", "*.entity.js")],
22
+ // Register entities explicitly for consistent metadata in Node and test runners.
23
+ entities: [{{#if hasTodoExample}}Todo{{/if}}],
16
24
  migrations: [{{#if isMonorepo}}"migrations/*.{ts,js}"{{else}}"migrations/*.{ts,js}"{{/if}}],
17
- synchronize: true,
25
+ synchronize: env.NODE_ENV === "development",
18
26
  type: "{{#if hasPostgres}}postgres{{/if}}{{#if hasMysql}}mysql{{/if}}{{#if hasSqlite}}better-sqlite3{{/if}}",
19
27
  url: databaseUrl,
20
28
  {{#unless hasSqlite}}
@@ -3,7 +3,7 @@ import { OnErrorOptions, TRPCErrorHandler } from "nestjs-trpc";
3
3
 
4
4
  @Injectable()
5
5
  export class AppErrorHandler implements TRPCErrorHandler {
6
- private logger = new Logger("TRPC Router");
6
+ private readonly logger = new Logger("TRPC Router");
7
7
  onError(opts: OnErrorOptions): void {
8
8
  this.logger.error(`[${opts.type}] ${opts.path}: ${opts.error.message}`);
9
9
  }
@@ -2,10 +2,18 @@ import { NestFactory } from "@nestjs/core";
2
2
  {{#if (eq httpProvider "fastify")}}
3
3
  import { FastifyAdapter, NestFastifyApplication } from "@nestjs/platform-fastify";
4
4
  {{/if}}
5
+ {{#if (includes addons "nestjs-zod")}}
6
+ import { Logger } from "@nestjs/common";
7
+ import { ZodValidationPipe } from "nestjs-zod";
8
+ {{else}}
5
9
  import { Logger, StandardSchemaValidationPipe } from "@nestjs/common";
10
+ {{/if}}
6
11
 
7
12
  import { AppModule } from "./app.module.js";
8
13
  import { env } from "./config/env.js";
14
+ {{#if isPrisma8}}
15
+ import { connectDatabase } from "./prisma/db.js";
16
+ {{/if}}
9
17
  {{#if swagger}}
10
18
  import { createDocs } from "./docs/create-docs.js";
11
19
  {{/if}}
@@ -20,6 +28,9 @@ import { auth } from "./auth/auth.js";
20
28
  {{/if}}
21
29
 
22
30
  async function bootstrap() {
31
+ {{#if isPrisma8}}
32
+ await connectDatabase();
33
+ {{/if}}
23
34
  const app = await NestFactory.create{{#if (eq httpProvider "fastify")}}<NestFastifyApplication>(AppModule, new FastifyAdapter(){{else}}(AppModule{{/if}}{{#if (or (eq auth "better-auth") (eq auth "better-auth:stateless") hasBetterAuthWithoutRest)}}, { bodyParser: false }{{/if}});
24
35
  app.enableCors({
25
36
  origin: env.CORS_ORIGIN,
@@ -27,7 +38,11 @@ async function bootstrap() {
27
38
  {{#if hasRest}}
28
39
  app.setGlobalPrefix("api");
29
40
  {{/if}}
41
+ {{#if (includes addons "nestjs-zod")}}
42
+ app.useGlobalPipes(new ZodValidationPipe());
43
+ {{else}}
30
44
  app.useGlobalPipes(new StandardSchemaValidationPipe());
45
+ {{/if}}
31
46
  app.enableShutdownHooks();
32
47
  {{#if swagger}}
33
48
  const docsPath = "/docs";
@@ -86,15 +101,15 @@ async function bootstrap() {
86
101
  });
87
102
  SwaggerModule.setup(docsPath, app, document);
88
103
  {{/if}}
89
- Logger.log(`Swagger documentation available at http://localhost:${env.PORT ?? 3000}${docsPath}`, "SwaggerDocs");
104
+ Logger.log(`Swagger documentation available at http://localhost:${env.PORT}${docsPath}`, "SwaggerDocs");
90
105
  {{/if}}
91
- await app.listen(env.PORT ?? 3000);
106
+ await app.listen(env.PORT);
92
107
  {{#if hasRest}}
93
- Logger.log(`Started backend server on http://localhost:${env.PORT ?? 3000}/api`, "Main");
108
+ Logger.log(`Started backend server on http://localhost:${env.PORT}/api`, "Main");
94
109
  {{else if hasTrpc}}
95
- Logger.log(`Started trpc server on http://localhost:${env.PORT ?? 3000}/trpc`, "Main");
110
+ Logger.log(`Started trpc server on http://localhost:${env.PORT}/trpc`, "Main");
96
111
  {{else}}
97
- Logger.log(`Started server on http://localhost:${env.PORT ?? 3000}`, "Main");
112
+ Logger.log(`Started server on http://localhost:${env.PORT}`, "Main");
98
113
  {{/if}}
99
114
  }
100
- await bootstrap();
115
+ await bootstrap();
@@ -1,27 +1,229 @@
1
- import { INestApplication } from "@nestjs/common";
1
+ import { INestApplication{{#unless (includes addons "nestjs-zod")}}, StandardSchemaValidationPipe{{/unless}} } from "@nestjs/common";
2
+ {{#if (includes addons "nestjs-zod")}}
3
+ import { ZodValidationPipe } from "nestjs-zod";
4
+ {{/if}}
2
5
  import { Test, TestingModule } from "@nestjs/testing";
6
+ {{#if (eq httpProvider "fastify")}}
7
+ import { FastifyAdapter } from "@nestjs/platform-fastify";
8
+ {{/if}}
3
9
  import request from "supertest";
10
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
4
11
  import type { App } from "supertest/types";
5
12
 
13
+ {{#if isPassportAuth}}
14
+ import { JwtService } from "@nestjs/jwt";
15
+ import { DbService } from "../src/database/db.service.js";
16
+ import { env } from "../src/config/env.js";
17
+ {{/if}}
6
18
  import { AppModule } from "./../src/app.module.js";
19
+ {{#if (and useDrizzle (eq auth "none"))}}
20
+ import { DbService } from "../src/database/db.service.js";
21
+ import { users } from "../src/database/orm/drizzle/schema.js";
22
+ import { eq } from "drizzle-orm";
23
+ {{/if}}
7
24
 
8
25
  describe("AppController (e2e)", () => {
9
26
  let app: INestApplication<App>;
10
27
 
11
- beforeEach(async () => {
28
+ async function startApplication() {
12
29
  const moduleFixture: TestingModule = await Test.createTestingModule({
13
30
  imports: [AppModule],
14
31
  }).compile();
15
32
 
16
- app = moduleFixture.createNestApplication();
33
+ {{#if (eq httpProvider "fastify")}}
34
+ app = moduleFixture.createNestApplication(new FastifyAdapter(){{#if isBetterAuth}}, { bodyParser: false }{{/if}});
35
+ {{else}}
36
+ app = moduleFixture.createNestApplication({{#if isBetterAuth}}{ bodyParser: false }{{/if}});
37
+ {{/if}}
38
+ {{#if (includes addons "nestjs-zod")}}
39
+ app.useGlobalPipes(new ZodValidationPipe());
40
+ {{else}}
41
+ app.useGlobalPipes(new StandardSchemaValidationPipe());
42
+ {{/if}}
17
43
  await app.init();
44
+ {{#if (eq httpProvider "fastify")}}
45
+ await app.getHttpAdapter().getInstance().ready();
46
+ {{/if}}
47
+ }
48
+ beforeAll(startApplication);
49
+
50
+ {{#if hasRest}}
51
+ it("/ (GET)", async () => {
52
+ const response = await request(app.getHttpServer()).get("/").expect(200);
53
+ expect(response.text).toBe("Hello World!");
18
54
  });
55
+ {{/if}}
19
56
 
20
- it("/ (GET)", () => {
21
- return request(app.getHttpServer()).get("/").expect(200).expect("Hello World!");
57
+ {{#if hasTrpc}}
58
+ it("executes a tRPC query through HTTP", async () => {
59
+ const response = await request(app.getHttpServer())
60
+ .get("/trpc/appRouter.getHello").expect(200);
61
+ expect(response.body.result.data).toBe("Hello World!");
22
62
  });
63
+ {{/if}}
23
64
 
24
- afterEach(async () => {
65
+ {{#if (eq auth "better-auth")}}
66
+ it("registers, authenticates and revokes a persisted session", async () => {
67
+ const email = `scenario-${Date.now()}@example.com`;
68
+ const password = "Scenario-password-123!";
69
+ const origin = "http://localhost:3000";
70
+ let server = app.getHttpServer();
71
+ const signedUp = await request(server).post("/api/auth/sign-up/email")
72
+ .set("Origin", origin).send({ email, password, name: "Scenario" }).expect(200);
73
+ expect(signedUp.body.user.email).toBe(email);
74
+ expect(signedUp.body.user.password).toBeUndefined();
75
+ await request(server).post("/api/auth/sign-up/email")
76
+ .set("Origin", origin).send({ email, password, name: "Duplicate" }).expect(422);
77
+ await request(server).post("/api/auth/sign-in/email")
78
+ .set("Origin", origin).send({ email, password: "wrong-password" }).expect(401);
79
+ const signedIn = await request(server).post("/api/auth/sign-in/email")
80
+ .set("Origin", origin).send({ email, password }).expect(200);
81
+ const cookies = signedIn.headers["set-cookie"];
82
+ expect(cookies).toBeTruthy();
83
+ const session = await request(server).get("/api/auth/get-session")
84
+ .set("Cookie", cookies).expect(200);
85
+ expect(session.body.user.email).toBe(email);
86
+ {{#if (or usePrisma useDrizzle)}}
87
+ await request(server).get("/auth-profile").expect(401);
88
+ const protectedProfile = await request(server).get("/auth-profile").set("Cookie", cookies).expect(200);
89
+ expect(protectedProfile.body.email).toBe(email);
25
90
  await app.close();
91
+ await startApplication();
92
+ server = app.getHttpServer();
93
+ const persisted = await request(app.getHttpServer()).get("/api/auth/get-session").set("Cookie", cookies).expect(200);
94
+ expect(persisted.body.user.email).toBe(email);
95
+ {{/if}}
96
+ {{#if hasTrpc}}
97
+ await request(server).get("/trpc/appRouter.testAuth").expect(401);
98
+ const authenticated = await request(server).get("/trpc/appRouter.testAuth")
99
+ .set("Cookie", cookies).expect(200);
100
+ expect(authenticated.body.result.data.user.email).toBe(email);
101
+ {{/if}}
102
+ await request(server).post("/api/auth/sign-out").set("Origin", origin)
103
+ .set("Cookie", cookies).send({}).expect(200);
104
+ const revoked = await request(server).get("/api/auth/get-session")
105
+ .set("Cookie", cookies).expect(200);
106
+ expect(revoked.body).toBeNull();
107
+ });
108
+ {{/if}}
109
+
110
+ {{#if hasGraphql}}
111
+ it("queries and mutates persisted GraphQL todos with input validation", async () => {
112
+ const server = app.getHttpServer();
113
+ const execute = (query: string, variables: Record<string, unknown> = {}) => request(server).post("/graphql").send({ query, variables });
114
+ const hello = await execute("{ hello }");
115
+ expect(hello.body.data.hello).toBe("Hello GraphQL!");
116
+ {{#if hasTodoExample}}
117
+ const created = await execute('mutation($input: CreateTodoInput!) { createTodo(input: $input) { id title completed } }', { input: { title: "GraphQL integration" } });
118
+ expect(created.body.errors).toBeUndefined();
119
+ const { id } = created.body.data.createTodo;
120
+ const listed = await execute("{ todos { id title } }");
121
+ expect(listed.body.data.todos).toContainEqual({ id, title: "GraphQL integration" });
122
+ const updated = await execute('mutation($input: UpdateTodoInput!) { updateTodo(input: $input) { completed } }', { input: { id, completed: true } });
123
+ expect(updated.body.data.updateTodo.completed).toBe(true);
124
+ const invalid = await execute('mutation { createTodo(input: { title: " " }) { id } }');
125
+ expect(invalid.body.errors).toBeTruthy();
126
+ const deleted = await execute('mutation($id: String!) { deleteTodo(id: $id) }', { id });
127
+ expect(deleted.body.data.deleteTodo).toBe(true);
128
+ const missing = await execute('mutation($id: String!) { deleteTodo(id: $id) }', { id });
129
+ expect(missing.body.errors).toBeTruthy();
130
+ {{/if}}
131
+ });
132
+ {{/if}}
133
+
134
+ {{#if isPassportAuth}}
135
+ it("verifies credentials, rotates refresh tokens, detects replay and revokes access", async () => {
136
+ const server = app.getHttpServer();
137
+ const email = `passport-${Date.now()}@example.com`;
138
+ const password = "Scenario-password-123!";
139
+ const registered = await request(server).post("/auth/register").send({ email, password }).expect(201);
140
+ expect(registered.body.password).toBeUndefined();
141
+ expect(registered.body.role).toBe("USER");
142
+ await request(server).post("/auth/register").send({ email, password }).expect(409);
143
+ await request(server).post("/auth/login").send({ email, password, role: "ADMIN" }).expect(400);
144
+ await request(server).post("/auth/login").send({ email, password: "wrong-password-123" }).expect(401);
145
+ const login = await request(server).post("/auth/login").send({ email, password }).expect(200);
146
+ const { accessToken, refreshToken } = login.body;
147
+ await request(server).get("/auth/profile").expect(401);
148
+ const profile = await request(server).get("/auth/profile").auth(accessToken, { type: "bearer" }).expect(200);
149
+ expect(profile.body.email).toBe(email);
150
+ const jwt = new JwtService();
151
+ const decoded = jwt.decode(accessToken);
152
+ for (const overrides of [{ iss: "wrong" }, { aud: "wrong" }, { exp: 1 }, { sub: undefined }, { sid: "wrong" }]) {
153
+ const token = jwt.sign({ ...decoded, ...overrides }, { secret: env.JWT_ACCESS_SECRET, algorithm: "HS256" });
154
+ await request(server).get("/auth/profile").auth(token, { type: "bearer" }).expect(401);
155
+ }
156
+ const wrongSignature = jwt.sign(decoded, { secret: "another-secret-that-is-at-least-32-characters" });
157
+ await request(server).get("/auth/profile").auth(wrongSignature, { type: "bearer" }).expect(401);
158
+ const rotated = await request(server).post("/auth/refresh").send({ refreshToken }).expect(200);
159
+ expect(rotated.body.refreshToken).not.toBe(refreshToken);
160
+ await request(server).post("/auth/refresh").send({ refreshToken }).expect(401);
161
+ await request(server).post("/auth/refresh").send({ refreshToken: rotated.body.refreshToken }).expect(401);
162
+ await request(server).get("/auth/profile").auth(rotated.body.accessToken, { type: "bearer" }).expect(401);
163
+ const fresh = await request(server).post("/auth/login").send({ email, password }).expect(200);
164
+ await request(server).post("/auth/logout").send({ refreshToken: fresh.body.refreshToken }).expect(200);
165
+ await request(server).get("/auth/profile").auth(fresh.body.accessToken, { type: "bearer" }).expect(401);
166
+ const user = await app.get(DbService).prisma.user.findUniqueOrThrow({ where: { email }, include: { refreshTokens: true } });
167
+ expect(user.password).not.toBe(password);
168
+ expect(user.refreshTokens.every((token) => token.tokenHash !== refreshToken)).toBe(true);
169
+ const concurrentLogin = await request(server).post("/auth/login").send({ email, password }).expect(200);
170
+ const results = await Promise.all([1, 2].map(() => request(server).post("/auth/refresh").send({ refreshToken: concurrentLogin.body.refreshToken })));
171
+ expect(results.map(({ status }) => status).sort()).toEqual([200, 401]);
172
+ });
173
+ {{/if}}
174
+ {{#if hasMongodb}}
175
+ it("rejects malformed and missing MongoDB identifiers", async () => {
176
+ await request(app.getHttpServer()).delete("/todos/invalid").expect(400);
177
+ await request(app.getHttpServer()).delete("/todos/000000000000000000000000").expect(404);
178
+ await request(app.getHttpServer()).post("/todos").send({ title: "" }).expect(400);
179
+ });
180
+ {{/if}}
181
+
182
+ it("returns 404 for an unknown route", async () => {
183
+ await request(app.getHttpServer()).get("/__missing_route__").expect(404);
184
+ });
185
+
186
+ {{#if (and useDrizzle (eq auth "none"))}}
187
+ it("writes and reads the migrated Drizzle schema", async () => {
188
+ const db = app.get(DbService).drizzle;
189
+ const email = `drizzle-${Date.now()}@example.com`;
190
+ try {
191
+ await db.insert(users).values({ email, name: "Database integration" });
192
+ const rows = await db.select().from(users).where(eq(users.email, email));
193
+ expect(rows).toHaveLength(1);
194
+ expect(rows[0].name).toBe("Database integration");
195
+ } finally {
196
+ await db.delete(users).where(eq(users.email, email));
197
+ }
198
+ });
199
+ {{/if}}
200
+
201
+ {{#if (and hasTodoExample hasRest (eq auth "none"))}}
202
+ it("persists, updates and deletes a todo through HTTP", async () => {
203
+ const created = await request(app.getHttpServer()).post("/todos")
204
+ .send({ title: "Database integration", completed: false }).expect(201);
205
+ const { id } = created.body;
206
+ expect(id).toBeTruthy();
207
+ {{#if hasMongodb}}
208
+ await app.close();
209
+ await startApplication();
210
+ {{/if}}
211
+ const listed = await request(app.getHttpServer()).get("/todos").expect(200);
212
+ expect(listed.body).toEqual(expect.arrayContaining([expect.objectContaining({ id, title: "Database integration" })]));
213
+ await request(app.getHttpServer()).put(`/todos/${id}`).send({ completed: true }).expect(200);
214
+ const updated = await request(app.getHttpServer()).get("/todos").expect(200);
215
+ expect(updated.body).toEqual(expect.arrayContaining([expect.objectContaining({ id, completed: true })]));
216
+ await request(app.getHttpServer()).delete(`/todos/${id}`).expect(200);
217
+ const deleted = await request(app.getHttpServer()).get("/todos").expect(200);
218
+ expect(deleted.body).not.toEqual(expect.arrayContaining([expect.objectContaining({ id })]));
219
+ });
220
+ {{/if}}
221
+
222
+ it("rejects unsupported methods", async () => {
223
+ await request(app.getHttpServer()).post("/").send({ unexpected: true }).expect(404);
224
+ });
225
+
226
+ afterAll(async () => {
227
+ await app?.close();
26
228
  });
27
229
  });
@@ -0,0 +1,74 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ import { once } from "node:events";
3
+ import { createServer } from "node:net";
4
+ import { spawn, type ChildProcess } from "node:child_process";
5
+
6
+ import { afterEach, expect, it } from "vitest";
7
+
8
+ const startedProcesses: ChildProcess[] = [];
9
+
10
+ async function getFreePort(): Promise<number> {
11
+ const server = createServer();
12
+ server.listen(0, "127.0.0.1");
13
+ await once(server, "listening");
14
+ const address = server.address();
15
+ server.close();
16
+ if (!address || typeof address === "string") {
17
+ throw new Error("Could not allocate a loopback port for the bootstrap test.");
18
+ }
19
+ return address.port;
20
+ }
21
+
22
+ async function waitForServer(url: string, application: ChildProcess, deadline = Date.now() + 15000): Promise<Response> {
23
+ if (application.exitCode !== null) throw new Error(`Generated application exited during startup with code ${application.exitCode}.`);
24
+ try {
25
+ return await fetch(url, { signal: AbortSignal.timeout(500) });
26
+ } catch (error) {
27
+ if (Date.now() >= deadline) throw new Error("Generated application did not become ready", { cause: error });
28
+ await delay(100);
29
+ return waitForServer(url, application, deadline);
30
+ }
31
+ }
32
+
33
+ async function stopApplication(application: ChildProcess): Promise<void> {
34
+ if (application.exitCode !== null) return;
35
+ const exited = once(application, "exit");
36
+ application.kill();
37
+ const timeout = setTimeout(() => { application.kill("SIGKILL"); }, 5000);
38
+ try { await exited; } finally { clearTimeout(timeout); }
39
+ }
40
+
41
+ afterEach(async () => {
42
+ await Promise.all(startedProcesses.splice(0).map(stopApplication));
43
+ });
44
+
45
+ it("boots the compiled application through main.ts", async () => {
46
+ const port = await getFreePort();
47
+ const origin = "https://allowed.example";
48
+ const application = spawn(process.execPath, ["dist/main.js"], {
49
+ env: { ...process.env, CORS_ORIGIN: origin, NODE_ENV: "test", PORT: String(port) },
50
+ stdio: ["ignore", "pipe", "pipe"],
51
+ });
52
+ startedProcesses.push(application);
53
+ let output = "";
54
+ application.stdout?.on("data", (chunk) => { output += chunk.toString(); });
55
+ application.stderr?.on("data", (chunk) => { output += chunk.toString(); });
56
+
57
+ {{#if hasRest}}
58
+ const response = await waitForServer(`http://127.0.0.1:${port}/api`, application).catch((error) => { throw new Error(`${error}\n${output}`); });
59
+ expect(response.status).toBe(200);
60
+ expect(await response.text()).toBe("Hello World!");
61
+
62
+ const corsResponse = await fetch(`http://127.0.0.1:${port}/api`, {
63
+ headers: { Origin: origin },
64
+ });
65
+ expect(corsResponse.headers.get("access-control-allow-origin")).toBe(origin);
66
+ const rootResponse = await fetch(`http://127.0.0.1:${port}/`, { redirect: "manual" });
67
+ expect(rootResponse.status).toBe(404);
68
+ {{else}}
69
+ await waitForServer(`http://127.0.0.1:${port}/graphql?query=%7Bhello%7D`, application).catch((error) => { throw new Error(`${error}\n${output}`); });
70
+ const response = await fetch(`http://127.0.0.1:${port}/graphql`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: "{ hello }" }) });
71
+ const body = await response.json();
72
+ expect(body.data.hello).toBe("Hello GraphQL!");
73
+ {{/if}}
74
+ }, 20_000);
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "extends": "./tsconfig.json",
3
+ "include": ["src/**/*.ts"],
3
4
  "exclude": [
4
5
  "node_modules",
5
6
  "test",
@@ -7,6 +8,7 @@
7
8
  "**/*spec.ts"
8
9
  ],
9
10
  "compilerOptions": {
11
+ "rootDir": "./src",
10
12
  "strictNullChecks": true
11
13
  }
12
14
  }
@@ -24,4 +24,4 @@
24
24
  ],
25
25
  "strictNullChecks": true
26
26
  }
27
- }
27
+ }
@@ -1,11 +1,12 @@
1
1
  import { defineConfig } from "vitest/config";
2
- import tsconfigPaths from "vite-tsconfig-paths";
3
2
 
4
3
  export default defineConfig({
5
- plugins: [tsconfigPaths()],
4
+ resolve: {
5
+ tsconfigPaths: true,
6
+ },
6
7
  test: {
8
+ include: ["**/*.e2e-spec.ts"],
7
9
  globals: true,
8
10
  root: "./",
9
- include: ["**/*.e2e-spec.ts"],
10
11
  },
11
12
  });