@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.
- package/dist/cli.js +1 -1
- package/dist/{constants-7dJLZpB2.js → constants-DVhgS48N.js} +1 -1
- package/dist/index.js +148 -133
- package/dist/templates/addons/husky/noop-hook.hbs +2 -0
- package/dist/templates/base/.env.example.hbs +9 -0
- package/dist/templates/base/.env.hbs +9 -0
- package/dist/templates/base/README.md.hbs +12 -0
- package/dist/templates/base/prisma.config.ts.hbs +1 -0
- package/dist/templates/base/src/app.module.ts.hbs +1 -0
- package/dist/templates/base/src/database/adapters/interfaces/db-adapter.interface.ts.hbs +4 -4
- package/dist/templates/base/src/database/adapters/mongo/prisma-mongo.adapter.ts.hbs +4 -4
- package/dist/templates/base/src/database/db.module.ts.hbs +15 -2
- package/dist/templates/base/src/database/db.service.ts.hbs +5 -5
- package/dist/templates/base/src/database/index.ts.hbs +12 -3
- package/dist/templates/base/src/main.ts.hbs +5 -4
- package/dist/templates/base/test/app.e2e-spec.ts.hbs +50 -13
- package/dist/templates/base/test/bootstrap.e2e-spec.ts.hbs +2 -2
- package/dist/templates/base/tsconfig.build.json.hbs +1 -0
- package/dist/templates/base/vitest.config.e2e.ts.hbs +1 -1
- package/dist/templates/base/vitest.config.ts.hbs +1 -1
- package/dist/templates/extras/docker/Dockerfile.hbs +1 -1
- package/dist/templates/extras/health/src/health/health.controller.ts.hbs +7 -3
- package/dist/templates/feature/rest/todo-example/src/api/todo/todo.controller.ts.hbs +4 -6
- package/dist/templates/feature/rest/todo-example/src/api/todo/todo.router.ts.hbs +38 -37
- package/dist/templates/monorepo/apps/api-gateway/src/main.ts.hbs +8 -7
- package/dist/templates/monorepo/apps/api-gateway/test/app.e2e-spec.ts.hbs +4 -1
- package/dist/templates/single-project/architecture/nest-api/.env.hbs +9 -0
- package/dist/templates/single-project/architecture/nest-api/src/main.ts.hbs +8 -7
- package/dist/templates/single-project/architecture/nest-gateway/src/main.ts.hbs +8 -7
- package/dist/templates/single-project/architecture/nest-microservice/src/app.controller.ts.hbs +52 -1
- package/dist/templates/single-project/architecture/nest-microservice/src/app.service.ts.hbs +21 -0
- package/dist/templates/single-project/architecture/nest-microservice/src/main.ts.hbs +35 -16
- package/dist/templates/single-project/architecture/nest-microservice/test/microservice.e2e-spec.ts.hbs +51 -19
- package/dist/templates/single-project/shared/config/env.ts.hbs +27 -16
- package/package.json +2 -2
|
@@ -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)
|
|
15
|
+
collection: <T extends Document = Document>(name: string) => Collection<T>;
|
|
16
16
|
{{else}}
|
|
17
|
-
collection<T = unknown>(name: string)
|
|
17
|
+
collection: <T = unknown>(name: string) => T;
|
|
18
18
|
{{/if}}
|
|
19
|
-
getClient()
|
|
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
|
|
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
|
-
|
|
35
|
-
return (this.client as
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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({
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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() +
|
|
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
|
-
},
|
|
84
|
+
}, 65_000);
|
|
@@ -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{{
|
|
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
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
51
|
-
.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
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
|
-
|
|
63
|
-
.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
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
|
-
|
|
84
|
-
.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
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
|
-
|
|
95
|
-
.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
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
|
}
|