@nest-arch/tui 0.5.0 → 0.5.1
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/README.md +31 -0
- package/dist/cli.js +2 -1
- package/dist/{constants-DX5wiqrq.js → constants-CUTbfu9C.js} +1 -1
- package/dist/index.js +6 -417
- package/dist/nonInteractive-C1KZ0b_6.js +5 -0
- package/dist/nonInteractiveOptions-CIaoLAWg.js +24 -0
- package/dist/projectGeneratorService-CeWJK-HA.js +420 -0
- package/dist/projectName-CcE90t2V.js +1 -0
- package/dist/templates/base/.env.example.hbs +23 -0
- package/dist/templates/base/prisma.config.ts.hbs +6 -6
- package/dist/templates/base/src/app.controller.spec.ts.hbs +6 -0
- package/dist/templates/base/src/app.module.ts.hbs +1 -1
- package/dist/templates/base/src/database/adapters/interfaces/db-adapter.interface.ts.hbs +5 -5
- package/dist/templates/base/src/database/adapters/mongo/raw-mongo.adapter.ts.hbs +2 -2
- package/dist/templates/base/src/database/adapters/mongo/typeorm-mongo.adapter.ts.hbs +27 -0
- package/dist/templates/base/src/database/db.module.ts.hbs +22 -16
- package/dist/templates/base/src/database/db.service.ts.hbs +7 -2
- package/dist/templates/base/src/database/entities/todo.entity.ts.hbs +15 -3
- package/dist/templates/base/src/database/orm/drizzle/schema.ts.hbs +38 -14
- package/dist/templates/base/src/database/orm/typeorm/data-source.ts.hbs +12 -6
- package/dist/templates/base/src/libs/middlewares/rate-limit.trpc.middleware.ts.hbs +2 -1
- package/dist/templates/base/test/app.e2e-spec.ts.hbs +14 -10
- package/dist/templates/extras/docker/Dockerfile.hbs +2 -2
- package/dist/templates/extras/health/src/health/health.controller.ts.hbs +5 -0
- package/dist/templates/feature/rest/todo-example/src/api/todo/todo.router.ts.hbs +4 -4
- package/dist/templates/feature/rest/todo-example/src/api/todo/todo.service.ts.hbs +135 -11
- package/dist/templates/single-project/architecture/nest-microservice/.env.hbs +24 -1
- package/dist/templates/single-project/architecture/nest-microservice/proto/app.proto.hbs +13 -0
- package/dist/templates/single-project/architecture/nest-microservice/src/app.controller.ts.hbs +24 -0
- package/dist/templates/single-project/architecture/nest-microservice/src/app.module.ts.hbs +33 -0
- package/dist/templates/single-project/architecture/nest-microservice/src/app.service.ts.hbs +16 -4
- package/dist/templates/single-project/architecture/nest-microservice/src/main.ts.hbs +25 -6
- package/dist/templates/single-project/architecture/nest-microservice/test/microservice.e2e-spec.ts.hbs +7 -122
- package/dist/templates/single-project/shared/config/env.ts.hbs +14 -14
- package/package.json +2 -1
|
@@ -1,25 +1,123 @@
|
|
|
1
|
+
{{#if (and hasMongodb useTypeorm)}}
|
|
2
|
+
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
|
3
|
+
import { ObjectId } from "mongodb";
|
|
4
|
+
import type { Document, WithId } from "mongodb";
|
|
5
|
+
|
|
6
|
+
import { DbService } from "../../database/db.service.js";
|
|
7
|
+
import { CreateTodoDto } from "./dto/create-todo.dto.js";
|
|
8
|
+
import { UpdateTodoDto } from "./dto/update-todo.dto.js";
|
|
9
|
+
|
|
10
|
+
interface TodoDocument extends Document {
|
|
11
|
+
completed: boolean;
|
|
12
|
+
createdAt: Date;
|
|
13
|
+
description?: string;
|
|
14
|
+
title: string;
|
|
15
|
+
updatedAt: Date;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const serialize = ({ _id, ...todo }: WithId<TodoDocument>) => ({
|
|
19
|
+
...todo,
|
|
20
|
+
id: _id.toHexString(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function objectId(id: string): ObjectId {
|
|
24
|
+
if (!/^[a-f0-9]{24}$/iu.test(id)) {
|
|
25
|
+
throw new BadRequestException("Invalid todo identifier");
|
|
26
|
+
}
|
|
27
|
+
return new ObjectId(id);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Injectable()
|
|
31
|
+
export class TodoService {
|
|
32
|
+
constructor(private readonly db: DbService) {}
|
|
33
|
+
|
|
34
|
+
private get todos() {
|
|
35
|
+
return this.db.mongo.collection<TodoDocument>("todos");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async findAll() {
|
|
39
|
+
const todos = await this.todos.find().sort({ _id: 1 }).limit(100).toArray();
|
|
40
|
+
return todos.map(serialize);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async create(input: CreateTodoDto) {
|
|
44
|
+
const now = new Date();
|
|
45
|
+
const todo = {
|
|
46
|
+
completed: input.completed ?? false,
|
|
47
|
+
createdAt: now,
|
|
48
|
+
...(input.description === undefined ? {} : { description: input.description }),
|
|
49
|
+
title: input.title,
|
|
50
|
+
updatedAt: now,
|
|
51
|
+
};
|
|
52
|
+
const result = await this.todos.insertOne(todo);
|
|
53
|
+
return { ...todo, id: result.insertedId.toHexString() };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async update(id: string, input: UpdateTodoDto) {
|
|
57
|
+
const _id = objectId(id);
|
|
58
|
+
await this.todos.updateOne({ _id }, { $set: { ...input, updatedAt: new Date() } });
|
|
59
|
+
const todo = await this.todos.findOne({ _id });
|
|
60
|
+
if (!todo) {
|
|
61
|
+
throw new NotFoundException("Todo not found");
|
|
62
|
+
}
|
|
63
|
+
return serialize(todo);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async delete(id: string) {
|
|
67
|
+
const result = await this.todos.deleteOne({ _id: objectId(id) });
|
|
68
|
+
if (result.deletedCount === 0) {
|
|
69
|
+
throw new NotFoundException("Todo not found");
|
|
70
|
+
}
|
|
71
|
+
return { message: "Todo deleted successfully" };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
{{else}}
|
|
1
75
|
{{#if (and hasMongodb useRawSql)}}
|
|
2
76
|
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
|
3
|
-
import { ObjectId
|
|
77
|
+
import { ObjectId } from "mongodb";
|
|
78
|
+
import type { Document, WithId } from "mongodb";
|
|
79
|
+
|
|
4
80
|
import { DbService } from "../../database/db.service.js";
|
|
5
81
|
import { CreateTodoDto } from "./dto/create-todo.dto.js";
|
|
6
82
|
import { UpdateTodoDto } from "./dto/update-todo.dto.js";
|
|
7
|
-
|
|
8
|
-
|
|
83
|
+
|
|
84
|
+
interface TodoDocument extends Document {
|
|
85
|
+
completed: boolean;
|
|
86
|
+
description?: string;
|
|
87
|
+
title: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const serialize = ({ _id, ...todo }: WithId<TodoDocument>) => ({
|
|
91
|
+
...todo,
|
|
92
|
+
id: _id.toHexString(),
|
|
93
|
+
});
|
|
94
|
+
|
|
9
95
|
function objectId(id: string): ObjectId {
|
|
10
|
-
if (!/^[a-f0-9]{24}$/iu.test(id))
|
|
96
|
+
if (!/^[a-f0-9]{24}$/iu.test(id)) {
|
|
97
|
+
throw new BadRequestException("Invalid todo identifier");
|
|
98
|
+
}
|
|
11
99
|
return new ObjectId(id);
|
|
12
100
|
}
|
|
101
|
+
|
|
13
102
|
@Injectable()
|
|
14
103
|
export class TodoService {
|
|
15
104
|
constructor(private readonly db: DbService) {}
|
|
16
|
-
|
|
105
|
+
|
|
106
|
+
private get todos() {
|
|
107
|
+
return this.db.raw.mongo.collection<TodoDocument>("todos");
|
|
108
|
+
}
|
|
109
|
+
|
|
17
110
|
async findAll() {
|
|
18
111
|
const todos = await this.todos.find().sort({ _id: 1 }).limit(100).toArray();
|
|
19
112
|
return todos.map(serialize);
|
|
20
113
|
}
|
|
114
|
+
|
|
21
115
|
async create(input: CreateTodoDto) {
|
|
22
|
-
const todo = {
|
|
116
|
+
const todo = {
|
|
117
|
+
completed: input.completed ?? false,
|
|
118
|
+
...(input.description === undefined ? {} : { description: input.description }),
|
|
119
|
+
title: input.title,
|
|
120
|
+
};
|
|
23
121
|
const result = await this.todos.insertOne(todo);
|
|
24
122
|
return { ...todo, id: result.insertedId.toHexString() };
|
|
25
123
|
}
|
|
@@ -29,18 +127,22 @@ export class TodoService {
|
|
|
29
127
|
async update(id: string, input: UpdateTodoDto) {
|
|
30
128
|
{{/if}}
|
|
31
129
|
const todo = await this.todos.findOneAndUpdate({ _id: objectId(id) }, { $set: input }, { returnDocument: "after" });
|
|
32
|
-
if (!todo)
|
|
130
|
+
if (!todo) {
|
|
131
|
+
throw new NotFoundException("Todo not found");
|
|
132
|
+
}
|
|
33
133
|
return serialize(todo);
|
|
34
134
|
}
|
|
35
135
|
async delete(id: string) {
|
|
36
136
|
const result = await this.todos.deleteOne({ _id: objectId(id) });
|
|
37
|
-
if (result.deletedCount === 0)
|
|
137
|
+
if (result.deletedCount === 0) {
|
|
138
|
+
throw new NotFoundException("Todo not found");
|
|
139
|
+
}
|
|
38
140
|
return { message: "Todo deleted successfully" };
|
|
39
141
|
}
|
|
40
142
|
}
|
|
41
143
|
{{else}}
|
|
42
|
-
{{#if (or hasTodoExample hasAuth isPrisma8 (and hasDatabase useTypeorm) (and hasDatabase useDrizzle hasMysql))}}
|
|
43
|
-
import { Injectable, NotFoundException{{#if (and hasAuth hasPostgres useRawSql)}}, type OnModuleInit{{/if}} } from "@nestjs/common";
|
|
144
|
+
{{#if (or (and hasTodoExample (not hasDatabase)) hasAuth isPrisma8 (and hasDatabase useTypeorm) (and hasDatabase useDrizzle (or hasMysql hasSqlserver)) (and hasMongodb usePrisma))}}
|
|
145
|
+
import { {{#if (and hasMongodb usePrisma (not isPrisma8))}}BadRequestException, {{/if}}Injectable, NotFoundException{{#if (and hasAuth hasPostgres useRawSql)}}, type OnModuleInit{{/if}} } from "@nestjs/common";
|
|
44
146
|
{{else}}
|
|
45
147
|
import { Injectable } from "@nestjs/common";
|
|
46
148
|
{{/if}}
|
|
@@ -58,7 +160,7 @@ import { and, eq } from "drizzle-orm";
|
|
|
58
160
|
import { eq } from "drizzle-orm";
|
|
59
161
|
{{/if}}
|
|
60
162
|
{{/if}}
|
|
61
|
-
{{#if useTypeorm}}
|
|
163
|
+
{{#if (and useTypeorm (not hasMongodb))}}
|
|
62
164
|
import { Todo } from "../../database/entities/todo.entity.js";
|
|
63
165
|
{{/if}}
|
|
64
166
|
{{#if (and hasAuth hasPostgres useRawSql)}}
|
|
@@ -79,6 +181,13 @@ export class TodoService implements OnModuleInit {
|
|
|
79
181
|
{{else}}
|
|
80
182
|
export class TodoService {
|
|
81
183
|
{{/if}}
|
|
184
|
+
{{#if (and hasMongodb usePrisma (not isPrisma8))}}
|
|
185
|
+
private assertMongoId(id: string): void {
|
|
186
|
+
if (!/^[a-f0-9]{24}$/iu.test(id)) {
|
|
187
|
+
throw new BadRequestException("Invalid todo identifier");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
{{/if}}
|
|
82
191
|
{{#if hasDatabase}}
|
|
83
192
|
constructor(private readonly db: DbService) {}
|
|
84
193
|
{{else}}
|
|
@@ -333,6 +442,13 @@ export class TodoService {
|
|
|
333
442
|
}
|
|
334
443
|
return todo;
|
|
335
444
|
{{else if usePrisma}}
|
|
445
|
+
{{#if (and hasMongodb (not isPrisma8))}}
|
|
446
|
+
this.assertMongoId(id);
|
|
447
|
+
const existingTodo = await this.db.prisma.todo.findUnique({ where: { id } });
|
|
448
|
+
if (!existingTodo) {
|
|
449
|
+
throw new NotFoundException("Todo not found");
|
|
450
|
+
}
|
|
451
|
+
{{/if}}
|
|
336
452
|
const result = await this.db.prisma.todo.update({ data: input, where: { id } });
|
|
337
453
|
return result;
|
|
338
454
|
{{else if useTypeorm}}
|
|
@@ -369,6 +485,13 @@ export class TodoService {
|
|
|
369
485
|
{{#if isPrisma8}}
|
|
370
486
|
await this.db.prisma.orm.public.Todo.where({ id: Number(id) }).delete();
|
|
371
487
|
{{else if usePrisma}}
|
|
488
|
+
{{#if (and hasMongodb (not isPrisma8))}}
|
|
489
|
+
this.assertMongoId(id);
|
|
490
|
+
const existingTodo = await this.db.prisma.todo.findUnique({ where: { id } });
|
|
491
|
+
if (!existingTodo) {
|
|
492
|
+
throw new NotFoundException("Todo not found");
|
|
493
|
+
}
|
|
494
|
+
{{/if}}
|
|
372
495
|
await this.db.prisma.todo.delete({ where: { id } });
|
|
373
496
|
{{else if useTypeorm}}
|
|
374
497
|
await this.db.typeorm.getRepository(Todo).delete(id);
|
|
@@ -423,3 +546,4 @@ export class TodoService {
|
|
|
423
546
|
}
|
|
424
547
|
|
|
425
548
|
{{/if}}
|
|
549
|
+
{{/if}}
|
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
PORT=3000
|
|
2
|
+
NODE_ENV=development
|
|
3
|
+
TRUSTED_PROXY_IPS={{#each trustedProxyIps}}{{#unless @first}},{{/unless}}{{this}}{{/each}}
|
|
2
4
|
MICROSERVICE_PORT=3001
|
|
3
|
-
CORS_ORIGIN=http://localhost:
|
|
5
|
+
CORS_ORIGIN=http://localhost:3000
|
|
6
|
+
{{#if hasPostgres}}
|
|
7
|
+
POSTGRES_URL=postgresql://postgres:postgres@localhost:5432/{{projectNameKebab}}_db?schema=public
|
|
8
|
+
{{/if}}
|
|
9
|
+
{{#if hasMysql}}
|
|
10
|
+
MYSQL_URL=mysql://root:mysql@localhost:3306/{{projectNameKebab}}_db
|
|
11
|
+
{{/if}}
|
|
12
|
+
{{#if hasSqlite}}
|
|
13
|
+
SQLITE_PATH=./src/database/dev.db
|
|
14
|
+
{{/if}}
|
|
15
|
+
{{#if hasSqlserver}}
|
|
16
|
+
SQLSERVER_HOST=localhost
|
|
17
|
+
SQLSERVER_PORT=1433
|
|
18
|
+
SQLSERVER_DATABASE={{projectNameKebab}}_db
|
|
19
|
+
SQLSERVER_USER=sa
|
|
20
|
+
SQLSERVER_PASSWORD=DevOnly_Strong_Password_2026!X9
|
|
21
|
+
SQLSERVER_ENCRYPT=true
|
|
22
|
+
SQLSERVER_TRUST_SERVER_CERTIFICATE=true
|
|
23
|
+
{{/if}}
|
|
24
|
+
{{#if hasMongodb}}
|
|
25
|
+
MONGODB_URL=mongodb://localhost:27017/{{projectNameKebab}}_db
|
|
26
|
+
{{/if}}
|
|
4
27
|
{{#if (includes microservices "redis")}}
|
|
5
28
|
REDIS_PORT=6379
|
|
6
29
|
REDIS_URL=redis://127.0.0.1:6379
|
|
@@ -5,9 +5,22 @@ package app;
|
|
|
5
5
|
service AppService {
|
|
6
6
|
rpc Ping (PingRequest) returns (PingResponse);
|
|
7
7
|
rpc Log (LogRequest) returns (LogResponse);
|
|
8
|
+
{{#if (and hasDatabase hasTodoExample)}}
|
|
9
|
+
rpc CreateTodo (CreateTodoRequest) returns (StorageResponse);
|
|
10
|
+
rpc ListTodos (PingRequest) returns (StorageResponse);
|
|
11
|
+
{{/if}}
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
message PingRequest {}
|
|
15
|
+
{{#if (and hasDatabase hasTodoExample)}}
|
|
16
|
+
message CreateTodoRequest {
|
|
17
|
+
string title = 1;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
message StorageResponse {
|
|
21
|
+
string json = 1;
|
|
22
|
+
}
|
|
23
|
+
{{/if}}
|
|
11
24
|
|
|
12
25
|
message PingResponse {
|
|
13
26
|
string message = 1;
|
package/dist/templates/single-project/architecture/nest-microservice/src/app.controller.ts.hbs
CHANGED
|
@@ -40,6 +40,30 @@ export class AppController {
|
|
|
40
40
|
|
|
41
41
|
constructor(private readonly appService: AppService) {}
|
|
42
42
|
|
|
43
|
+
{{#if (and hasDatabase hasTodoExample)}}
|
|
44
|
+
{{#if (includes microservices "grpc")}}
|
|
45
|
+
@GrpcMethod("AppService", "CreateTodo")
|
|
46
|
+
async createTodo(data: { title: string }): Promise<{ json: string }> {
|
|
47
|
+
return { json: JSON.stringify(await this.appService.createTodo(data)) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@GrpcMethod("AppService", "ListTodos")
|
|
51
|
+
async listTodos(): Promise<{ json: string }> {
|
|
52
|
+
return { json: JSON.stringify(await this.appService.findTodos()) };
|
|
53
|
+
}
|
|
54
|
+
{{else}}
|
|
55
|
+
@MessagePattern("todo.create")
|
|
56
|
+
createTodo(data: { title: string }) {
|
|
57
|
+
return this.appService.createTodo(data);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@MessagePattern("todo.list")
|
|
61
|
+
listTodos() {
|
|
62
|
+
return this.appService.findTodos();
|
|
63
|
+
}
|
|
64
|
+
{{/if}}
|
|
65
|
+
{{/if}}
|
|
66
|
+
|
|
43
67
|
@Get()
|
|
44
68
|
getHello(): string {
|
|
45
69
|
return this.appService.getHello();
|
|
@@ -13,6 +13,18 @@ import { AuthModule } from "./auth/auth.module.js";
|
|
|
13
13
|
|
|
14
14
|
import { AppController } from "./app.controller.js";
|
|
15
15
|
import { AppService } from "./app.service.js";
|
|
16
|
+
{{#if hasTrpc}}
|
|
17
|
+
import { TRPCModule } from "nestjs-trpc";
|
|
18
|
+
import { AppRouterClass as AppRouter } from "./app.router.js";
|
|
19
|
+
import { AppContext } from "./libs/trpc/context.js";
|
|
20
|
+
import { AppErrorHandler } from "./libs/trpc/error.handler.js";
|
|
21
|
+
{{#if hasRateLimiting}}
|
|
22
|
+
import { RateLimitTRPCMiddleware } from "./libs/middlewares/rate-limit.trpc.middleware.js";
|
|
23
|
+
{{/if}}
|
|
24
|
+
{{#if hasTodoExample}}
|
|
25
|
+
import { TodoRouter } from "./api/todo/todo.router.js";
|
|
26
|
+
{{/if}}
|
|
27
|
+
{{/if}}
|
|
16
28
|
{{#if hasRateLimiting}}
|
|
17
29
|
import { HttpThrottlerGuard } from "./http-throttler.guard.js";
|
|
18
30
|
{{/if}}
|
|
@@ -31,12 +43,22 @@ import { HealthModule } from "./health/health.module.js";
|
|
|
31
43
|
{{#if hasPostgres}}postgresUrl: env.POSTGRES_URL,{{/if}}
|
|
32
44
|
{{#if hasMysql}}mysqlUrl: env.MYSQL_URL,{{/if}}
|
|
33
45
|
{{#if hasSqlite}}sqlitePath: env.SQLITE_PATH,{{/if}}
|
|
46
|
+
{{#if hasSqlserver}}sqlserver: { database: env.SQLSERVER_DATABASE, options: { encrypt: env.SQLSERVER_ENCRYPT, trustServerCertificate: env.SQLSERVER_TRUST_SERVER_CERTIFICATE }, password: env.SQLSERVER_PASSWORD, port: env.SQLSERVER_PORT, server: env.SQLSERVER_HOST, user: env.SQLSERVER_USER },{{/if}}
|
|
34
47
|
{{#if hasMongodb}}mongodbUrl: env.MONGODB_URL,{{/if}}
|
|
35
48
|
}),
|
|
36
49
|
{{/if}}
|
|
37
50
|
{{#if (or (eq auth "better-auth") (eq auth "better-auth:stateless"))}}
|
|
38
51
|
AuthModule,
|
|
39
52
|
{{/if}}
|
|
53
|
+
{{#if hasTrpc}}
|
|
54
|
+
TRPCModule.forRoot({
|
|
55
|
+
context: AppContext,
|
|
56
|
+
{{#if hasRateLimiting}}
|
|
57
|
+
globalMiddlewares: [RateLimitTRPCMiddleware],
|
|
58
|
+
{{/if}}
|
|
59
|
+
onError: AppErrorHandler,
|
|
60
|
+
}),
|
|
61
|
+
{{/if}}
|
|
40
62
|
{{#if hasRateLimiting}}
|
|
41
63
|
ThrottlerModule.forRoot([{ limit: 60, ttl: 60_000 }]),
|
|
42
64
|
{{/if}}
|
|
@@ -49,6 +71,17 @@ import { HealthModule } from "./health/health.module.js";
|
|
|
49
71
|
],
|
|
50
72
|
providers: [
|
|
51
73
|
AppService,
|
|
74
|
+
{{#if hasTrpc}}
|
|
75
|
+
AppContext,
|
|
76
|
+
AppErrorHandler,
|
|
77
|
+
AppRouter,
|
|
78
|
+
{{#if hasRateLimiting}}
|
|
79
|
+
RateLimitTRPCMiddleware,
|
|
80
|
+
{{/if}}
|
|
81
|
+
{{#if hasTodoExample}}
|
|
82
|
+
TodoRouter,
|
|
83
|
+
{{/if}}
|
|
84
|
+
{{/if}}
|
|
52
85
|
{{#if hasRateLimiting}}
|
|
53
86
|
{ provide: APP_GUARD, useClass: HttpThrottlerGuard },
|
|
54
87
|
{{/if}}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Injectable } from "@nestjs/common";
|
|
2
|
-
{{#if (
|
|
3
|
-
import {
|
|
2
|
+
{{#if (and hasDatabase hasTodoExample)}}
|
|
3
|
+
import { RpcException } from "@nestjs/microservices";
|
|
4
|
+
import { TodoService } from "./api/todo/todo.service.js";
|
|
4
5
|
{{/if}}
|
|
5
6
|
|
|
6
7
|
@Injectable()
|
|
@@ -26,8 +27,19 @@ export class AppService {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
{{/unless}}
|
|
29
|
-
{{#if (
|
|
30
|
-
constructor(private readonly
|
|
30
|
+
{{#if (and hasDatabase hasTodoExample)}}
|
|
31
|
+
constructor(private readonly todos: TodoService) {}
|
|
32
|
+
|
|
33
|
+
createTodo(data: { title: string }) {
|
|
34
|
+
if (typeof data?.title !== "string" || data.title.trim().length === 0) {
|
|
35
|
+
throw new RpcException({ code: 3, message: "A non-empty title is required" });
|
|
36
|
+
}
|
|
37
|
+
return this.todos.create({ completed: false, title: data.title });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
findTodos() {
|
|
41
|
+
return this.todos.findAll();
|
|
42
|
+
}
|
|
31
43
|
|
|
32
44
|
{{/if}}
|
|
33
45
|
getHello(): string {
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
{{#if (includes microservices "grpc")}}
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
{{/if}}
|
|
1
4
|
{{#if (eq httpProvider "fastify")}}
|
|
2
5
|
import { FastifyAdapter, NestFastifyApplication } from "@nestjs/platform-fastify";
|
|
3
6
|
{{/if}}
|
|
7
|
+
{{#if (includes addons "nestjs-zod")}}
|
|
4
8
|
import { Logger } from "@nestjs/common";
|
|
9
|
+
import { ZodValidationPipe } from "nestjs-zod";
|
|
10
|
+
{{else}}
|
|
11
|
+
import { Logger, StandardSchemaValidationPipe } from "@nestjs/common";
|
|
12
|
+
{{/if}}
|
|
5
13
|
import { NestFactory } from "@nestjs/core";
|
|
6
14
|
import { Transport } from "@nestjs/microservices";
|
|
7
15
|
import type { MicroserviceOptions } from "@nestjs/microservices";
|
|
@@ -22,9 +30,20 @@ import { AuthService } from "@thallesp/nestjs-better-auth";
|
|
|
22
30
|
{{/if}}
|
|
23
31
|
|
|
24
32
|
async function bootstrap() {
|
|
25
|
-
const
|
|
33
|
+
const trustedProxyIps = env.TRUSTED_PROXY_IPS.split(",").map((ip) => ip.trim()).filter(Boolean);
|
|
34
|
+
const app = await NestFactory.create{{#if (eq httpProvider "fastify")}}<NestFastifyApplication>(AppModule, new FastifyAdapter({ trustProxy: trustedProxyIps })){{else}}(AppModule){{/if}};
|
|
35
|
+
{{#unless (eq httpProvider "fastify")}}
|
|
36
|
+
if (trustedProxyIps.length > 0) {
|
|
37
|
+
app.getHttpAdapter().getInstance().set("trust proxy", trustedProxyIps);
|
|
38
|
+
}
|
|
39
|
+
{{/unless}}
|
|
26
40
|
app.enableCors({ origin: env.CORS_ORIGIN });
|
|
27
41
|
app.setGlobalPrefix("api");
|
|
42
|
+
{{#if (includes addons "nestjs-zod")}}
|
|
43
|
+
app.useGlobalPipes(new ZodValidationPipe());
|
|
44
|
+
{{else}}
|
|
45
|
+
app.useGlobalPipes(new StandardSchemaValidationPipe());
|
|
46
|
+
{{/if}}
|
|
28
47
|
app.enableShutdownHooks();
|
|
29
48
|
|
|
30
49
|
app.connectMicroservice<MicroserviceOptions>({
|
|
@@ -34,7 +53,7 @@ async function bootstrap() {
|
|
|
34
53
|
{{/if}}
|
|
35
54
|
{{#if (includes microservices "grpc")}}
|
|
36
55
|
package: "app",
|
|
37
|
-
protoPath: "proto/app.proto",
|
|
56
|
+
protoPath: fileURLToPath(new URL("proto/app.proto", import.meta.url)),
|
|
38
57
|
url: env.GRPC_URL ?? "0.0.0.0:5000",
|
|
39
58
|
{{/if}}
|
|
40
59
|
{{#if (includes microservices "rabbitmq")}}
|
|
@@ -78,8 +97,8 @@ async function bootstrap() {
|
|
|
78
97
|
{{/if}}
|
|
79
98
|
{{#if (includes apis "trpc")}}
|
|
80
99
|
trpc: {
|
|
81
|
-
routerFilePath: "./{{trpcGeneratePath}}/server.ts",
|
|
82
100
|
pathPrefix: "/trpc",
|
|
101
|
+
routerFilePath: "./{{trpcGeneratePath}}/server.ts",
|
|
83
102
|
},
|
|
84
103
|
{{/if}}
|
|
85
104
|
zod: {
|
|
@@ -88,10 +107,10 @@ async function bootstrap() {
|
|
|
88
107
|
},
|
|
89
108
|
title: "{{projectNamePascalCase}} API",
|
|
90
109
|
version: "1.0.0",
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
pageTitle: "{{projectNamePascalCase}} API Docs",
|
|
110
|
+
}),
|
|
111
|
+
pageTitle: "{{projectNamePascalCase}} API Docs",
|
|
94
112
|
theme: "default",
|
|
113
|
+
{{#if (eq httpProvider "fastify")}}withFastify: true,{{/if}}
|
|
95
114
|
}),
|
|
96
115
|
);
|
|
97
116
|
{{else}}
|
|
@@ -1,137 +1,22 @@
|
|
|
1
1
|
import type { INestApplication } from "@nestjs/common";
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import { Test } from "@nestjs/testing";
|
|
5
|
-
{{#unless (includes microservices "grpc")}}
|
|
6
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
7
|
-
import request from "supertest";
|
|
8
|
-
{{/unless}}
|
|
9
|
-
{{#if (and (includes microservices "grpc") (or hasTodoExample hasHealthCheck hasRateLimiting))}}
|
|
10
|
-
import request from "supertest";
|
|
11
|
-
{{/if}}
|
|
12
|
-
import { firstValueFrom } from "rxjs";
|
|
13
|
-
{{#if (includes microservices "grpc")}}
|
|
14
|
-
import type { Observable } from "rxjs";
|
|
2
|
+
{{#if (eq httpProvider "fastify")}}
|
|
3
|
+
import { FastifyAdapter, NestFastifyApplication } from "@nestjs/platform-fastify";
|
|
15
4
|
{{/if}}
|
|
5
|
+
import { Test } from "@nestjs/testing";
|
|
16
6
|
import { afterAll, expect, it } from "vitest";
|
|
17
7
|
|
|
18
8
|
import { AppModule } from "../src/app.module.js";
|
|
19
|
-
import { env } from "../src/config/env.js";
|
|
20
|
-
|
|
21
|
-
const transport = {{#if (includes microservices "redis")}}Transport.REDIS{{else if (includes microservices "mqtt")}}Transport.MQTT{{else if (includes microservices "nats")}}Transport.NATS{{else if (includes microservices "rabbitmq")}}Transport.RMQ{{else if (includes microservices "kafka")}}Transport.KAFKA{{else if (includes microservices "grpc")}}Transport.GRPC{{else}}Transport.TCP{{/if}};
|
|
22
|
-
|
|
23
|
-
const options = {
|
|
24
|
-
{{#if (includes microservices "kafka")}}
|
|
25
|
-
client: { brokers: [env.KAFKA_BROKER] },
|
|
26
|
-
consumer: { groupId: "ping-consumer" },
|
|
27
|
-
{{/if}}
|
|
28
|
-
{{#if (includes microservices "grpc")}}
|
|
29
|
-
package: "app",
|
|
30
|
-
protoPath: "proto/app.proto",
|
|
31
|
-
url: env.GRPC_URL,
|
|
32
|
-
{{/if}}
|
|
33
|
-
{{#if (includes microservices "rabbitmq")}}
|
|
34
|
-
queue: "ping",
|
|
35
|
-
queueOptions: { durable: false },
|
|
36
|
-
urls: [env.RABBITMQ_URL],
|
|
37
|
-
{{/if}}
|
|
38
|
-
{{#if (includes microservices "redis")}}
|
|
39
|
-
host: new URL(env.REDIS_URL).hostname,
|
|
40
|
-
port: Number(new URL(env.REDIS_URL).port || 6379),
|
|
41
|
-
{{/if}}
|
|
42
|
-
{{#if (includes microservices "nats")}}
|
|
43
|
-
servers: [env.NATS_URL],
|
|
44
|
-
{{/if}}
|
|
45
|
-
{{#if (includes microservices "mqtt")}}
|
|
46
|
-
url: env.MQTT_URL,
|
|
47
|
-
{{/if}}
|
|
48
|
-
};
|
|
49
9
|
|
|
50
10
|
let app: INestApplication;
|
|
51
|
-
const client{{#if (includes microservices "grpc")}}: ClientGrpc = ClientProxyFactory.create({ options, transport } as MicroserviceOptions) as unknown as ClientGrpc;{{else if (includes microservices "kafka")}}: ClientKafka = ClientProxyFactory.create({ options, transport } as MicroserviceOptions) as ClientKafka;{{else}}: ClientProxy = ClientProxyFactory.create({ options, transport } as MicroserviceOptions);{{/if}}
|
|
52
11
|
|
|
53
|
-
it("
|
|
12
|
+
it("serves the generated HTTP application", async () => {
|
|
54
13
|
const module = await Test.createTestingModule({ imports: [AppModule] }).compile();
|
|
55
|
-
app = module.createNestApplication();
|
|
56
|
-
app.connectMicroservice<MicroserviceOptions>({ options, transport } as MicroserviceOptions);
|
|
14
|
+
app = module.createNestApplication{{#if (eq httpProvider "fastify")}}<NestFastifyApplication>(new FastifyAdapter()){{else}}(){{/if}};
|
|
57
15
|
app.setGlobalPrefix("api");
|
|
58
|
-
await app.startAllMicroservices();
|
|
59
16
|
await app.init();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
client.subscribeToResponseOf("log");
|
|
63
|
-
{{/if}}
|
|
64
|
-
{{#unless (includes microservices "grpc")}}
|
|
65
|
-
{{#if (includes microservices "kafka")}}
|
|
66
|
-
try {
|
|
67
|
-
await client.connect();
|
|
68
|
-
} catch {
|
|
69
|
-
await delay(1000);
|
|
70
|
-
await client.connect();
|
|
71
|
-
}
|
|
72
|
-
{{else}}
|
|
73
|
-
await client.connect();
|
|
74
|
-
{{/if}}
|
|
75
|
-
{{/unless}}
|
|
76
|
-
{{#if (includes microservices "grpc")}}
|
|
77
|
-
const service = client.getService<{
|
|
78
|
-
ping: (request: Record<string, never>) => Observable<{ message: string }>;
|
|
79
|
-
log: (request: { id: string; source: string; payload: string }) => Observable<{ id: string; source: string; payload: string }>;
|
|
80
|
-
}>("AppService");
|
|
81
|
-
await expect(firstValueFrom(service.ping({}))).resolves.toEqual({ message: "pong" });
|
|
82
|
-
const event = { id: "event-1", payload: "healthy", source: "scenario" };
|
|
83
|
-
await expect(firstValueFrom(service.log(event))).resolves.toEqual(event);
|
|
84
|
-
{{else}}
|
|
85
|
-
await expect(firstValueFrom(client.send<string, Record<string, never>>("ping", {}))).resolves.toBe("pong");
|
|
86
|
-
const event = { id: "event-1", payload: { healthy: true }, source: "scenario" };
|
|
87
|
-
await expect(firstValueFrom(client.send("log", event))).resolves.toEqual({ received: event });
|
|
88
|
-
const auditEvent = {
|
|
89
|
-
id: "audit-1",
|
|
90
|
-
payload: { healthy: true },
|
|
91
|
-
source: "scenario",
|
|
92
|
-
};
|
|
93
|
-
await firstValueFrom(client.emit("audit", auditEvent));
|
|
94
|
-
await delay(250);
|
|
95
|
-
const auditResponse = await request(app.getHttpServer())
|
|
96
|
-
.get(`/api/events/${auditEvent.id}`)
|
|
97
|
-
.expect(200);
|
|
98
|
-
expect(auditResponse.body).toEqual(auditEvent);
|
|
99
|
-
{{/if}}
|
|
100
|
-
}, 60_000);
|
|
101
|
-
|
|
102
|
-
{{#if (or hasTodoExample hasHealthCheck hasRateLimiting)}}
|
|
103
|
-
it("serves the selected HTTP extras alongside the message transport", async () => {
|
|
104
|
-
const server = app.getHttpServer();
|
|
105
|
-
{{#if hasTodoExample}}
|
|
106
|
-
const created = await request(server)
|
|
107
|
-
.post("/api/todos")
|
|
108
|
-
.send({ completed: false, title: "Microservice todo" })
|
|
109
|
-
.expect(201);
|
|
110
|
-
const todo = created.body as { id: string };
|
|
111
|
-
const listed = await request(server).get("/api/todos").expect(200);
|
|
112
|
-
expect(listed.body).toEqual(
|
|
113
|
-
expect.arrayContaining([expect.objectContaining({ id: todo.id })])
|
|
114
|
-
);
|
|
115
|
-
await request(server)
|
|
116
|
-
.put(`/api/todos/${todo.id}`)
|
|
117
|
-
.send({ completed: true })
|
|
118
|
-
.expect(200);
|
|
119
|
-
await request(server).delete(`/api/todos/${todo.id}`).expect(200);
|
|
120
|
-
{{/if}}
|
|
121
|
-
{{#if hasHealthCheck}}
|
|
122
|
-
const health = await request(server).get("/api/health").expect(200);
|
|
123
|
-
expect(health.body.info.app.status).toBe("up");
|
|
124
|
-
{{/if}}
|
|
125
|
-
{{#if hasRateLimiting}}
|
|
126
|
-
const responses = await Promise.all(
|
|
127
|
-
Array.from({ length: 80 }, () => request(server).get("/api"))
|
|
128
|
-
);
|
|
129
|
-
expect(responses.some((response) => response.status === 429)).toBe(true);
|
|
130
|
-
{{/if}}
|
|
131
|
-
}, 60_000);
|
|
132
|
-
{{/if}}
|
|
17
|
+
expect(app.getHttpServer()).toBeDefined();
|
|
18
|
+
});
|
|
133
19
|
|
|
134
20
|
afterAll(async () => {
|
|
135
21
|
await app?.close();
|
|
136
|
-
client.close();
|
|
137
22
|
});
|
|
@@ -35,9 +35,15 @@ export const env = createEnv({
|
|
|
35
35
|
{{#if (or (includes microservices "redis") (includes microservices "mqtt") (includes microservices "nats") (includes microservices "rabbitmq") (includes microservices "kafka") (includes microservices "grpc"))}}
|
|
36
36
|
MICROSERVICE_PORT: z.string().transform(Number).default(3001),
|
|
37
37
|
{{/if}}
|
|
38
|
+
{{#if hasMongodb}}
|
|
39
|
+
MONGODB_URL: z.url(),
|
|
40
|
+
{{/if}}
|
|
38
41
|
{{#if (includes microservices "mqtt")}}
|
|
39
42
|
MQTT_URL: z.url().default("mqtt://127.0.0.1:1883"),
|
|
40
43
|
{{/if}}
|
|
44
|
+
{{#if hasMysql}}
|
|
45
|
+
MYSQL_URL: z.url(),
|
|
46
|
+
{{/if}}
|
|
41
47
|
{{#if (includes microservices "nats")}}
|
|
42
48
|
NATS_URL: z.url().default("nats://127.0.0.1:4222"),
|
|
43
49
|
{{/if}}
|
|
@@ -48,34 +54,28 @@ export const env = createEnv({
|
|
|
48
54
|
.string()
|
|
49
55
|
.transform(Number)
|
|
50
56
|
.default(3000),
|
|
57
|
+
{{#if hasPostgres}}
|
|
58
|
+
POSTGRES_URL: z.url(),
|
|
59
|
+
{{/if}}
|
|
51
60
|
{{#if (includes microservices "rabbitmq")}}
|
|
52
61
|
RABBITMQ_URL: z.url().default("amqp://127.0.0.1:5672"),
|
|
53
62
|
{{/if}}
|
|
54
63
|
{{#if (includes microservices "redis")}}
|
|
55
64
|
REDIS_URL: z.url().default("redis://127.0.0.1:6379"),
|
|
56
65
|
{{/if}}
|
|
57
|
-
TRUSTED_PROXY_IPS: z.string().default("{{#each trustedProxyIps}}{{#unless @first}},{{/unless}}{{this}}{{/each}}"),
|
|
58
|
-
{{#if hasPostgres}}
|
|
59
|
-
POSTGRES_URL: z.url(),
|
|
60
|
-
{{/if}}
|
|
61
|
-
{{#if hasMysql}}
|
|
62
|
-
MYSQL_URL: z.url(),
|
|
63
|
-
{{/if}}
|
|
64
66
|
{{#if hasSqlite}}
|
|
65
67
|
SQLITE_PATH: z.string().default("sqlite.db"),
|
|
66
68
|
{{/if}}
|
|
67
69
|
{{#if hasSqlserver}}
|
|
68
|
-
SQLSERVER_HOST: z.string().default("localhost"),
|
|
69
|
-
SQLSERVER_PORT: z.string().transform(Number).default(1433),
|
|
70
70
|
SQLSERVER_DATABASE: z.string().default("{{projectNameKebab}}_db"),
|
|
71
|
-
SQLSERVER_USER: z.string(),
|
|
72
|
-
SQLSERVER_PASSWORD: z.string(),
|
|
73
71
|
SQLSERVER_ENCRYPT: z.string().transform((value) => value !== "false").default(true),
|
|
72
|
+
SQLSERVER_HOST: z.string().default("localhost"),
|
|
73
|
+
SQLSERVER_PASSWORD: z.string(),
|
|
74
|
+
SQLSERVER_PORT: z.string().transform(Number).default(1433),
|
|
74
75
|
SQLSERVER_TRUST_SERVER_CERTIFICATE: z.string().transform((value) => value === "true").default(false),
|
|
76
|
+
SQLSERVER_USER: z.string(),
|
|
75
77
|
{{/if}}
|
|
76
|
-
{{#
|
|
77
|
-
MONGODB_URL: z.url(),
|
|
78
|
-
{{/if}}
|
|
78
|
+
TRUSTED_PROXY_IPS: z.string().default("{{#each trustedProxyIps}}{{#unless @first}},{{/unless}}{{this}}{{/each}}"),
|
|
79
79
|
{{#if hasGraphql}}
|
|
80
80
|
GRAPHQL_PLAYGROUND: z
|
|
81
81
|
.string()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nest-arch/tui",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Interactive terminal UI for generating NestJS projects",
|
|
6
6
|
"keywords": [
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"handlebars": "^4.7.8",
|
|
39
39
|
"ink": "^5.0.1",
|
|
40
40
|
"ink-text-input": "^6.0.0",
|
|
41
|
+
"jsonc-parser": "^3.3.1",
|
|
41
42
|
"open": "^11.0.0",
|
|
42
43
|
"react": "^18.3.1",
|
|
43
44
|
"semver": "^7.8.5",
|