@nest-arch/tui 0.4.4 → 0.4.6

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 (35) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/{constants-7dJLZpB2.js → constants-DVhgS48N.js} +1 -1
  3. package/dist/index.js +148 -133
  4. package/dist/templates/addons/husky/noop-hook.hbs +2 -0
  5. package/dist/templates/base/.env.example.hbs +9 -0
  6. package/dist/templates/base/.env.hbs +9 -0
  7. package/dist/templates/base/README.md.hbs +12 -0
  8. package/dist/templates/base/prisma.config.ts.hbs +1 -0
  9. package/dist/templates/base/src/app.module.ts.hbs +1 -0
  10. package/dist/templates/base/src/database/adapters/interfaces/db-adapter.interface.ts.hbs +4 -4
  11. package/dist/templates/base/src/database/adapters/mongo/prisma-mongo.adapter.ts.hbs +4 -4
  12. package/dist/templates/base/src/database/db.module.ts.hbs +15 -2
  13. package/dist/templates/base/src/database/db.service.ts.hbs +5 -5
  14. package/dist/templates/base/src/database/index.ts.hbs +12 -3
  15. package/dist/templates/base/src/main.ts.hbs +5 -4
  16. package/dist/templates/base/test/app.e2e-spec.ts.hbs +50 -13
  17. package/dist/templates/base/test/bootstrap.e2e-spec.ts.hbs +2 -2
  18. package/dist/templates/base/tsconfig.build.json.hbs +1 -0
  19. package/dist/templates/base/vitest.config.e2e.ts.hbs +1 -1
  20. package/dist/templates/base/vitest.config.ts.hbs +1 -1
  21. package/dist/templates/extras/docker/Dockerfile.hbs +1 -1
  22. package/dist/templates/extras/health/src/health/health.controller.ts.hbs +7 -3
  23. package/dist/templates/feature/rest/todo-example/src/api/todo/todo.controller.ts.hbs +4 -6
  24. package/dist/templates/feature/rest/todo-example/src/api/todo/todo.router.ts.hbs +38 -37
  25. package/dist/templates/monorepo/apps/api-gateway/src/main.ts.hbs +8 -7
  26. package/dist/templates/monorepo/apps/api-gateway/test/app.e2e-spec.ts.hbs +4 -1
  27. package/dist/templates/single-project/architecture/nest-api/.env.hbs +9 -0
  28. package/dist/templates/single-project/architecture/nest-api/src/main.ts.hbs +8 -7
  29. package/dist/templates/single-project/architecture/nest-gateway/src/main.ts.hbs +8 -7
  30. package/dist/templates/single-project/architecture/nest-microservice/src/app.controller.ts.hbs +52 -1
  31. package/dist/templates/single-project/architecture/nest-microservice/src/app.service.ts.hbs +21 -0
  32. package/dist/templates/single-project/architecture/nest-microservice/src/main.ts.hbs +35 -16
  33. package/dist/templates/single-project/architecture/nest-microservice/test/microservice.e2e-spec.ts.hbs +51 -19
  34. package/dist/templates/single-project/shared/config/env.ts.hbs +27 -16
  35. package/package.json +2 -2
