@nest-arch/tui 0.4.4 → 0.4.5
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-BbC7KTI7.js} +1 -1
- package/dist/index.js +137 -123
- package/dist/templates/base/src/database/adapters/interfaces/db-adapter.interface.ts.hbs +3 -3
- package/dist/templates/base/src/database/adapters/mongo/prisma-mongo.adapter.ts.hbs +4 -4
- package/dist/templates/base/src/database/index.ts.hbs +12 -3
- package/dist/templates/base/test/app.e2e-spec.ts.hbs +44 -11
- 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/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/test/app.e2e-spec.ts.hbs +4 -1
- 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 +27 -9
- 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 +18 -16
- package/package.json +1 -1
|
@@ -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
|
|
|
@@ -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}}
|
|
@@ -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}}
|
|
@@ -319,8 +319,8 @@ describe("AppController (e2e)", () => {
|
|
|
319
319
|
{{#if (and hasHealthCheck hasDatabase (or usePrisma useTypeorm useDrizzle))}}
|
|
320
320
|
it("reports database readiness failure with HTTP 503", async () => {
|
|
321
321
|
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}})
|
|
322
|
+
const database = {{#if hasMongodb}}db.raw.mongo{{else if usePrisma}}db.prisma{{else if useTypeorm}}db.typeorm{{else}}db.drizzle{{/if}};
|
|
323
|
+
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
324
|
.mockRejectedValue(new Error("scenario database outage"));
|
|
325
325
|
try {
|
|
326
326
|
const response = await request(app.getHttpServer()).get("/health").expect(503);
|
|
@@ -344,7 +344,6 @@ describe("AppController (e2e)", () => {
|
|
|
344
344
|
it("enforces todo ownership through authenticated tRPC procedures", async () => {
|
|
345
345
|
const server = app.getHttpServer();
|
|
346
346
|
const password = "Scenario-password-123!";
|
|
347
|
-
const origin = "http://localhost:3000";
|
|
348
347
|
const call = (procedure: string, input?: unknown, credential?: { cookie?: string[]; token?: string }) => {
|
|
349
348
|
const operation = procedure === "findAll"
|
|
350
349
|
? request(server).get("/trpc/todoRouter.findAll")
|
|
@@ -354,11 +353,10 @@ describe("AppController (e2e)", () => {
|
|
|
354
353
|
if (input !== undefined) operation.send(input);
|
|
355
354
|
return operation;
|
|
356
355
|
};
|
|
357
|
-
const unwrap = (body: { result?: { data?: { json?: unknown } } }) =>
|
|
358
|
-
body.result?.data?.json ?? body.result?.data;
|
|
359
356
|
{{#if isBetterAuth}}
|
|
360
357
|
async function register(prefix: string) {
|
|
361
358
|
const email = `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
|
|
359
|
+
const origin = "http://localhost:3000";
|
|
362
360
|
const response = await request(server).post("/api/auth/sign-up/email")
|
|
363
361
|
.set("Origin", origin).send({ email, password, name: prefix }).expect(200);
|
|
364
362
|
return { cookie: response.headers["set-cookie"] as string[] };
|
|
@@ -379,21 +377,50 @@ describe("AppController (e2e)", () => {
|
|
|
379
377
|
const alice = await register("trpc-alice");
|
|
380
378
|
const bob = await register("trpc-bob");
|
|
381
379
|
const created = await call("create", { title: "Private tRPC todo", completed: false }, alice).expect(200);
|
|
382
|
-
const createdTodo =
|
|
380
|
+
const createdTodo = (created.body.result?.data?.json ?? created.body.result?.data) as { id: string; title: string };
|
|
383
381
|
expect(createdTodo.title).toBe("Private tRPC todo");
|
|
384
382
|
const id = createdTodo.id;
|
|
385
|
-
const
|
|
383
|
+
const aliceResponse = await call("findAll", undefined, alice).expect(200);
|
|
384
|
+
const aliceTodos = aliceResponse.body.result?.data?.json ?? aliceResponse.body.result?.data as { id: string }[];
|
|
386
385
|
expect(aliceTodos).toEqual(expect.arrayContaining([expect.objectContaining({ id })]));
|
|
387
|
-
const
|
|
386
|
+
const bobResponse = await call("findAll", undefined, bob).expect(200);
|
|
387
|
+
const bobTodos = bobResponse.body.result?.data?.json ?? bobResponse.body.result?.data as { id: string }[];
|
|
388
388
|
expect(bobTodos).not.toEqual(expect.arrayContaining([expect.objectContaining({ id })]));
|
|
389
389
|
await call("update", { id, data: { completed: true } }, bob).expect(404);
|
|
390
390
|
await call("delete", { id }, bob).expect(404);
|
|
391
391
|
const updated = await call("update", { id, data: { completed: true } }, alice).expect(200);
|
|
392
|
-
|
|
392
|
+
const updatedTodo = updated.body.result?.data?.json ?? updated.body.result?.data;
|
|
393
|
+
expect((updatedTodo as { completed: boolean }).completed).toBe(true);
|
|
393
394
|
await call("delete", { id }, alice).expect(200);
|
|
394
395
|
});
|
|
395
396
|
{{/if}}
|
|
396
397
|
|
|
398
|
+
{{#if (and hasTrpc hasTodoExample (not hasAuth))}}
|
|
399
|
+
it("creates, reads, updates, and deletes tRPC todos", async () => {
|
|
400
|
+
const server = app.getHttpServer();
|
|
401
|
+
const create = await request(server)
|
|
402
|
+
.post("/trpc/todoRouter.create")
|
|
403
|
+
.send({ title: "Public tRPC todo", completed: false })
|
|
404
|
+
.expect(200);
|
|
405
|
+
const created = create.body.result?.data?.json ?? create.body.result?.data;
|
|
406
|
+
expect(created.title).toBe("Public tRPC todo");
|
|
407
|
+
const id = created.id as string | number;
|
|
408
|
+
|
|
409
|
+
const list = await request(server).get("/trpc/todoRouter.findAll").expect(200);
|
|
410
|
+
const todos = list.body.result?.data?.json ?? list.body.result?.data;
|
|
411
|
+
expect(todos).toEqual(expect.arrayContaining([expect.objectContaining({ id })]));
|
|
412
|
+
|
|
413
|
+
const update = await request(server)
|
|
414
|
+
.post("/trpc/todoRouter.update")
|
|
415
|
+
.send({ id, data: { completed: true } })
|
|
416
|
+
.expect(200);
|
|
417
|
+
const updated = update.body.result?.data?.json ?? update.body.result?.data;
|
|
418
|
+
expect(updated.completed).toBe(true);
|
|
419
|
+
|
|
420
|
+
await request(server).post("/trpc/todoRouter.delete").send({ id }).expect(200);
|
|
421
|
+
});
|
|
422
|
+
{{/if}}
|
|
423
|
+
|
|
397
424
|
{{#if (eq auth "better-auth")}}
|
|
398
425
|
it("registers, authenticates and revokes a persisted session", async () => {
|
|
399
426
|
const email = `scenario-${Date.now()}@example.com`;
|
|
@@ -634,8 +661,14 @@ expect(revoked.body).toBeNull();
|
|
|
634
661
|
it("throttles repeated tRPC procedure requests", async () => {
|
|
635
662
|
const server = app.getHttpServer();
|
|
636
663
|
server.setMaxListeners(100);
|
|
637
|
-
const procedure = () => request(server).get(
|
|
664
|
+
const procedure = () => request(server).get(
|
|
665
|
+
"/trpc/appRouter.{{#if hasTrpcAndAuth}}testAuth{{else}}getHello{{/if}}"
|
|
666
|
+
);
|
|
667
|
+
{{#if hasTrpcAndAuth}}
|
|
638
668
|
expect((await procedure()).status).toBe(401);
|
|
669
|
+
{{else}}
|
|
670
|
+
expect((await procedure()).status).toBe(200);
|
|
671
|
+
{{/if}}
|
|
639
672
|
const responses = await Promise.all(
|
|
640
673
|
Array.from({ length: 80 }, () => procedure())
|
|
641
674
|
);
|
|
@@ -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
|
}
|
|
@@ -4,7 +4,10 @@ import { Test, TestingModule } from "@nestjs/testing";
|
|
|
4
4
|
import { FastifyAdapter } from "@nestjs/platform-fastify";
|
|
5
5
|
{{/if}}
|
|
6
6
|
import request from "supertest";
|
|
7
|
-
import { afterEach, beforeEach, describe,
|
|
7
|
+
import { afterEach, beforeEach, describe, it } from "vitest";
|
|
8
|
+
{{#if (eq auth "better-auth")}}
|
|
9
|
+
import { expect } from "vitest";
|
|
10
|
+
{{/if}}
|
|
8
11
|
import type { App } from "supertest/types";
|
|
9
12
|
|
|
10
13
|
import { AppModule } from "./../src/app.module.js";
|
package/dist/templates/single-project/architecture/nest-microservice/src/app.controller.ts.hbs
CHANGED
|
@@ -1,10 +1,43 @@
|
|
|
1
|
+
{{#if (includes microservices "grpc")}}
|
|
1
2
|
import { Controller, Get } from "@nestjs/common";
|
|
2
|
-
import {
|
|
3
|
+
import { GrpcMethod } from "@nestjs/microservices";
|
|
4
|
+
{{else}}
|
|
5
|
+
import { Controller, Get, Logger, Param } from "@nestjs/common";
|
|
6
|
+
import { EventPattern, MessagePattern } from "@nestjs/microservices";
|
|
7
|
+
{{/if}}
|
|
3
8
|
|
|
4
9
|
import { AppService } from "./app.service.js";
|
|
5
10
|
|
|
11
|
+
{{#unless (includes microservices "grpc")}}
|
|
12
|
+
type AuditEvent = {
|
|
13
|
+
id: string;
|
|
14
|
+
payload: Record<string, unknown>;
|
|
15
|
+
source: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const isAuditEvent = (value: unknown): value is AuditEvent => {
|
|
19
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const candidate = value as Record<string, unknown>;
|
|
23
|
+
return (
|
|
24
|
+
typeof candidate.id === "string" &&
|
|
25
|
+
candidate.id.trim().length > 0 &&
|
|
26
|
+
typeof candidate.source === "string" &&
|
|
27
|
+
candidate.source.trim().length > 0 &&
|
|
28
|
+
typeof candidate.payload === "object" &&
|
|
29
|
+
candidate.payload !== null &&
|
|
30
|
+
!Array.isArray(candidate.payload)
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
{{/unless}}
|
|
34
|
+
|
|
6
35
|
@Controller()
|
|
7
36
|
export class AppController {
|
|
37
|
+
{{#unless (includes microservices "grpc")}}
|
|
38
|
+
private readonly logger = new Logger(AppController.name);
|
|
39
|
+
{{/unless}}
|
|
40
|
+
|
|
8
41
|
constructor(private readonly appService: AppService) {}
|
|
9
42
|
|
|
10
43
|
@Get()
|
|
@@ -12,6 +45,24 @@ export class AppController {
|
|
|
12
45
|
return this.appService.getHello();
|
|
13
46
|
}
|
|
14
47
|
|
|
48
|
+
{{#unless (includes microservices "grpc")}}
|
|
49
|
+
@Get("events/:id")
|
|
50
|
+
getAuditEvent(
|
|
51
|
+
@Param("id") id: string
|
|
52
|
+
): { id: string; payload: Record<string, unknown>; source: string } | null {
|
|
53
|
+
return this.appService.getAuditEvent(id);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@EventPattern("audit")
|
|
57
|
+
handleAuditEvent(data: unknown): void {
|
|
58
|
+
if (!isAuditEvent(data)) {
|
|
59
|
+
this.logger.warn("Discarded malformed audit event payload");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this.appService.recordAuditEvent(data);
|
|
63
|
+
}
|
|
64
|
+
{{/unless}}
|
|
65
|
+
|
|
15
66
|
{{#if (includes microservices "grpc")}}
|
|
16
67
|
@GrpcMethod("AppService", "Ping")
|
|
17
68
|
handleGrpcPing(): { message: string } {
|
|
@@ -5,6 +5,27 @@ import { DbService } from "./database/db.service.js";
|
|
|
5
5
|
|
|
6
6
|
@Injectable()
|
|
7
7
|
export class AppService {
|
|
8
|
+
{{#unless (includes microservices "grpc")}}
|
|
9
|
+
private readonly auditEvents = new Map<
|
|
10
|
+
string,
|
|
11
|
+
{ id: string; payload: Record<string, unknown>; source: string }
|
|
12
|
+
>();
|
|
13
|
+
|
|
14
|
+
recordAuditEvent(event: {
|
|
15
|
+
id: string;
|
|
16
|
+
payload: Record<string, unknown>;
|
|
17
|
+
source: string;
|
|
18
|
+
}): void {
|
|
19
|
+
this.auditEvents.set(event.id, event);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getAuditEvent(
|
|
23
|
+
id: string
|
|
24
|
+
): { id: string; payload: Record<string, unknown>; source: string } | null {
|
|
25
|
+
return this.auditEvents.get(id) ?? null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
{{/unless}}
|
|
8
29
|
{{#if (or hasDatabase hasOrm)}}
|
|
9
30
|
constructor(private readonly db: DbService) {}
|
|
10
31
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { NestFactory } from "@nestjs/core";
|
|
2
1
|
{{#if (eq httpProvider "fastify")}}
|
|
3
2
|
import { FastifyAdapter, NestFastifyApplication } from "@nestjs/platform-fastify";
|
|
4
3
|
{{/if}}
|
|
5
4
|
import { Logger } from "@nestjs/common";
|
|
6
|
-
import {
|
|
5
|
+
import { NestFactory } from "@nestjs/core";
|
|
6
|
+
import { Transport } from "@nestjs/microservices";
|
|
7
|
+
import type { MicroserviceOptions } from "@nestjs/microservices";
|
|
7
8
|
|
|
8
9
|
import { AppModule } from "./app.module.js";
|
|
9
10
|
import { env } from "./config/env.js";
|
|
@@ -27,19 +28,36 @@ async function bootstrap() {
|
|
|
27
28
|
app.enableShutdownHooks();
|
|
28
29
|
|
|
29
30
|
app.connectMicroservice<MicroserviceOptions>({
|
|
30
|
-
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}},
|
|
31
31
|
options: {
|
|
32
|
-
{{#if (includes microservices "
|
|
33
|
-
{
|
|
34
|
-
{{
|
|
35
|
-
{{#if (includes microservices "
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
{{#if (includes microservices "kafka")}}
|
|
33
|
+
client: { brokers: [env.KAFKA_BROKER ?? "127.0.0.1:9092"] },
|
|
34
|
+
{{/if}}
|
|
35
|
+
{{#if (includes microservices "grpc")}}
|
|
36
|
+
package: "app",
|
|
37
|
+
protoPath: "proto/app.proto",
|
|
38
|
+
url: env.GRPC_URL ?? "0.0.0.0:5000",
|
|
39
|
+
{{/if}}
|
|
40
|
+
{{#if (includes microservices "rabbitmq")}}
|
|
41
|
+
queue: "ping",
|
|
42
|
+
queueOptions: { durable: false },
|
|
43
|
+
urls: [env.RABBITMQ_URL ?? "amqp://127.0.0.1:5672"],
|
|
44
|
+
{{/if}}
|
|
45
|
+
{{#if (includes microservices "redis")}}
|
|
46
|
+
host: new URL(env.REDIS_URL).hostname,
|
|
47
|
+
port: Number(new URL(env.REDIS_URL).port || 6379),
|
|
48
|
+
{{/if}}
|
|
49
|
+
{{#if (includes microservices "nats")}}
|
|
50
|
+
servers: [env.NATS_URL ?? "nats://127.0.0.1:4222"],
|
|
51
|
+
{{/if}}
|
|
52
|
+
{{#if (includes microservices "mqtt")}}
|
|
53
|
+
url: env.MQTT_URL ?? "mqtt://127.0.0.1:1883",
|
|
54
|
+
{{/if}}
|
|
38
55
|
{{#unless (or (includes microservices "redis") (includes microservices "mqtt") (includes microservices "nats") (includes microservices "rabbitmq") (includes microservices "kafka") (includes microservices "grpc"))}}
|
|
39
56
|
host: "0.0.0.0",
|
|
40
57
|
port: env.MICROSERVICE_PORT,
|
|
41
58
|
{{/unless}}
|
|
42
59
|
},
|
|
60
|
+
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}},
|
|
43
61
|
});
|
|
44
62
|
|
|
45
63
|
{{#if swagger}}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
{{#if (includes microservices "grpc")}}type ClientGrpc,{{else if (includes microservices "kafka")}}type ClientKafka,{{else}}type ClientProxy,{{/if}}
|
|
5
|
-
type MicroserviceOptions,
|
|
6
|
-
Transport,
|
|
7
|
-
} from "@nestjs/microservices";
|
|
1
|
+
import type { INestApplication } from "@nestjs/common";
|
|
2
|
+
import { ClientProxyFactory, Transport } from "@nestjs/microservices";
|
|
3
|
+
import type { {{#if (includes microservices "grpc")}}ClientGrpc{{else if (includes microservices "kafka")}}ClientKafka{{else}}ClientProxy{{/if}}, MicroserviceOptions } from "@nestjs/microservices";
|
|
8
4
|
import { Test } from "@nestjs/testing";
|
|
5
|
+
{{#unless (includes microservices "grpc")}}
|
|
9
6
|
import { setTimeout as delay } from "node:timers/promises";
|
|
10
|
-
import
|
|
7
|
+
import request from "supertest";
|
|
8
|
+
{{/unless}}
|
|
9
|
+
import { firstValueFrom } from "rxjs";
|
|
10
|
+
{{#if (includes microservices "grpc")}}
|
|
11
|
+
import type { Observable } from "rxjs";
|
|
12
|
+
{{/if}}
|
|
11
13
|
import { afterAll, expect, it } from "vitest";
|
|
12
14
|
|
|
13
15
|
import { AppModule } from "../src/app.module.js";
|
|
@@ -16,21 +18,40 @@ import { env } from "../src/config/env.js";
|
|
|
16
18
|
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}};
|
|
17
19
|
|
|
18
20
|
const options = {
|
|
19
|
-
{{#if (includes microservices "
|
|
20
|
-
{
|
|
21
|
-
{
|
|
22
|
-
{{
|
|
23
|
-
{{#if (includes microservices "
|
|
24
|
-
|
|
21
|
+
{{#if (includes microservices "kafka")}}
|
|
22
|
+
client: { brokers: [env.KAFKA_BROKER] },
|
|
23
|
+
consumer: { groupId: "ping-consumer" },
|
|
24
|
+
{{/if}}
|
|
25
|
+
{{#if (includes microservices "grpc")}}
|
|
26
|
+
package: "app",
|
|
27
|
+
protoPath: "proto/app.proto",
|
|
28
|
+
url: env.GRPC_URL,
|
|
29
|
+
{{/if}}
|
|
30
|
+
{{#if (includes microservices "rabbitmq")}}
|
|
31
|
+
queue: "ping",
|
|
32
|
+
queueOptions: { durable: false },
|
|
33
|
+
urls: [env.RABBITMQ_URL],
|
|
34
|
+
{{/if}}
|
|
35
|
+
{{#if (includes microservices "redis")}}
|
|
36
|
+
host: new URL(env.REDIS_URL).hostname,
|
|
37
|
+
port: Number(new URL(env.REDIS_URL).port || 6379),
|
|
38
|
+
{{/if}}
|
|
39
|
+
{{#if (includes microservices "nats")}}
|
|
40
|
+
servers: [env.NATS_URL],
|
|
41
|
+
{{/if}}
|
|
42
|
+
{{#if (includes microservices "mqtt")}}
|
|
43
|
+
url: env.MQTT_URL,
|
|
44
|
+
{{/if}}
|
|
25
45
|
};
|
|
26
46
|
|
|
27
47
|
let app: INestApplication;
|
|
28
|
-
|
|
48
|
+
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}}
|
|
29
49
|
|
|
30
50
|
it("sends ping through the selected message transport", async () => {
|
|
31
51
|
const module = await Test.createTestingModule({ imports: [AppModule] }).compile();
|
|
32
52
|
app = module.createNestApplication();
|
|
33
|
-
app.connectMicroservice<MicroserviceOptions>({
|
|
53
|
+
app.connectMicroservice<MicroserviceOptions>({ options, transport } as MicroserviceOptions);
|
|
54
|
+
app.setGlobalPrefix("api");
|
|
34
55
|
await app.startAllMicroservices();
|
|
35
56
|
await app.init();
|
|
36
57
|
{{#if (includes microservices "kafka")}}
|
|
@@ -42,7 +63,7 @@ it("sends ping through the selected message transport", async () => {
|
|
|
42
63
|
try {
|
|
43
64
|
await client.connect();
|
|
44
65
|
} catch {
|
|
45
|
-
await delay(
|
|
66
|
+
await delay(1000);
|
|
46
67
|
await client.connect();
|
|
47
68
|
}
|
|
48
69
|
{{else}}
|
|
@@ -55,12 +76,23 @@ it("sends ping through the selected message transport", async () => {
|
|
|
55
76
|
log: (request: { id: string; source: string; payload: string }) => Observable<{ id: string; source: string; payload: string }>;
|
|
56
77
|
}>("AppService");
|
|
57
78
|
await expect(firstValueFrom(service.ping({}))).resolves.toEqual({ message: "pong" });
|
|
58
|
-
const event = { id: "event-1",
|
|
79
|
+
const event = { id: "event-1", payload: "healthy", source: "scenario" };
|
|
59
80
|
await expect(firstValueFrom(service.log(event))).resolves.toEqual(event);
|
|
60
81
|
{{else}}
|
|
61
82
|
await expect(firstValueFrom(client.send<string, Record<string, never>>("ping", {}))).resolves.toBe("pong");
|
|
62
|
-
const event = { id: "event-1",
|
|
83
|
+
const event = { id: "event-1", payload: { healthy: true }, source: "scenario" };
|
|
63
84
|
await expect(firstValueFrom(client.send("log", event))).resolves.toEqual({ received: event });
|
|
85
|
+
const auditEvent = {
|
|
86
|
+
id: "audit-1",
|
|
87
|
+
payload: { healthy: true },
|
|
88
|
+
source: "scenario",
|
|
89
|
+
};
|
|
90
|
+
await firstValueFrom(client.emit("audit", auditEvent));
|
|
91
|
+
await delay(250);
|
|
92
|
+
const auditResponse = await request(app.getHttpServer())
|
|
93
|
+
.get(`/api/events/${auditEvent.id}`)
|
|
94
|
+
.expect(200);
|
|
95
|
+
expect(auditResponse.body).toEqual(auditEvent);
|
|
64
96
|
{{/if}}
|
|
65
97
|
}, 60_000);
|
|
66
98
|
|