@leo-h/create-nodejs-app 1.0.5 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/package.json.js +1 -1
  2. package/dist/src/validations/framework.validation.js +1 -1
  3. package/package.json +1 -1
  4. package/templates/nest/.env.example +11 -0
  5. package/templates/nest/.eslintignore +2 -0
  6. package/templates/nest/.eslintrc.json +22 -0
  7. package/templates/nest/.husky/pre-commit +1 -0
  8. package/templates/nest/.lintstagedrc.json +4 -0
  9. package/templates/nest/.prettierignore +7 -0
  10. package/templates/nest/.prettierrc.json +6 -0
  11. package/templates/nest/.swcrc +20 -0
  12. package/templates/nest/.vscode/settings.json +8 -0
  13. package/templates/nest/nest-cli.json +10 -0
  14. package/templates/nest/package.json +59 -0
  15. package/templates/nest/pnpm-lock.yaml +6372 -0
  16. package/templates/nest/src/core/errors/domain-error.ts +8 -0
  17. package/templates/nest/src/core/errors/errors.ts +9 -0
  18. package/templates/nest/src/infra/app.module.ts +7 -0
  19. package/templates/nest/src/infra/env.ts +22 -0
  20. package/templates/nest/src/infra/http/controllers/hello/hello-multipart.controller.e2e-spec.ts +58 -0
  21. package/templates/nest/src/infra/http/controllers/hello/hello-multipart.controller.ts +75 -0
  22. package/templates/nest/src/infra/http/controllers/hello/hello.controller.e2e-spec.ts +38 -0
  23. package/templates/nest/src/infra/http/controllers/hello/hello.controller.ts +51 -0
  24. package/templates/nest/src/infra/http/errors/upload-validation.error.ts +17 -0
  25. package/templates/nest/src/infra/http/events/fastify-multer.event.module.ts +15 -0
  26. package/templates/nest/src/infra/http/filters/domain-exception.filter.ts +53 -0
  27. package/templates/nest/src/infra/http/filters/http-exception.filter.ts +26 -0
  28. package/templates/nest/src/infra/http/http.module.ts +23 -0
  29. package/templates/nest/src/infra/http/middlewares/upload-interceptor.ts +365 -0
  30. package/templates/nest/src/infra/http/middlewares/zod-schema-pipe.ts +78 -0
  31. package/templates/nest/src/infra/http/middlewares/zod-validation-pipe.ts +33 -0
  32. package/templates/nest/src/infra/presenters/error.presenter.ts +14 -0
  33. package/templates/nest/src/infra/server.ts +45 -0
  34. package/templates/nest/test/e2e/sample-upload.jpg +0 -0
  35. package/templates/nest/tsconfig.json +17 -0
  36. package/templates/nest/vitest.config.e2e.mts +8 -0
  37. package/templates/nest/vitest.config.mts +6 -0