@@ -0,0 +1,2 @@
1
+ #!/bin/sh
2
+ exit 0
@@ -17,6 +17,15 @@ MYSQL_URL=mysql://root:mysql@localhost:3306/{{projectNameKebab}}_db
17
17
  {{#if hasSqlite}}
18
18
  SQLITE_PATH=./src/database/dev.db
19
19
  {{/if}}
20
+ {{#if hasSqlserver}}
21
+ SQLSERVER_HOST=localhost
22
+ SQLSERVER_PORT=1433
23
+ SQLSERVER_DATABASE={{projectNameKebab}}_db
24
+ SQLSERVER_USER=sa
25
+ SQLSERVER_PASSWORD=DevOnly_Strong_Password_2026!X9
26
+ SQLSERVER_ENCRYPT=true
27
+ SQLSERVER_TRUST_SERVER_CERTIFICATE=true
28
+ {{/if}}
20
29
  {{#if hasMongodb}}
21
30
  MONGODB_URL=mongodb://localhost:27017/{{projectNameKebab}}_db
22
31
  {{/if}}
@@ -14,6 +14,15 @@ MYSQL_URL=mysql://root:mysql@localhost:3306/{{projectNameKebab}}_db
14
14
  {{#if hasSqlite}}
15
15
  SQLITE_PATH=./src/database/dev.db
16
16
  {{/if}}
17
+ {{#if hasSqlserver}}
18
+ SQLSERVER_HOST=localhost
19
+ SQLSERVER_PORT=1433
20
+ SQLSERVER_DATABASE={{projectNameKebab}}_db
21
+ SQLSERVER_USER=sa
22
+ SQLSERVER_PASSWORD=DevOnly_Strong_Password_2026!X9
23
+ SQLSERVER_ENCRYPT=true
24
+ SQLSERVER_TRUST_SERVER_CERTIFICATE=true
25
+ {{/if}}
17
26
  {{#if hasMongodb}}
18
27
  MONGODB_URL=mongodb://localhost:27017/{{projectNameKebab}}_db
19
28
  {{/if}}
@@ -54,6 +54,10 @@ This project uses **Prisma** as the ORM. Available commands:
54
54
  {{#if isPnpm}}pnpm prisma:migrate{{else if isYarn}}yarn prisma:migrate{{else if isBun}}bun run prisma:migrate{{else}}npm run prisma:migrate{{/if}}
55
55
  {{#if isPnpm}}pnpm prisma:migrate:deploy{{else if isYarn}}yarn prisma:migrate:deploy{{else if isBun}}bun run prisma:migrate:deploy{{else}}npm run prisma:migrate:deploy{{/if}}
56
56
  ```
57
+ {{#if hasSqlserver}}
58
+
59
+ Set `SQLSERVER_HOST`, `SQLSERVER_PORT`, `SQLSERVER_DATABASE`, `SQLSERVER_USER`, and `SQLSERVER_PASSWORD` in `.env` before running Prisma commands. The SQL Server database must already exist. The generated password is a development-only example that satisfies SQL Server's password policy; replace it with a unique secret before sharing the project or deploying it. For local development, set `SQLSERVER_TRUST_SERVER_CERTIFICATE=true` when using a self-signed certificate; keep certificate validation enabled for production.
60
+ {{/if}}
57
61
  {{/if}}
58
62
 
59
63
  {{#if useDrizzle}}
@@ -229,3 +233,11 @@ Send queries to `POST /graphql`. `{ hello }` is always available. With the Todo
229
233
 
230
234
  Set `MONGODB_URL` including the database name. The native client connects and pings MongoDB during startup and closes with the Nest application. No Prisma migration commands are required. Todo identifiers are serialized ObjectId strings; malformed IDs return 400 and missing mutation targets return 404. Listing returns at most 100 items in identifier order. This baseline performs single-document writes and does not promise multi-document transactions.
231
235
  {{/if}}
236
+ {{#if docker}}
237
+ ## Docker Compose
238
+
239
+ Start the application and its database with `docker compose up --build`. Compose publishes the API on `127.0.0.1:3000` by default (Fastify uses `3001`). If that host port is already in use or reserved, set `APP_PORT` to a free host port before starting; the container's internal port stays unchanged. In PowerShell, for example: `$env:APP_PORT = "3100"; docker compose up --build`.
240
+ {{#if hasSqlserver}}
241
+ Compose also publishes SQL Server on `127.0.0.1:1433` so Prisma commands run on the host can connect. The API container uses the internal `sqlserver:1433` address. If host port 1433 is occupied, change `SQLSERVER_PORT` in `.env`; keep the API container's internal database port at 1433.
242
+ {{/if}}
243
+ {{/if}}
@@ -31,6 +31,7 @@ export default defineConfig({
31
31
  {{#if hasPostgres}}url: env.POSTGRES_URL,{{/if}}
32
32
  {{#if hasMysql}}url: env.MYSQL_URL,{{/if}}
33
33
  {{#if hasSqlite}}url: env.SQLITE_PATH,{{/if}}
34
+ {{#if hasSqlserver}}url: `sqlserver://${env.SQLSERVER_HOST}:${env.SQLSERVER_PORT};database=${env.SQLSERVER_DATABASE};user={${env.SQLSERVER_USER.replaceAll("}", "}}")}};password={${env.SQLSERVER_PASSWORD.replaceAll("}", "}}")}};encrypt=${env.SQLSERVER_ENCRYPT};trustServerCertificate=${env.SQLSERVER_TRUST_SERVER_CERTIFICATE}`,{{/if}}
34
35
  {{#if hasMongodb}}url: env.MONGODB_URL,{{/if}}
35
36
  },
36
37
  });
@@ -86,6 +86,7 @@ class GraphqlThrottlerGuard extends ThrottlerGuard {
86
86
  {{#if hasPostgres}}postgresUrl: env.POSTGRES_URL,{{/if}}
87
87
  {{#if hasMysql}}mysqlUrl: env.MYSQL_URL,{{/if}}
88
88
  {{#if hasSqlite}}sqlitePath: env.SQLITE_PATH,{{/if}}
89
+ {{#if hasSqlserver}}sqlserver: { server: env.SQLSERVER_HOST, port: env.SQLSERVER_PORT, database: env.SQLSERVER_DATABASE, user: env.SQLSERVER_USER, password: env.SQLSERVER_PASSWORD, options: { encrypt: env.SQLSERVER_ENCRYPT, trustServerCertificate: env.SQLSERVER_TRUST_SERVER_CERTIFICATE } },{{/if}}
89
90
  {{#if hasMongodb}}mongodbUrl: env.MONGODB_URL,{{/if}}
90
91
  }),
91
92
  {{/if}}
@@ -1,4 +1,4 @@
1
- {{#if (or hasPostgres hasMysql hasSqlite)}}
1
+ {{#if (or hasPostgres hasMysql hasSqlite hasSqlserver)}}
2
2
  export interface ISqlAdapter<TClient = unknown> {
3
3
  query: <T = unknown>(sql: string, params?: unknown[]) => Promise<T[]>;
4
4
  execute: (sql: string, params?: unknown[]) => Promise<unknown>;
@@ -12,10 +12,10 @@ import type { Collection, Document } from "mongodb";
12
12
  {{/if}}
13
13
  export interface IMongoAdapter<TClient = unknown> {
14
14
  {{#if useRawSql}}
15
- collection<T extends Document = Document>(name: string): Collection<T>;
15
+ collection: <T extends Document = Document>(name: string) => Collection<T>;
16
16
  {{else}}
17
- collection<T = unknown>(name: string): T;
17
+ collection: <T = unknown>(name: string) => T;
18
18
  {{/if}}
19
- getClient(): TClient;
19
+ getClient: () => TClient;
20
20
  }
21
21
  {{/if}}
@@ -23,7 +23,7 @@ import type { Prisma, PrismaClient } from '../../orm/prisma/generated/client.js'
23
23
  {{/if}}
24
24
 
25
25
  @Injectable()
26
- export class PrismaMongoAdapter implements IMongoAdapter<any> {
26
+ export class PrismaMongoAdapter implements IMongoAdapter {
27
27
  {{#if isPrisma8}}
28
28
  readonly client = db;
29
29
 
@@ -31,8 +31,8 @@ export class PrismaMongoAdapter implements IMongoAdapter<any> {
31
31
  return ((this.client as any).orm as any)?.[name] as T;
32
32
  }
33
33
 
34
- async runCommand(command: unknown): Promise<unknown> {
35
- return (this.client as any).query?.(command);
34
+ runCommand(command: unknown): Promise<unknown> {
35
+ return (this.client as { query?: (input: unknown) => Promise<unknown> }).query?.(command) ?? Promise.resolve(undefined);
36
36
  }
37
37
 
38
38
  getClient() {
@@ -46,7 +46,7 @@ export class PrismaMongoAdapter implements IMongoAdapter<any> {
46
46
  return this.prisma[name as keyof PrismaClient] as T;
47
47
  }
48
48
 
49
- async runCommand(command: Prisma.InputJsonObject): Promise<unknown> {
49
+ runCommand(command: Prisma.InputJsonObject): Promise<unknown> {
50
50
  return this.prisma.$runCommandRaw(command);
51
51
  }
52
52
 
@@ -27,7 +27,7 @@ import { PrismaClient } from '@prisma/client';
27
27
  import { PrismaClient } from '../database/orm/prisma/generated/client.js';
28
28
  {{/if}}
29
29
  {{/if}}
30
- {{#if (or hasPostgres hasMysql hasSqlite)}}
30
+ {{#if (or hasPostgres hasMysql hasSqlite hasSqlserver)}}
31
31
  import { PrismaSqlAdapter } from './adapters/sql/prisma-sql.adapter.js';
32
32
  {{/if}}
33
33
  {{#if hasPostgres}}
@@ -38,6 +38,9 @@ import { PrismaMariaDb } from '@prisma/adapter-mariadb';
38
38
  {{/if}}
39
39
  {{#if hasSqlite}}
40
40
  import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3';
41
+ {{/if}}
42
+ {{#if hasSqlserver}}
43
+ import { PrismaMssql } from '@prisma/adapter-mssql';
41
44
  {{/if}}
42
45
  {{#if hasMongodb}}
43
46
  import { PrismaMongoAdapter } from './adapters/mongo/prisma-mongo.adapter.js';
@@ -99,6 +102,9 @@ export interface DbModuleOptions {
99
102
  {{#if hasSqlite}}
100
103
  sqlitePath?: string;
101
104
  {{/if}}
105
+ {{#if hasSqlserver}}
106
+ sqlserver?: { server: string; port: number; database: string; user: string; password: string; options: { encrypt: boolean; trustServerCertificate: boolean } };
107
+ {{/if}}
102
108
  {{#if hasMongodb}}
103
109
  mongodbUrl?: string;
104
110
  {{/if}}
@@ -119,7 +125,7 @@ export class DbModule {
119
125
  const providers = initialProviders;
120
126
 
121
127
  {{#if usePrisma}}
122
- {{#if (or hasPostgres hasMysql hasSqlite)}}
128
+ {{#if (or hasPostgres hasMysql hasSqlite hasSqlserver)}}
123
129
  {{#if hasPostgres}}
124
130
  const sqlAdapter = new PrismaPg({
125
131
  connectionString: options.postgresUrl,
@@ -137,6 +143,13 @@ export class DbModule {
137
143
  const sqlAdapter = new PrismaBetterSqlite3({
138
144
  url: options.sqlitePath || ':memory:',
139
145
  });
146
+ const prismaSql = new PrismaClient({ adapter: sqlAdapter });
147
+ {{/if}}
148
+ {{#if hasSqlserver}}
149
+ if (!options.sqlserver) {
150
+ throw new Error("SQL Server connection settings are required to initialize Prisma");
151
+ }
152
+ const sqlAdapter = new PrismaMssql(options.sqlserver);
140
153
  const prismaSql = new PrismaClient({ adapter: sqlAdapter });
141
154
  {{/if}}
142
155
  providers.push({
@@ -18,7 +18,7 @@ export class DbService {
18
18
  }
19
19
  {{else}}
20
20
  import { Injectable, Inject } from '@nestjs/common';
21
- {{#if (or hasPostgres hasMysql hasSqlite)}}
21
+ {{#if (or hasPostgres hasMysql hasSqlite hasSqlserver)}}
22
22
  import type { ISqlAdapter } from './adapters/interfaces/db-adapter.interface.js';
23
23
  {{/if}}
24
24
  {{#if hasMongodb}}
@@ -58,7 +58,7 @@ import type * as schema from './orm/drizzle/schema.js';
58
58
  {{#if useTypeorm}}
59
59
  import type { DataSource } from 'typeorm';
60
60
  {{/if}}
61
- {{#if (or hasPostgres hasMysql hasSqlite)}}
61
+ {{#if (or hasPostgres hasMysql hasSqlite hasSqlserver)}}
62
62
  {{#if usePrisma}}
63
63
  type SqlClient = PrismaClient;
64
64
  {{else if useTypeorm}}
@@ -91,7 +91,7 @@ import { db as prismaDb } from './prisma/db.js';
91
91
  @Injectable()
92
92
  export class DbService {
93
93
  constructor(
94
- {{#if (or hasPostgres hasMysql hasSqlite)}}
94
+ {{#if (or hasPostgres hasMysql hasSqlite hasSqlserver)}}
95
95
  @Inject('SQL_ADAPTER') readonly sql: ISqlAdapter<SqlClient>{{#if hasMongodb}},{{/if}}
96
96
  {{/if}}
97
97
  {{#if hasMongodb}}
@@ -101,7 +101,7 @@ export class DbService {
101
101
 
102
102
  get raw() {
103
103
  return {
104
- {{#if (or hasPostgres hasMysql hasSqlite)}}
104
+ {{#if (or hasPostgres hasMysql hasSqlite hasSqlserver)}}
105
105
  sql: this.sql.getClient(),
106
106
  {{/if}}
107
107
  {{#if hasMongodb}}
@@ -114,7 +114,7 @@ export class DbService {
114
114
  return prismaDb;
115
115
  }
116
116
  {{else if usePrisma}}
117
- {{#if (or hasPostgres hasMysql hasSqlite)}}
117
+ {{#if (or hasPostgres hasMysql hasSqlite hasSqlserver)}}
118
118
  get prisma(): PrismaClient {
119
119
  return this.sql.getClient();
120
120
  }
@@ -4,12 +4,21 @@ export * from './db.module.js';
4
4
  export * from './db.service.js';
5
5
  {{#if usePrisma}}
6
6
  {{#if isPrisma6}}
7
- export type { PrismaClient, Todo } from '@prisma/client';
7
+ export type { PrismaClient } from '@prisma/client';
8
+ {{#if hasTodoExample}}
9
+ export type { Todo } from '@prisma/client';
10
+ {{/if}}
8
11
  {{else}}
9
12
  {{#if isMonorepo}}
10
- export type { PrismaClient, Todo } from './database/orm/prisma/generated/client.js';
13
+ export type { PrismaClient } from './database/orm/prisma/generated/client.js';
14
+ {{#if hasTodoExample}}
15
+ export type { Todo } from './database/orm/prisma/generated/client.js';
16
+ {{/if}}
11
17
  {{else}}
12
- export type { PrismaClient, Todo } from './orm/prisma/generated/client.js';
18
+ export type { PrismaClient } from './orm/prisma/generated/client.js';
19
+ {{#if hasTodoExample}}
20
+ export type { Todo } from './orm/prisma/generated/client.js';
21
+ {{/if}}
13
22
  {{/if}}
14
23
  {{/if}}
15
24
  {{/if}}
@@ -61,7 +61,6 @@ async function bootstrap() {
61
61
  apiReference({
62
62
  content: await createDocs(app, {
63
63
  description: "{{description}}",
64
- title: "{{projectNamePascalCase}} API",
65
64
  integrations: {
66
65
  {{#if (or (eq auth "better-auth") (eq auth "better-auth:stateless") hasBetterAuthWithoutRest)}}
67
66
  betterAuth: {
@@ -79,17 +78,17 @@ async function bootstrap() {
79
78
  withFastify: {{#if (eq httpProvider "fastify")}}true{{else}}false{{/if}},
80
79
  },
81
80
  },
81
+ title: "{{projectNamePascalCase}} API",
82
82
  version: "1.0.0",
83
83
  }),
84
+ {{#if (eq httpProvider "fastify")}}withFastify: true,{{/if}}
84
85
  pageTitle: "{{projectNamePascalCase}} API Docs",
85
86
  theme: "default",
86
87
  }),
87
88
  );
88
89
  {{else}}
89
90
  const document = await createDocs(app, {
90
- title: "{{projectNamePascalCase}} API",
91
91
  description: "{{description}}",
92
- version: "1.0.0",
93
92
  integrations: {
94
93
  {{#if (or (eq auth "better-auth") (eq auth "better-auth:stateless") hasBetterAuthWithoutRest)}}
95
94
  betterAuth: {
@@ -107,12 +106,14 @@ async function bootstrap() {
107
106
  withFastify: {{#if (eq httpProvider "fastify")}}true{{else}}false{{/if}},
108
107
  },
109
108
  },
109
+ title: "{{projectNamePascalCase}} API",
110
+ version: "1.0.0",
110
111
  });
111
112
  SwaggerModule.setup(docsPath, app, document);
112
113
  {{/if}}
113
114
  Logger.log(`Swagger documentation available at http://localhost:${env.PORT}${docsPath}`, "SwaggerDocs");
114
115
  {{/if}}
115
- await app.listen(env.PORT);
116
+ await app.listen(env.PORT, "0.0.0.0");
116
117
  {{#if hasRest}}
117
118
  Logger.log(`Started backend server on http://localhost:${env.PORT}/api`, "Main");
118
119
  {{else if hasTrpc}}
@@ -7,7 +7,7 @@ import { Test, TestingModule } from "@nestjs/testing";
7
7
  import { createServer } from "node:http";
8
8
  import { generateKeyPairSync, createSign } from "node:crypto";
9
9
  {{/if}}
10
- {{#if (and isPassportAuth hasRateLimiting hasDatabase)}}
10
+ {{#if (and hasTrustedProxy isPassportAuth hasRateLimiting hasDatabase)}}
11
11
  import { createServer, request as httpRequest } from "node:http";
12
12
  {{/if}}
13
13
  {{#if hasTrustedProxy}}
@@ -184,7 +184,11 @@ describe("AppController (e2e)", () => {
184
184
  },
185
185
  });
186
186
  {{#if useSwaggerScalar}}
187
- app.use("/docs", apiReference({ content: generatedOpenApi, theme: "default" }));
187
+ app.use("/docs", apiReference({
188
+ content: generatedOpenApi,
189
+ {{#if (eq httpProvider "fastify")}}withFastify: true,{{/if}}
190
+ theme: "default",
191
+ }));
188
192
  {{/if}}
189
193
  {{/if}}
190
194
  await app.init();
@@ -236,7 +240,7 @@ describe("AppController (e2e)", () => {
236
240
  {{/if}}
237
241
 
238
242
  {{#if swagger}}
239
- it("generates authenticated OpenAPI docs and serves the Scalar reference", async () => {
243
+ it("generates authenticated OpenAPI docs and serves the Scalar reference", {{#if useSwaggerScalar}}async {{/if}}() => {
240
244
  expect(generatedOpenApi.openapi).toMatch(/^3\./u);
241
245
  {{#if isBetterAuth}}
242
246
  expect(generatedOpenApi.paths?.["/api/auth/sign-up/email"]).toBeDefined();
@@ -319,8 +323,8 @@ describe("AppController (e2e)", () => {
319
323
  {{#if (and hasHealthCheck hasDatabase (or usePrisma useTypeorm useDrizzle))}}
320
324
  it("reports database readiness failure with HTTP 503", async () => {
321
325
  const db = app.get(DbService);
322
- const database = {{#if usePrisma}}db.prisma{{else if useTypeorm}}db.typeorm{{else}}db.drizzle{{/if}};
323
- const query = vi.spyOn(database, {{#if usePrisma}}"$queryRaw"{{else if useTypeorm}}"query"{{else if hasSqlite}}"get"{{else}}"execute"{{/if}})
326
+ const database = {{#if hasMongodb}}db.raw.mongo{{else if usePrisma}}db.prisma{{else if useTypeorm}}db.typeorm{{else}}db.drizzle{{/if}};
327
+ const query = vi.spyOn(database, {{#if hasMongodb}}{{#if usePrisma}}"$runCommandRaw"{{else}}"command"{{/if}}{{else if usePrisma}}"$queryRaw"{{else if useTypeorm}}"query"{{else if hasSqlite}}"get"{{else}}"execute"{{/if}})
324
328
  .mockRejectedValue(new Error("scenario database outage"));
325
329
  try {
326
330
  const response = await request(app.getHttpServer()).get("/health").expect(503);
@@ -344,7 +348,6 @@ describe("AppController (e2e)", () => {
344
348
  it("enforces todo ownership through authenticated tRPC procedures", async () => {
345
349
  const server = app.getHttpServer();
346
350
  const password = "Scenario-password-123!";
347
- const origin = "http://localhost:3000";
348
351
  const call = (procedure: string, input?: unknown, credential?: { cookie?: string[]; token?: string }) => {
349
352
  const operation = procedure === "findAll"
350
353
  ? request(server).get("/trpc/todoRouter.findAll")
@@ -354,11 +357,10 @@ describe("AppController (e2e)", () => {
354
357
  if (input !== undefined) operation.send(input);
355
358
  return operation;
356
359
  };
357
- const unwrap = (body: { result?: { data?: { json?: unknown } } }) =>
358
- body.result?.data?.json ?? body.result?.data;
359
360
  {{#if isBetterAuth}}
360
361
  async function register(prefix: string) {
361
362
  const email = `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
363
+ const origin = "http://localhost:3000";
362
364
  const response = await request(server).post("/api/auth/sign-up/email")
363
365
  .set("Origin", origin).send({ email, password, name: prefix }).expect(200);
364
366
  return { cookie: response.headers["set-cookie"] as string[] };
@@ -379,21 +381,50 @@ describe("AppController (e2e)", () => {
379
381
  const alice = await register("trpc-alice");
380
382
  const bob = await register("trpc-bob");
381
383
  const created = await call("create", { title: "Private tRPC todo", completed: false }, alice).expect(200);
382
- const createdTodo = unwrap(created.body) as { id: string; title: string };
384
+ const createdTodo = (created.body.result?.data?.json ?? created.body.result?.data) as { id: string; title: string };
383
385
  expect(createdTodo.title).toBe("Private tRPC todo");
384
386
  const id = createdTodo.id;
385
- const aliceTodos = unwrap((await call("findAll", undefined, alice).expect(200)).body) as { id: string }[];
387
+ const aliceResponse = await call("findAll", undefined, alice).expect(200);
388
+ const aliceTodos = aliceResponse.body.result?.data?.json ?? aliceResponse.body.result?.data as { id: string }[];
386
389
  expect(aliceTodos).toEqual(expect.arrayContaining([expect.objectContaining({ id })]));
387
- const bobTodos = unwrap((await call("findAll", undefined, bob).expect(200)).body) as { id: string }[];
390
+ const bobResponse = await call("findAll", undefined, bob).expect(200);
391
+ const bobTodos = bobResponse.body.result?.data?.json ?? bobResponse.body.result?.data as { id: string }[];
388
392
  expect(bobTodos).not.toEqual(expect.arrayContaining([expect.objectContaining({ id })]));
389
393
  await call("update", { id, data: { completed: true } }, bob).expect(404);
390
394
  await call("delete", { id }, bob).expect(404);
391
395
  const updated = await call("update", { id, data: { completed: true } }, alice).expect(200);
392
- expect((unwrap(updated.body) as { completed: boolean }).completed).toBe(true);
396
+ const updatedTodo = updated.body.result?.data?.json ?? updated.body.result?.data;
397
+ expect((updatedTodo as { completed: boolean }).completed).toBe(true);
393
398
  await call("delete", { id }, alice).expect(200);
394
399
  });
395
400
  {{/if}}
396
401
 
402
+ {{#if (and hasTrpc hasTodoExample (not hasAuth))}}
403
+ it("creates, reads, updates, and deletes tRPC todos", async () => {
404
+ const server = app.getHttpServer();
405
+ const create = await request(server)
406
+ .post("/trpc/todoRouter.create")
407
+ .send({ title: "Public tRPC todo", completed: false })
408
+ .expect(200);
409
+ const created = create.body.result?.data?.json ?? create.body.result?.data;
410
+ expect(created.title).toBe("Public tRPC todo");
411
+ const id = created.id as string | number;
412
+
413
+ const list = await request(server).get("/trpc/todoRouter.findAll").expect(200);
414
+ const todos = list.body.result?.data?.json ?? list.body.result?.data;
415
+ expect(todos).toEqual(expect.arrayContaining([expect.objectContaining({ id })]));
416
+
417
+ const update = await request(server)
418
+ .post("/trpc/todoRouter.update")
419
+ .send({ id, data: { completed: true } })
420
+ .expect(200);
421
+ const updated = update.body.result?.data?.json ?? update.body.result?.data;
422
+ expect(updated.completed).toBe(true);
423
+
424
+ await request(server).post("/trpc/todoRouter.delete").send({ id }).expect(200);
425
+ });
426
+ {{/if}}
427
+
397
428
  {{#if (eq auth "better-auth")}}
398
429
  it("registers, authenticates and revokes a persisted session", async () => {
399
430
  const email = `scenario-${Date.now()}@example.com`;
@@ -634,8 +665,14 @@ expect(revoked.body).toBeNull();
634
665
  it("throttles repeated tRPC procedure requests", async () => {
635
666
  const server = app.getHttpServer();
636
667
  server.setMaxListeners(100);
637
- const procedure = () => request(server).get("/trpc/appRouter.testAuth");
668
+ const procedure = () => request(server).get(
669
+ "/trpc/appRouter.{{#if hasTrpcAndAuth}}testAuth{{else}}getHello{{/if}}"
670
+ );
671
+ {{#if hasTrpcAndAuth}}
638
672
  expect((await procedure()).status).toBe(401);
673
+ {{else}}
674
+ expect((await procedure()).status).toBe(200);
675
+ {{/if}}
639
676
  const responses = await Promise.all(
640
677
  Array.from({ length: 80 }, () => procedure())
641
678
  );
@@ -19,7 +19,7 @@ async function getFreePort(): Promise<number> {
19
19
  return address.port;
20
20
  }
21
21
 
22
- async function waitForServer(url: string, application: ChildProcess, deadline = Date.now() + 15000): Promise<Response> {
22
+ async function waitForServer(url: string, application: ChildProcess, deadline = Date.now() + 60000): Promise<Response> {
23
23
  if (application.exitCode !== null) throw new Error(`Generated application exited during startup with code ${application.exitCode}.`);
24
24
  try {
25
25
  return await fetch(url, { signal: AbortSignal.timeout(500) });
@@ -81,4 +81,4 @@ it("boots the compiled application through main.ts", async () => {
81
81
  const body = await response.json();
82
82
  expect(body.data.hello).toBe("Hello GraphQL!");
83
83
  {{/if}}
84
- }, 20_000);
84
+ }, 65_000);
@@ -8,6 +8,7 @@
8
8
  "**/*spec.ts"
9
9
  ],
10
10
  "compilerOptions": {
11
+ "incremental": false,
11
12
  "rootDir": "./src",
12
13
  "strictNullChecks": true
13
14
  }
@@ -5,8 +5,8 @@ export default defineConfig({
5
5
  tsconfigPaths: true,
6
6
  },
7
7
  test: {
8
- include: ["**/*.e2e-spec.ts"],
9
8
  globals: true,
9
+ include: ["**/*.e2e-spec.ts"],
10
10
  root: "./",
11
11
  },
12
12
  });
@@ -5,8 +5,8 @@ export default defineConfig({
5
5
  tsconfigPaths: true,
6
6
  },
7
7
  test: {
8
- include: ["**/*.spec.ts"],
9
8
  globals: true,
9
+ include: ["**/*.spec.ts"],
10
10
  root: "./",
11
11
  },
12
12
  });
@@ -10,7 +10,7 @@ RUN corepack enable
10
10
  {{/if}}
11
11
 
12
12
  COPY . .
13
- RUN POSTGRES_URL=postgresql://postgres:postgres@postgres:5432/build_db MYSQL_URL=mysql://root:mysql@mysql:3306/build_db MONGODB_URL=mongodb://mongodb:27017/build_db {{#if (eq auth "passport")}}JWT_ACCESS_SECRET=build-only-access-secret-that-is-long-enough JWT_REFRESH_SECRET=build-only-refresh-secret-that-is-long-enough {{/if}}{{#if (or (eq auth "better-auth") (eq auth "better-auth:stateless"))}}BETTER_AUTH_SECRET=build-only-better-auth-secret-that-is-long-enough BETTER_AUTH_URL=http://localhost:{{#if (eq httpProvider "fastify")}}3001{{else}}3000{{/if}} {{/if}}{{#if (eq packageManager "pnpm")}}pnpm install --no-lockfile{{else if (eq packageManager "yarn")}}yarn install{{else if (eq packageManager "bun")}}bun install{{else}}npm install{{/if}}
13
+ RUN POSTGRES_URL=postgresql://postgres:postgres@postgres:5432/build_db MYSQL_URL=mysql://root:mysql@mysql:3306/build_db MONGODB_URL=mongodb://mongodb:27017/build_db {{#if hasSqlserver}}SQLSERVER_HOST=localhost SQLSERVER_PORT=1433 SQLSERVER_DATABASE=build_db SQLSERVER_USER=sa SQLSERVER_PASSWORD=Build_only_Password_2026!X9 SQLSERVER_ENCRYPT=true SQLSERVER_TRUST_SERVER_CERTIFICATE=true {{/if}}{{#if (eq auth "passport")}}JWT_ACCESS_SECRET=build-only-access-secret-that-is-long-enough JWT_REFRESH_SECRET=build-only-refresh-secret-that-is-long-enough {{/if}}{{#if (or (eq auth "better-auth") (eq auth "better-auth:stateless"))}}BETTER_AUTH_SECRET=build-only-better-auth-secret-that-is-long-enough BETTER_AUTH_URL=http://localhost:{{#if (eq httpProvider "fastify")}}3001{{else}}3000{{/if}} {{/if}}{{#if (eq packageManager "pnpm")}}pnpm install --no-lockfile{{else if (eq packageManager "yarn")}}yarn install{{else if (eq packageManager "bun")}}bun install{{/if}}
14
14
  RUN {{#if (eq packageManager "pnpm")}}pnpm run{{else if (eq packageManager "yarn")}}yarn{{else if (eq packageManager "bun")}}bun run{{else}}npm run{{/if}} build
15
15
 
16
16
  ENV NODE_ENV=production
@@ -28,7 +28,13 @@ export class HealthController {
28
28
  {{#if hasDatabase}}
29
29
  async () => {
30
30
  try {
31
- {{#if (or usePrisma isPrisma8)}}
31
+ {{#if hasMongodb}}
32
+ {{#if usePrisma}}
33
+ await this.db.raw.mongo.$runCommandRaw({ ping: 1 });
34
+ {{else}}
35
+ await this.db.raw.mongo.command({ ping: 1 });
36
+ {{/if}}
37
+ {{else if (or usePrisma isPrisma8)}}
32
38
  await this.db.prisma.$queryRaw`SELECT 1`;
33
39
  {{else if useDrizzle}}
34
40
  {{#if hasSqlite}}
@@ -38,8 +44,6 @@ export class HealthController {
38
44
  {{/if}}
39
45
  {{else if useTypeorm}}
40
46
  await this.db.typeorm.query("SELECT 1");
41
- {{else if hasMongodb}}
42
- await this.db.raw.mongo.command({ ping: 1 });
43
47
  {{else}}
44
48
  await this.db.raw.sql.query("SELECT 1");
45
49
  {{/if}}
@@ -22,11 +22,9 @@ import {
22
22
  {{#if (or isPassportAuth isBetterAuth)}}
23
23
  ApiUnauthorizedResponse,
24
24
  {{/if}}
25
- } from "@nestjs/swagger";
26
- {{/if}}
27
- {{#if (and isPassportAuth swagger)}}
28
- import {
25
+ {{#if isPassportAuth}}
29
26
  ApiBearerAuth,
27
+ {{/if}}
30
28
  } from "@nestjs/swagger";
31
29
  {{/if}}
32
30
  {{#if (and isBetterAuth hasDatabase)}}
@@ -162,14 +160,14 @@ export class TodoController {
162
160
  {{/if}}
163
161
  {{/if}}
164
162
  {{#if (and isPassportAuth hasDatabase)}}
165
- async delete(
163
+ delete(
166
164
  @Param("id") id: string,
167
165
  @CurrentUser() user: { id: string }
168
166
  ) {
169
167
  return this.todoService.delete(id, user.id);
170
168
  }
171
169
  {{else if (and isBetterAuth hasDatabase)}}
172
- async delete(
170
+ delete(
173
171
  @Param("id") id: string,
174
172
  @Session() session: UserSession
175
173
  ) {
@@ -1,11 +1,8 @@
1
1
  {{#if hasTrpc}}
2
- import { Router, Query, Mutation, Ctx, Input{{#if hasTrpcAndAuth}}, UseMiddlewares{{/if}} } from "nestjs-trpc";
2
+ import { Router, Query, Mutation{{#if hasTrpcAndAuth}}, Ctx, UseMiddlewares{{/if}}, Input } from "nestjs-trpc";
3
3
  import { NotFoundException } from "@nestjs/common";
4
4
  import { TRPCError } from "@trpc/server";
5
- import z from "zod";
6
- {{#if (and hasTrpcAndAuth isBetterAuth)}}
7
- import type { UserSession } from "../../auth/auth.js";
8
- {{/if}}
5
+ import { z } from "zod";
9
6
  {{#if hasTrpcAndAuth}}
10
7
  import { AuthTRPCMiddleware } from "../../libs/middlewares/auth.trpc.middleware.js";
11
8
  import type { InnerContext } from "../../libs/trpc/inner.context.js";
@@ -46,26 +43,28 @@ export class TodoRouter {
46
43
 
47
44
  @Mutation({ input: updateTodoInput, output: todoSchema })
48
45
  @UseMiddlewares(AuthTRPCMiddleware)
49
- update(@Ctx() ctx: InnerContext, @Input() input: { id: string | number; data: UpdateTodoDto }) {
50
- return this.todoService.update(String(input.id), input.data, ctx.session!.user.id)
51
- .catch((error: unknown) => {
52
- if (error instanceof NotFoundException) {
53
- throw new TRPCError({ code: "NOT_FOUND", message: error.message });
54
- }
55
- throw error;
56
- });
46
+ async update(@Ctx() ctx: InnerContext, @Input() input: { id: string | number; data: UpdateTodoDto }) {
47
+ try {
48
+ return await this.todoService.update(String(input.id), input.data, ctx.session!.user.id);
49
+ } catch (error) {
50
+ if (error instanceof NotFoundException) {
51
+ throw new TRPCError({ code: "NOT_FOUND", message: error.message });
52
+ }
53
+ throw error;
54
+ }
57
55
  }
58
56
 
59
57
  @Mutation({ input: z.object({ id: z.union([z.string(), z.number()]) }) })
60
58
  @UseMiddlewares(AuthTRPCMiddleware)
61
- delete(@Ctx() ctx: InnerContext, @Input() input: { id: string | number }) {
62
- return this.todoService.delete(String(input.id), ctx.session!.user.id)
63
- .catch((error: unknown) => {
64
- if (error instanceof NotFoundException) {
65
- throw new TRPCError({ code: "NOT_FOUND", message: error.message });
66
- }
67
- throw error;
68
- });
59
+ async delete(@Ctx() ctx: InnerContext, @Input() input: { id: string | number }) {
60
+ try {
61
+ return await this.todoService.delete(String(input.id), ctx.session!.user.id);
62
+ } catch (error) {
63
+ if (error instanceof NotFoundException) {
64
+ throw new TRPCError({ code: "NOT_FOUND", message: error.message });
65
+ }
66
+ throw error;
67
+ }
69
68
  }
70
69
  {{else}}
71
70
  @Query({ output: z.array(todoSchema) })
@@ -79,25 +78,27 @@ export class TodoRouter {
79
78
  }
80
79
 
81
80
  @Mutation({ input: updateTodoInput, output: todoSchema })
82
- update(@Input() input: { id: string; data: UpdateTodoDto }) {
83
- return this.todoService.update(input.id, input.data)
84
- .catch((error: unknown) => {
85
- if (error instanceof NotFoundException) {
86
- throw new TRPCError({ code: "NOT_FOUND", message: error.message });
87
- }
88
- throw error;
89
- });
81
+ async update(@Input() input: { id: string; data: UpdateTodoDto }) {
82
+ try {
83
+ return await this.todoService.update(input.id, input.data);
84
+ } catch (error) {
85
+ if (error instanceof NotFoundException) {
86
+ throw new TRPCError({ code: "NOT_FOUND", message: error.message });
87
+ }
88
+ throw error;
89
+ }
90
90
  }
91
91
 
92
92
  @Mutation({ input: z.object({ id: z.union([z.string(), z.number()]) }) })
93
- delete(@Input() input: { id: string | number }) {
94
- return this.todoService.delete(String(input.id))
95
- .catch((error: unknown) => {
96
- if (error instanceof NotFoundException) {
97
- throw new TRPCError({ code: "NOT_FOUND", message: error.message });
98
- }
99
- throw error;
100
- });
93
+ async delete(@Input() input: { id: string | number }) {
94
+ try {
95
+ return await this.todoService.delete(String(input.id));
96
+ } catch (error) {
97
+ if (error instanceof NotFoundException) {
98
+ throw new TRPCError({ code: "NOT_FOUND", message: error.message });
99
+ }
100
+ throw error;
101
+ }
101
102
  }
102
103
  {{/if}}
103
104
  }