@nest-arch/tui 0.4.3 → 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.
@@ -12,10 +12,10 @@ import type { Collection, Document } from "mongodb";
12
12
  {{/if}}
13
13
  export interface IMongoAdapter<TClient = unknown> {
14
14
  {{#if useRawSql}}
15
- collection<T extends Document = Document>(name: string): Collection<T>;
15
+ collection: <T extends Document = Document>(name: string) => Collection<T>;
16
16
  {{else}}
17
- collection<T = unknown>(name: string): T;
17
+ collection: <T = unknown>(name: string) => T;
18
18
  {{/if}}
19
- getClient(): TClient;
19
+ getClient: () => TClient;
20
20
  }
21
21
  {{/if}}
@@ -23,7 +23,7 @@ import type { Prisma, PrismaClient } from '../../orm/prisma/generated/client.js'
23
23
  {{/if}}
24
24
 
25
25
  @Injectable()
26
- export class PrismaMongoAdapter implements IMongoAdapter<any> {
26
+ export class PrismaMongoAdapter implements IMongoAdapter {
27
27
  {{#if isPrisma8}}
28
28
  readonly client = db;
29
29
 
@@ -31,8 +31,8 @@ export class PrismaMongoAdapter implements IMongoAdapter<any> {
31
31
  return ((this.client as any).orm as any)?.[name] as T;
32
32
  }
33
33
 
34
- async runCommand(command: unknown): Promise<unknown> {
35
- return (this.client as any).query?.(command);
34
+ runCommand(command: unknown): Promise<unknown> {
35
+ return (this.client as { query?: (input: unknown) => Promise<unknown> }).query?.(command) ?? Promise.resolve(undefined);
36
36
  }
37
37
 
38
38
  getClient() {
@@ -46,7 +46,7 @@ export class PrismaMongoAdapter implements IMongoAdapter<any> {
46
46
  return this.prisma[name as keyof PrismaClient] as T;
47
47
  }
48
48
 
49
- async runCommand(command: Prisma.InputJsonObject): Promise<unknown> {
49
+ runCommand(command: Prisma.InputJsonObject): Promise<unknown> {
50
50
  return this.prisma.$runCommandRaw(command);
51
51
  }
52
52
 
@@ -4,12 +4,21 @@ export * from './db.module.js';
4
4
  export * from './db.service.js';
5
5
  {{#if usePrisma}}
6
6
  {{#if isPrisma6}}
7
- export type { PrismaClient, Todo } from '@prisma/client';
7
+ export type { PrismaClient } from '@prisma/client';
8
+ {{#if hasTodoExample}}
9
+ export type { Todo } from '@prisma/client';
10
+ {{/if}}
8
11
  {{else}}
9
12
  {{#if isMonorepo}}
10
- export type { PrismaClient, Todo } from './database/orm/prisma/generated/client.js';
13
+ export type { PrismaClient } from './database/orm/prisma/generated/client.js';
14
+ {{#if hasTodoExample}}
15
+ export type { Todo } from './database/orm/prisma/generated/client.js';
16
+ {{/if}}
11
17
  {{else}}
12
- export type { PrismaClient, Todo } from './orm/prisma/generated/client.js';
18
+ export type { PrismaClient } from './orm/prisma/generated/client.js';
19
+ {{#if hasTodoExample}}
20
+ export type { Todo } from './orm/prisma/generated/client.js';
21
+ {{/if}}
13
22
  {{/if}}
14
23
  {{/if}}
15
24
  {{/if}}
@@ -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 = unwrap(created.body) as { id: string; title: string };
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 aliceTodos = unwrap((await call("findAll", undefined, alice).expect(200)).body) as { id: string }[];
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 bobTodos = unwrap((await call("findAll", undefined, bob).expect(200)).body) as { id: string }[];
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
- expect((unwrap(updated.body) as { completed: boolean }).completed).toBe(true);
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("/trpc/appRouter.testAuth");
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
  );
@@ -8,6 +8,7 @@
8
8
  "**/*spec.ts"
9
9
  ],
10
10
  "compilerOptions": {
11
+ "incremental": false,
11
12
  "rootDir": "./src",
12
13
  "strictNullChecks": true
13
14
  }
@@ -5,8 +5,8 @@ export default defineConfig({
5
5
  tsconfigPaths: true,
6
6
  },
7
7
  test: {
8
- include: ["**/*.e2e-spec.ts"],
9
8
  globals: true,
9
+ include: ["**/*.e2e-spec.ts"],
10
10
  root: "./",
11
11
  },
12
12
  });
@@ -5,8 +5,8 @@ export default defineConfig({
5
5
  tsconfigPaths: true,
6
6
  },
7
7
  test: {
8
- include: ["**/*.spec.ts"],
9
8
  globals: true,
9
+ include: ["**/*.spec.ts"],
10
10
  root: "./",
11
11
  },
12
12
  });
@@ -28,7 +28,13 @@ export class HealthController {
28
28
  {{#if hasDatabase}}
29
29
  async () => {
30
30
  try {
31
- {{#if (or usePrisma isPrisma8)}}
31
+ {{#if hasMongodb}}
32
+ {{#if usePrisma}}
33
+ await this.db.raw.mongo.$runCommandRaw({ ping: 1 });
34
+ {{else}}
35
+ await this.db.raw.mongo.command({ ping: 1 });
36
+ {{/if}}
37
+ {{else if (or usePrisma isPrisma8)}}
32
38
  await this.db.prisma.$queryRaw`SELECT 1`;
33
39
  {{else if useDrizzle}}
34
40
  {{#if hasSqlite}}
@@ -38,8 +44,6 @@ export class HealthController {
38
44
  {{/if}}
39
45
  {{else if useTypeorm}}
40
46
  await this.db.typeorm.query("SELECT 1");
41
- {{else if hasMongodb}}
42
- await this.db.raw.mongo.command({ ping: 1 });
43
47
  {{else}}
44
48
  await this.db.raw.sql.query("SELECT 1");
45
49
  {{/if}}
@@ -22,11 +22,9 @@ import {
22
22
  {{#if (or isPassportAuth isBetterAuth)}}
23
23
  ApiUnauthorizedResponse,
24
24
  {{/if}}
25
- } from "@nestjs/swagger";
26
- {{/if}}
27
- {{#if (and isPassportAuth swagger)}}
28
- import {
25
+ {{#if isPassportAuth}}
29
26
  ApiBearerAuth,
27
+ {{/if}}
30
28
  } from "@nestjs/swagger";
31
29
  {{/if}}
32
30
  {{#if (and isBetterAuth hasDatabase)}}
@@ -162,14 +160,14 @@ export class TodoController {
162
160
  {{/if}}
163
161
  {{/if}}
164
162
  {{#if (and isPassportAuth hasDatabase)}}
165
- async delete(
163
+ delete(
166
164
  @Param("id") id: string,
167
165
  @CurrentUser() user: { id: string }
168
166
  ) {
169
167
  return this.todoService.delete(id, user.id);
170
168
  }
171
169
  {{else if (and isBetterAuth hasDatabase)}}
172
- async delete(
170
+ delete(
173
171
  @Param("id") id: string,
174
172
  @Session() session: UserSession
175
173
  ) {
@@ -1,11 +1,8 @@
1
1
  {{#if hasTrpc}}
2
- import { Router, Query, Mutation, Ctx, Input{{#if hasTrpcAndAuth}}, UseMiddlewares{{/if}} } from "nestjs-trpc";
2
+ import { Router, Query, Mutation{{#if hasTrpcAndAuth}}, Ctx, UseMiddlewares{{/if}}, Input } from "nestjs-trpc";
3
3
  import { NotFoundException } from "@nestjs/common";
4
4
  import { TRPCError } from "@trpc/server";
5
- import z from "zod";
6
- {{#if (and hasTrpcAndAuth isBetterAuth)}}
7
- import type { UserSession } from "../../auth/auth.js";
8
- {{/if}}
5
+ import { z } from "zod";
9
6
  {{#if hasTrpcAndAuth}}
10
7
  import { AuthTRPCMiddleware } from "../../libs/middlewares/auth.trpc.middleware.js";
11
8
  import type { InnerContext } from "../../libs/trpc/inner.context.js";
@@ -46,26 +43,28 @@ export class TodoRouter {
46
43
 
47
44
  @Mutation({ input: updateTodoInput, output: todoSchema })
48
45
  @UseMiddlewares(AuthTRPCMiddleware)
49
- update(@Ctx() ctx: InnerContext, @Input() input: { id: string | number; data: UpdateTodoDto }) {
50
- return this.todoService.update(String(input.id), input.data, ctx.session!.user.id)
51
- .catch((error: unknown) => {
52
- if (error instanceof NotFoundException) {
53
- throw new TRPCError({ code: "NOT_FOUND", message: error.message });
54
- }
55
- throw error;
56
- });
46
+ async update(@Ctx() ctx: InnerContext, @Input() input: { id: string | number; data: UpdateTodoDto }) {
47
+ try {
48
+ return await this.todoService.update(String(input.id), input.data, ctx.session!.user.id);
49
+ } catch (error) {
50
+ if (error instanceof NotFoundException) {
51
+ throw new TRPCError({ code: "NOT_FOUND", message: error.message });
52
+ }
53
+ throw error;
54
+ }
57
55
  }
58
56
 
59
57
  @Mutation({ input: z.object({ id: z.union([z.string(), z.number()]) }) })
60
58
  @UseMiddlewares(AuthTRPCMiddleware)
61
- delete(@Ctx() ctx: InnerContext, @Input() input: { id: string | number }) {
62
- return this.todoService.delete(String(input.id), ctx.session!.user.id)
63
- .catch((error: unknown) => {
64
- if (error instanceof NotFoundException) {
65
- throw new TRPCError({ code: "NOT_FOUND", message: error.message });
66
- }
67
- throw error;
68
- });
59
+ async delete(@Ctx() ctx: InnerContext, @Input() input: { id: string | number }) {
60
+ try {
61
+ return await this.todoService.delete(String(input.id), ctx.session!.user.id);
62
+ } catch (error) {
63
+ if (error instanceof NotFoundException) {
64
+ throw new TRPCError({ code: "NOT_FOUND", message: error.message });
65
+ }
66
+ throw error;
67
+ }
69
68
  }
70
69
  {{else}}
71
70
  @Query({ output: z.array(todoSchema) })
@@ -79,25 +78,27 @@ export class TodoRouter {
79
78
  }
80
79
 
81
80
  @Mutation({ input: updateTodoInput, output: todoSchema })
82
- update(@Input() input: { id: string; data: UpdateTodoDto }) {
83
- return this.todoService.update(input.id, input.data)
84
- .catch((error: unknown) => {
85
- if (error instanceof NotFoundException) {
86
- throw new TRPCError({ code: "NOT_FOUND", message: error.message });
87
- }
88
- throw error;
89
- });
81
+ async update(@Input() input: { id: string; data: UpdateTodoDto }) {
82
+ try {
83
+ return await this.todoService.update(input.id, input.data);
84
+ } catch (error) {
85
+ if (error instanceof NotFoundException) {
86
+ throw new TRPCError({ code: "NOT_FOUND", message: error.message });
87
+ }
88
+ throw error;
89
+ }
90
90
  }
91
91
 
92
92
  @Mutation({ input: z.object({ id: z.union([z.string(), z.number()]) }) })
93
- delete(@Input() input: { id: string | number }) {
94
- return this.todoService.delete(String(input.id))
95
- .catch((error: unknown) => {
96
- if (error instanceof NotFoundException) {
97
- throw new TRPCError({ code: "NOT_FOUND", message: error.message });
98
- }
99
- throw error;
100
- });
93
+ async delete(@Input() input: { id: string | number }) {
94
+ try {
95
+ return await this.todoService.delete(String(input.id));
96
+ } catch (error) {
97
+ if (error instanceof NotFoundException) {
98
+ throw new TRPCError({ code: "NOT_FOUND", message: error.message });
99
+ }
100
+ throw error;
101
+ }
101
102
  }
102
103
  {{/if}}
103
104
  }
@@ -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, expect, it } from "vitest";
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";
@@ -1,10 +1,43 @@
1
+ {{#if (includes microservices "grpc")}}
1
2
  import { Controller, Get } from "@nestjs/common";
2
- import { {{#if (includes microservices "grpc")}}GrpcMethod{{else}}MessagePattern{{/if}} } from "@nestjs/microservices";
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 { Transport, type MicroserviceOptions } from "@nestjs/microservices";
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 "redis")}}host: new URL(env.REDIS_URL).hostname, port: Number(new URL(env.REDIS_URL).port || 6379),{{/if}}
33
- {{#if (includes microservices "mqtt")}}url: env.MQTT_URL ?? "mqtt://127.0.0.1:1883",{{/if}}
34
- {{#if (includes microservices "nats")}}servers: [env.NATS_URL ?? "nats://127.0.0.1:4222"],{{/if}}
35
- {{#if (includes microservices "rabbitmq")}}urls: [env.RABBITMQ_URL ?? "amqp://127.0.0.1:5672"], queue: "ping", queueOptions: { durable: false },{{/if}}
36
- {{#if (includes microservices "kafka")}}client: { brokers: [env.KAFKA_BROKER ?? "127.0.0.1:9092"] },{{/if}}
37
- {{#if (includes microservices "grpc")}}url: env.GRPC_URL ?? "0.0.0.0:5000", package: "app", protoPath: "proto/app.proto",{{/if}}
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 { type INestApplication } from "@nestjs/common";
2
- import {
3
- ClientProxyFactory,
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 { firstValueFrom, type Observable } from "rxjs";
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 "redis")}}host: new URL(env.REDIS_URL).hostname, port: Number(new URL(env.REDIS_URL).port || 6379),{{/if}}
20
- {{#if (includes microservices "mqtt")}}url: env.MQTT_URL,{{/if}}
21
- {{#if (includes microservices "nats")}}servers: [env.NATS_URL],{{/if}}
22
- {{#if (includes microservices "rabbitmq")}}urls: [env.RABBITMQ_URL], queue: "ping", queueOptions: { durable: false },{{/if}}
23
- {{#if (includes microservices "kafka")}}client: { brokers: [env.KAFKA_BROKER] }, consumer: { groupId: "ping-consumer" },{{/if}}
24
- {{#if (includes microservices "grpc")}}url: env.GRPC_URL, package: "app", protoPath: "proto/app.proto",{{/if}}
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
- let client{{#if (includes microservices "grpc")}}: ClientGrpc = ClientProxyFactory.create({ transport, options } as MicroserviceOptions) as unknown as ClientGrpc;{{else if (includes microservices "kafka")}}: ClientKafka = ClientProxyFactory.create({ transport, options } as MicroserviceOptions) as ClientKafka;{{else}}: ClientProxy = ClientProxyFactory.create({ transport, options } as MicroserviceOptions);{{/if}}
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>({ transport, options } as 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(1_000);
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", source: "scenario", payload: "healthy" };
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", source: "scenario", payload: { healthy: true } };
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