@@ -0,0 +1,8 @@
1
+ export abstract class DomainError extends Error {
2
+ public abstract error: string;
3
+ public abstract debug: unknown;
4
+
5
+ constructor(public message: string) {
6
+ super(message);
7
+ }
8
+ }
@@ -0,0 +1,9 @@
1
+ import { DomainError } from "./domain-error";
2
+
3
+ export class ValidationError extends DomainError {
4
+ public error = "ValidationError";
5
+
6
+ constructor(public debug: object | null = null) {
7
+ super("Os dados enviados são inválidos.");
8
+ }
9
+ }
@@ -0,0 +1,7 @@
1
+ import { Module } from "@nestjs/common";
2
+ import { HttpModule } from "./http/http.module";
3
+
4
+ @Module({
5
+ imports: [HttpModule],
6
+ })
7
+ export class AppModule {}
@@ -0,0 +1,22 @@
1
+ import packageJson from "@/../package.json";
2
+ import { config } from "dotenv";
3
+ import { z } from "zod";
4
+
5
+ config({ override: true });
6
+
7
+ const schema = z.object({
8
+ NODE_ENV: z.enum(["test", "development", "production"]),
9
+ API_NAME: z.string().default(packageJson.name),
10
+ API_PORT: z.coerce.number().default(3333),
11
+ API_ACCESS_PERMISSION_CLIENT_SIDE: z.string().default("*"),
12
+ });
13
+
14
+ const parsedEnv = schema.safeParse(process.env);
15
+
16
+ if (!parsedEnv.success) {
17
+ console.error(parsedEnv.error.flatten().fieldErrors);
18
+
19
+ throw new Error("Invalid environment variables.");
20
+ }
21
+
22
+ export const env = parsedEnv.data;
@@ -0,0 +1,58 @@
1
+ import { AppModule } from "@/infra/app.module";
2
+ import { faker } from "@faker-js/faker";
3
+ import {
4
+ FastifyAdapter,
5
+ NestFastifyApplication,
6
+ } from "@nestjs/platform-fastify";
7
+ import { Test } from "@nestjs/testing";
8
+ import { lookup } from "mime-types";
9
+ import { basename, extname } from "path";
10
+ import request from "supertest";
11
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
12
+
13
+ const SAMPLE_UPLOAD_PATH = "./test/e2e/sample-upload.jpg";
14
+
15
+ describe("[Controller] Hello multipart", () => {
16
+ let app: NestFastifyApplication;
17
+
18
+ beforeAll(async () => {
19
+ const moduleRef = await Test.createTestingModule({
20
+ imports: [AppModule],
21
+ }).compile();
22
+
23
+ app = moduleRef.createNestApplication<NestFastifyApplication>(
24
+ new FastifyAdapter(),
25
+ );
26
+
27
+ await app.init();
28
+ await app.getHttpAdapter().getInstance().ready();
29
+ });
30
+
31
+ afterAll(async () => {
32
+ await app.close();
33
+ });
34
+
35
+ it("[POST] /hello/multipart", async () => {
36
+ const fieldName = "attachment";
37
+ const description = faker.lorem.sentence();
38
+
39
+ const response = await request(app.getHttpServer())
40
+ .post("/hello/multipart")
41
+ .attach(fieldName, SAMPLE_UPLOAD_PATH)
42
+ .field({ description });
43
+
44
+ expect(response.statusCode).toEqual(200);
45
+ expect(response.body).toEqual(
46
+ expect.objectContaining({
47
+ message: "Hello world!",
48
+ description,
49
+ file: expect.objectContaining({
50
+ fieldname: fieldName,
51
+ originalname: basename(SAMPLE_UPLOAD_PATH),
52
+ mimetype: lookup(extname(SAMPLE_UPLOAD_PATH)),
53
+ size: expect.any(Number),
54
+ }),
55
+ }),
56
+ );
57
+ });
58
+ });
@@ -0,0 +1,75 @@
1
+ import { Body, Controller, HttpCode, Post, UploadedFile } from "@nestjs/common";
2
+ import { ApiOperation, ApiTags } from "@nestjs/swagger";
3
+ import { File } from "fastify-multer/lib/interfaces";
4
+ import { z } from "zod";
5
+ import { UploadInterceptor } from "../../middlewares/upload-interceptor";
6
+ import { ZodSchemaPipe } from "../../middlewares/zod-schema-pipe";
7
+
8
+ const helloMultipartControllerBodySchema = z.object({
9
+ description: z.string().min(2),
10
+ });
11
+
12
+ type HelloMultipartControllerBody = z.infer<
13
+ typeof helloMultipartControllerBodySchema
14
+ >;
15
+
16
+ const helloMultipartControllerResponseSchema = z.object({
17
+ message: z.literal("Hello world!"),
18
+ description: z.string().min(2),
19
+ file: z.object({
20
+ fieldname: z.string(),
21
+ originalname: z.string(),
22
+ encoding: z.string(),
23
+ mimetype: z.string(),
24
+ size: z.number().optional(),
25
+ }),
26
+ });
27
+
28
+ type HelloMultipartControllerResponse = z.infer<
29
+ typeof helloMultipartControllerResponseSchema
30
+ >;
31
+
32
+ @Controller()
33
+ export class HelloMultipartController {
34
+ @ApiTags("Hello")
35
+ @ApiOperation({
36
+ summary: "Hello world with multipart/form-data content-type!",
37
+ })
38
+ @Post("/hello/multipart")
39
+ @HttpCode(200)
40
+ @UploadInterceptor({
41
+ fieldName: "attachment",
42
+ storage: "memory",
43
+ allowedExtensions: ["jpeg", "png", "webp"],
44
+ maxFileSize: 10, // 10MB
45
+ nonFileFieldsZodSchema: helloMultipartControllerBodySchema,
46
+ })
47
+ @ZodSchemaPipe({
48
+ isMultipart: true,
49
+ body: helloMultipartControllerBodySchema,
50
+ response: {
51
+ 200: helloMultipartControllerResponseSchema,
52
+ },
53
+ })
54
+ async handle(
55
+ @UploadedFile()
56
+ file: File,
57
+ @Body()
58
+ body: HelloMultipartControllerBody,
59
+ ): Promise<HelloMultipartControllerResponse> {
60
+ const { fieldname, originalname, encoding, mimetype, size } = file;
61
+ const { description } = body;
62
+
63
+ return {
64
+ message: "Hello world!",
65
+ description,
66
+ file: {
67
+ fieldname,
68
+ originalname,
69
+ encoding,
70
+ mimetype,
71
+ size,
72
+ },
73
+ };
74
+ }
75
+ }
@@ -0,0 +1,38 @@
1
+ import { AppModule } from "@/infra/app.module";
2
+ import {
3
+ FastifyAdapter,
4
+ NestFastifyApplication,
5
+ } from "@nestjs/platform-fastify";
6
+ import { Test } from "@nestjs/testing";
7
+ import request from "supertest";
8
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
9
+
10
+ describe("[Controller] Hello", () => {
11
+ let app: NestFastifyApplication;
12
+
13
+ beforeAll(async () => {
14
+ const moduleRef = await Test.createTestingModule({
15
+ imports: [AppModule],
16
+ }).compile();
17
+
18
+ app = moduleRef.createNestApplication<NestFastifyApplication>(
19
+ new FastifyAdapter(),
20
+ );
21
+
22
+ await app.init();
23
+ await app.getHttpAdapter().getInstance().ready();
24
+ });
25
+
26
+ afterAll(async () => {
27
+ await app.close();
28
+ });
29
+
30
+ it("[GET] /hello", async () => {
31
+ const response = await request(app.getHttpServer())
32
+ .get("/hello")
33
+ .query({ show: true });
34
+
35
+ expect(response.statusCode).toEqual(200);
36
+ expect(response.body).toEqual({ message: "Hello world!" });
37
+ });
38
+ });
@@ -0,0 +1,51 @@
1
+ import {
2
+ BadRequestException,
3
+ Controller,
4
+ Get,
5
+ HttpCode,
6
+ Query,
7
+ } from "@nestjs/common";
8
+ import { ApiOperation, ApiTags } from "@nestjs/swagger";
9
+ import { z } from "zod";
10
+ import { ZodSchemaPipe } from "../../middlewares/zod-schema-pipe";
11
+
12
+ export const helloControllerQuerySchema = z.object({
13
+ show: z
14
+ .enum(["true", "false"])
15
+ .default("true")
16
+ .transform<boolean>(val => JSON.parse(val)),
17
+ });
18
+
19
+ type HelloControllerQuery = z.infer<typeof helloControllerQuerySchema>;
20
+
21
+ const helloControllerResponseSchema = z.object({
22
+ message: z.literal("Hello world!"),
23
+ });
24
+
25
+ type HelloControllerResponse = z.infer<typeof helloControllerResponseSchema>;
26
+
27
+ @Controller()
28
+ export class HelloController {
29
+ @ApiTags("Hello")
30
+ @ApiOperation({ summary: "Hello world!" })
31
+ @Get("/hello")
32
+ @HttpCode(200)
33
+ @ZodSchemaPipe({
34
+ queryParams: helloControllerQuerySchema,
35
+ response: {
36
+ 200: helloControllerResponseSchema,
37
+ },
38
+ })
39
+ async handle(
40
+ @Query() query: HelloControllerQuery,
41
+ ): Promise<HelloControllerResponse> {
42
+ const { show } = query;
43
+
44
+ if (!show)
45
+ throw new BadRequestException(
46
+ `You don't want to display the "hello world"!`,
47
+ );
48
+
49
+ return { message: "Hello world!" };
50
+ }
51
+ }
@@ -0,0 +1,17 @@
1
+ import { ErrorPresenter } from "@/infra/presenters/error.presenter";
2
+ import { HttpException } from "@nestjs/common";
3
+
4
+ export class UploadValidationError extends HttpException {
5
+ constructor(statusCode = 400, debug: object = {}) {
6
+ const presenter = ErrorPresenter.toHttp(statusCode, {
7
+ error: "UploadValidationError",
8
+ message: "Os dados enviados são inválidos.",
9
+ debug: {
10
+ multerError: null,
11
+ ...debug,
12
+ },
13
+ });
14
+
15
+ super(presenter, statusCode);
16
+ }
17
+ }
@@ -0,0 +1,15 @@
1
+ import { Module, OnApplicationBootstrap } from "@nestjs/common";
2
+ import { HttpAdapterHost } from "@nestjs/core";
3
+ import { FastifyAdapter } from "@nestjs/platform-fastify";
4
+ import multer from "fastify-multer";
5
+
6
+ @Module({})
7
+ export class FastifyMulterEventModule implements OnApplicationBootstrap {
8
+ constructor(private httpAdapterHost: HttpAdapterHost<FastifyAdapter>) {}
9
+
10
+ onApplicationBootstrap() {
11
+ const app = this.httpAdapterHost.httpAdapter.getInstance();
12
+
13
+ app.register(multer.contentParser);
14
+ }
15
+ }
@@ -0,0 +1,53 @@
1
+ import { DomainError } from "@/core/errors/domain-error";
2
+ import { ValidationError } from "@/core/errors/errors";
3
+ import { env } from "@/infra/env";
4
+ import { ErrorPresenter } from "@/infra/presenters/error.presenter";
5
+ import {
6
+ ArgumentsHost,
7
+ BadRequestException,
8
+ Catch,
9
+ ExceptionFilter,
10
+ HttpException,
11
+ HttpStatus,
12
+ InternalServerErrorException,
13
+ } from "@nestjs/common";
14
+ import { FastifyReply } from "fastify";
15
+
16
+ @Catch(DomainError)
17
+ export class DomainExceptionFilter implements ExceptionFilter {
18
+ catch(exception: DomainError, host: ArgumentsHost): void {
19
+ const ctx = host.switchToHttp();
20
+ const response = ctx.getResponse<FastifyReply>();
21
+
22
+ let httpException: HttpException;
23
+
24
+ switch (exception.constructor) {
25
+ case ValidationError:
26
+ httpException = new BadRequestException(
27
+ ErrorPresenter.toHttp(HttpStatus.BAD_REQUEST, exception),
28
+ );
29
+ break;
30
+
31
+ default:
32
+ httpException = new InternalServerErrorException(
33
+ ErrorPresenter.toHttp(HttpStatus.INTERNAL_SERVER_ERROR, {
34
+ error: "InternalServerError",
35
+ message: "Desculpe, um erro inesperado ocorreu.",
36
+ debug: exception.message,
37
+ }),
38
+ );
39
+ break;
40
+ }
41
+
42
+ const httpResponse = httpException.getResponse();
43
+
44
+ if (
45
+ env.NODE_ENV === "production" &&
46
+ typeof httpResponse === "object" &&
47
+ "debug" in httpResponse
48
+ )
49
+ delete httpResponse.debug;
50
+
51
+ response.status(httpException.getStatus()).send(httpResponse);
52
+ }
53
+ }
@@ -0,0 +1,26 @@
1
+ import { env } from "@/infra/env";
2
+ import {
3
+ ArgumentsHost,
4
+ Catch,
5
+ ExceptionFilter,
6
+ HttpException,
7
+ } from "@nestjs/common";
8
+ import { FastifyReply } from "fastify";
9
+
10
+ @Catch(HttpException)
11
+ export class HttpExceptionFilter implements ExceptionFilter {
12
+ catch(httpException: HttpException, host: ArgumentsHost): void {
13
+ const ctx = host.switchToHttp();
14
+ const response = ctx.getResponse<FastifyReply>();
15
+ const httpResponse = httpException.getResponse();
16
+
17
+ if (
18
+ env.NODE_ENV === "production" &&
19
+ typeof httpResponse === "object" &&
20
+ "debug" in httpResponse
21
+ )
22
+ delete httpResponse.debug;
23
+
24
+ response.status(httpException.getStatus()).send(httpResponse);
25
+ }
26
+ }
@@ -0,0 +1,23 @@
1
+ import { Module } from "@nestjs/common";
2
+ import { APP_FILTER } from "@nestjs/core";
3
+ import { HelloMultipartController } from "./controllers/hello/hello-multipart.controller";
4
+ import { HelloController } from "./controllers/hello/hello.controller";
5
+ import { FastifyMulterEventModule } from "./events/fastify-multer.event.module";
6
+ import { DomainExceptionFilter } from "./filters/domain-exception.filter";
7
+ import { HttpExceptionFilter } from "./filters/http-exception.filter";
8
+
9
+ @Module({
10
+ imports: [FastifyMulterEventModule],
11
+ providers: [
12
+ {
13
+ provide: APP_FILTER,
14
+ useClass: DomainExceptionFilter,
15
+ },
16
+ {
17
+ provide: APP_FILTER,
18
+ useClass: HttpExceptionFilter,
19
+ },
20
+ ],
21
+ controllers: [HelloController, HelloMultipartController],
22
+ })
23
+ export class HttpModule {}