@leo-h/create-nodejs-app 1.0.7 → 1.0.8

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 (36) hide show
  1. package/dist/package.json.js +1 -1
  2. package/package.json +1 -1
  3. package/templates/clean/tsconfig.json +1 -1
  4. package/templates/fastify/build.config.ts +1 -1
  5. package/templates/fastify/package.json +2 -2
  6. package/templates/fastify/src/core/errors/domain-error.ts +8 -0
  7. package/templates/fastify/src/core/errors/errors.ts +9 -0
  8. package/templates/fastify/src/{app.ts → infra/http/app.ts} +1 -1
  9. package/templates/fastify/src/infra/http/controllers/hello/hello-multipart.controller.e2e-spec.ts +34 -0
  10. package/templates/fastify/src/{controllers → infra/http/controllers}/hello/hello-multipart.controller.ts +23 -9
  11. package/templates/fastify/src/{controllers → infra/http/controllers}/hello/hello.controller.e2e-spec.ts +1 -1
  12. package/templates/fastify/src/{controllers → infra/http/controllers}/hello/hello.controller.ts +6 -4
  13. package/templates/fastify/src/infra/http/errors/bad-request.error.ts +11 -0
  14. package/templates/fastify/src/infra/http/errors/http-error.ts +9 -0
  15. package/templates/fastify/src/infra/http/errors/upload-validation.error.ts +15 -0
  16. package/templates/fastify/src/infra/http/plugins/error-handler.plugin.ts +45 -0
  17. package/templates/fastify/src/{plugins → infra/http/plugins}/require-upload.plugin.ts +57 -60
  18. package/templates/fastify/src/infra/http/routes/hello.routes.ts +8 -0
  19. package/templates/fastify/src/infra/presenters/error.presenter.ts +14 -0
  20. package/templates/fastify/src/{server.ts → infra/server.ts} +1 -1
  21. package/templates/fastify/test/e2e/sample-upload.jpg +0 -0
  22. package/templates/fastify/test/{e2e-setup.ts → e2e/setup.ts} +1 -1
  23. package/templates/fastify/tsconfig.json +1 -1
  24. package/templates/fastify/vitest.config.e2e.mts +1 -1
  25. package/templates/nest/src/infra/http/middlewares/upload-interceptor.ts +8 -5
  26. package/templates/fastify/src/controllers/hello/hello-multipart.controller.e2e-spec.ts +0 -32
  27. package/templates/fastify/src/errors/exceptions.ts +0 -87
  28. package/templates/fastify/src/errors/http-error-handler.ts +0 -111
  29. package/templates/fastify/src/plugins/error-handler.plugin.ts +0 -27
  30. package/templates/fastify/src/routes/hello.routes.ts +0 -8
  31. package/templates/fastify/src/utils/capitalize-word.ts +0 -3
  32. /package/templates/fastify/src/{env.ts → infra/env.ts} +0 -0
  33. /package/templates/fastify/src/{@types → infra/http/@types}/fastify-zod-type-provider.ts +0 -0
  34. /package/templates/fastify/src/{@types → infra/http/@types}/fastify.d.ts +0 -0
  35. /package/templates/fastify/src/{plugins → infra/http/plugins}/handle-swagger-multipart.plugin.ts +0 -0
  36. /package/templates/fastify/src/{routes → infra/http/routes}/index.ts +0 -0
@@ -1 +1 @@
1
- Object.defineProperty(exports,"__esModule",{value:!0});const name="@leo-h/create-nodejs-app",version="1.0.7",packageManager="pnpm@9.1.1",author="Leonardo Henrique <leonardo0507.business@gmail.com>",description="Create a modern Node.js app with TypeScript using one command.",license="MIT",keywords=["node","node.js","typescript"],bin={"create-nodejs-app":"./dist/index.js"},files=["./dist","./templates"],repository={type:"git",url:"https://github.com/Leo-Henrique/create-nodejs-app"},scripts={prepare:"husky",start:"node ./dist/index.js","start:dev":"tsx ./src/index.ts","start:dev:watch":"tsx watch ./src/index.ts",typecheck:"tsc --noEmit",lint:"eslint . --ext .ts --max-warnings 0 --cache","lint:fix":"pnpm lint --fix",format:"prettier . --write --cache","test:unit":"vitest run","test:unit:watch":"vitest","test:e2e":"vitest run --config ./vitest.config.e2e.mts","test:e2e:watch":"vitest --config ./vitest.config.e2e.mts","test:coverage":"vitest run --coverage.enabled=true",template:"tsx ./scripts/template-cli.ts",prebuild:"rimraf ./dist",build:"unbuild",prepublishOnly:"pnpm build"},dependencies={commander:"12.1.0",picocolors:"1.0.1",prompts:"2.4.2","validate-npm-package-name":"5.0.1",zod:"3.23.8"},devDependencies={"@faker-js/faker":"8.4.1","@types/node":"20.12.12","@types/prompts":"2.4.9","@types/validate-npm-package-name":"4.0.2","@typescript-eslint/eslint-plugin":"7.10.0","@typescript-eslint/parser":"7.10.0","conventional-changelog-conventionalcommits":"8.0.0",eslint:"8.57.0","eslint-config-prettier":"9.1.0","eslint-plugin-vitest":"0.4.0",husky:"9.0.11","lint-staged":"15.2.2","npm-run-all":"4.1.5",prettier:"3.2.5",rimraf:"5.0.7",tsx:"4.10.5",typescript:"5.4.5",unbuild:"2.0.0","vite-tsconfig-paths":"4.3.2",vitest:"1.6.0"},f={name,version,packageManager,author,description,license,keywords,bin,files,repository,scripts,dependencies,devDependencies};exports.author=author;exports.bin=bin;exports.default=f;exports.dependencies=dependencies;exports.description=description;exports.devDependencies=devDependencies;exports.files=files;exports.keywords=keywords;exports.license=license;exports.name=name;exports.packageManager=packageManager;exports.repository=repository;exports.scripts=scripts;exports.version=version;
1
+ Object.defineProperty(exports,"__esModule",{value:!0});const name="@leo-h/create-nodejs-app",version="1.0.8",packageManager="pnpm@9.1.1",author="Leonardo Henrique <leonardo0507.business@gmail.com>",description="Create a modern Node.js app with TypeScript using one command.",license="MIT",keywords=["node","node.js","typescript"],bin={"create-nodejs-app":"./dist/index.js"},files=["./dist","./templates"],repository={type:"git",url:"https://github.com/Leo-Henrique/create-nodejs-app"},scripts={prepare:"husky",start:"node ./dist/index.js","start:dev":"tsx ./src/index.ts","start:dev:watch":"tsx watch ./src/index.ts",typecheck:"tsc --noEmit",lint:"eslint . --ext .ts --max-warnings 0 --cache","lint:fix":"pnpm lint --fix",format:"prettier . --write --cache","test:unit":"vitest run","test:unit:watch":"vitest","test:e2e":"vitest run --config ./vitest.config.e2e.mts","test:e2e:watch":"vitest --config ./vitest.config.e2e.mts","test:coverage":"vitest run --coverage.enabled=true",template:"tsx ./scripts/template-cli.ts",prebuild:"rimraf ./dist",build:"unbuild",prepublishOnly:"pnpm build"},dependencies={commander:"12.1.0",picocolors:"1.0.1",prompts:"2.4.2","validate-npm-package-name":"5.0.1",zod:"3.23.8"},devDependencies={"@faker-js/faker":"8.4.1","@types/node":"20.12.12","@types/prompts":"2.4.9","@types/validate-npm-package-name":"4.0.2","@typescript-eslint/eslint-plugin":"7.10.0","@typescript-eslint/parser":"7.10.0","conventional-changelog-conventionalcommits":"8.0.0",eslint:"8.57.0","eslint-config-prettier":"9.1.0","eslint-plugin-vitest":"0.4.0",husky:"9.0.11","lint-staged":"15.2.2","npm-run-all":"4.1.5",prettier:"3.2.5",rimraf:"5.0.7",tsx:"4.10.5",typescript:"5.4.5",unbuild:"2.0.0","vite-tsconfig-paths":"4.3.2",vitest:"1.6.0"},f={name,version,packageManager,author,description,license,keywords,bin,files,repository,scripts,dependencies,devDependencies};exports.author=author;exports.bin=bin;exports.default=f;exports.dependencies=dependencies;exports.description=description;exports.devDependencies=devDependencies;exports.files=files;exports.keywords=keywords;exports.license=license;exports.name=name;exports.packageManager=packageManager;exports.repository=repository;exports.scripts=scripts;exports.version=version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo-h/create-nodejs-app",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "packageManager": "pnpm@9.1.1",
5
5
  "author": "Leonardo Henrique <leonardo0507.business@gmail.com>",
6
6
  "description": "Create a modern Node.js app with TypeScript using one command.",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ESNext",
4
- "module": "Commonjs",
4
+ "module": "CommonJS",
5
5
  "esModuleInterop": true,
6
6
  "resolveJsonModule": true,
7
7
  "forceConsistentCasingInFileNames": true,
@@ -33,7 +33,7 @@ const pathAliases = Object.keys(compilerOptions.paths).reduce(
33
33
 
34
34
  export default defineBuildConfig({
35
35
  outDir: "./dist",
36
- entries: ["./src/server.ts"],
36
+ entries: ["./src/infra/server.ts"],
37
37
  clean: true,
38
38
  alias: pathAliases,
39
39
  externals: Object.keys(packageJson.dependencies),
@@ -4,8 +4,8 @@
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "prepare": "husky",
7
- "start": "node ./dist/server.js",
8
- "start:dev": "tsx watch ./src/server.ts",
7
+ "start": "node ./dist/infra/server.js",
8
+ "start:dev": "tsx watch ./src/infra/server.ts",
9
9
  "typecheck": "tsc --noEmit",
10
10
  "lint": "eslint . --ext .ts --max-warnings 0 --cache",
11
11
  "lint:fix": "pnpm lint --fix",
@@ -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
+ }
@@ -10,7 +10,7 @@ import {
10
10
  serializerCompiler,
11
11
  validatorCompiler,
12
12
  } from "fastify-type-provider-zod";
13
- import { env } from "./env";
13
+ import { env } from "../env";
14
14
  import { errorHandlerPlugin } from "./plugins/error-handler.plugin";
15
15
  import { handleSwaggerMultipart } from "./plugins/handle-swagger-multipart.plugin";
16
16
  import { routes } from "./routes";
@@ -0,0 +1,34 @@
1
+ import { app } from "@/infra/http/app";
2
+ import { faker } from "@faker-js/faker";
3
+ import { lookup } from "mime-types";
4
+ import { basename, extname } from "path";
5
+ import request from "supertest";
6
+ import { describe, expect, it } from "vitest";
7
+
8
+ const SAMPLE_UPLOAD_PATH = "./test/e2e/sample-upload.jpg";
9
+
10
+ describe("[Controller] Hello multipart", () => {
11
+ it("[POST] /hello/multipart", async () => {
12
+ const fieldName = "attachment";
13
+ const description = faker.lorem.sentence();
14
+
15
+ const response = await request(app.server)
16
+ .post("/hello/multipart")
17
+ .attach(fieldName, SAMPLE_UPLOAD_PATH)
18
+ .field({ description });
19
+
20
+ expect(response.statusCode).toEqual(200);
21
+ expect(response.body).toEqual(
22
+ expect.objectContaining({
23
+ message: "Hello world!",
24
+ description,
25
+ file: expect.objectContaining({
26
+ fieldname: fieldName,
27
+ originalname: basename(SAMPLE_UPLOAD_PATH),
28
+ mimetype: lookup(extname(SAMPLE_UPLOAD_PATH)),
29
+ size: expect.any(Number),
30
+ }),
31
+ }),
32
+ );
33
+ });
34
+ });
@@ -1,4 +1,4 @@
1
- import { requireUpload } from "@/plugins/require-upload.plugin";
1
+ import { requireUpload } from "@/infra/http/plugins/require-upload.plugin";
2
2
  import { FastifyInstance } from "fastify";
3
3
  import { ZodTypeProvider } from "fastify-type-provider-zod";
4
4
  import { z } from "zod";
@@ -23,28 +23,42 @@ export async function helloMultipartController(app: FastifyInstance) {
23
23
  multipartAnotherFieldsSchema: helloMultipartControllerBodySchema,
24
24
  response: {
25
25
  200: z.object({
26
- message: z.string(),
27
- description: z.string(),
28
- file: z.custom(),
26
+ message: z.literal("Hello world!"),
27
+ description: z.string().min(2),
28
+ file: z.object({
29
+ fieldname: z.string(),
30
+ originalname: z.string(),
31
+ encoding: z.string(),
32
+ mimetype: z.string(),
33
+ size: z.number().optional(),
34
+ }),
29
35
  }),
30
36
  },
31
37
  },
32
38
  preHandler: requireUpload({
33
39
  fieldName: "attachment",
34
- allowedExtensions: ["png", "jpg", "jpeg", "webp"],
35
- limits: { fileSize: 1000000 * 15, files: 1 },
36
40
  storage: "memory",
41
+ allowedExtensions: ["jpeg", "png", "webp"],
42
+ limits: {
43
+ fileSize: 10 * 1000 * 1000, // 10MB
44
+ files: 1,
45
+ },
37
46
  }),
38
47
  handler: async (req, res) => {
39
- // eslint-disable-next-line
40
- const { buffer, ...fileInfo } = req.file;
48
+ const { fieldname, originalname, encoding, mimetype, size } = req.file;
41
49
  const { description } =
42
50
  req.body as unknown as HelloMultipartControllerBody;
43
51
 
44
52
  res.status(200).send({
45
53
  message: "Hello world!",
46
54
  description,
47
- file: fileInfo,
55
+ file: {
56
+ fieldname,
57
+ originalname,
58
+ encoding,
59
+ mimetype,
60
+ size,
61
+ },
48
62
  });
49
63
  },
50
64
  });
@@ -1,4 +1,4 @@
1
- import { app } from "@/app";
1
+ import { app } from "@/infra/http/app";
2
2
  import request from "supertest";
3
3
  import { describe, expect, it } from "vitest";
4
4
 
@@ -1,12 +1,12 @@
1
- import { BadRequestError } from "@/errors/exceptions";
2
1
  import { FastifyInstance } from "fastify";
3
2
  import { ZodTypeProvider } from "fastify-type-provider-zod";
4
3
  import { z } from "zod";
4
+ import { BadRequestError } from "../../errors/bad-request.error";
5
5
 
6
6
  const helloControllerQuerySchema = z.object({
7
7
  show: z
8
8
  .enum(["true", "false"])
9
- .transform(val => JSON.parse(val))
9
+ .transform<boolean>(val => JSON.parse(val))
10
10
  .default("true"),
11
11
  });
12
12
 
@@ -19,7 +19,9 @@ export async function helloController(app: FastifyInstance) {
19
19
  summary: "Hello world!",
20
20
  querystring: helloControllerQuerySchema,
21
21
  response: {
22
- 200: z.object({ message: z.string() }),
22
+ 200: z.object({
23
+ message: z.literal("Hello world!"),
24
+ }),
23
25
  },
24
26
  },
25
27
  handler: async (req, res) => {
@@ -27,7 +29,7 @@ export async function helloController(app: FastifyInstance) {
27
29
 
28
30
  if (!show)
29
31
  throw new BadRequestError(
30
- 'You don\'t want to display the "hello world"!',
32
+ `You don't want to display the "hello world"!`,
31
33
  );
32
34
 
33
35
  res.status(200).send({ message: "Hello world!" });
@@ -0,0 +1,11 @@
1
+ import { HttpError } from "./http-error";
2
+
3
+ export class BadRequestError extends HttpError {
4
+ public error = "BadRequestError";
5
+ public statusCode = 400;
6
+ public debug = null;
7
+
8
+ constructor(public message: string) {
9
+ super(message);
10
+ }
11
+ }
@@ -0,0 +1,9 @@
1
+ export abstract class HttpError extends Error {
2
+ public abstract error: string;
3
+ public abstract statusCode: number;
4
+ public abstract debug: unknown;
5
+
6
+ constructor(public message: string) {
7
+ super(message);
8
+ }
9
+ }
@@ -0,0 +1,15 @@
1
+ import { HttpError } from "./http-error";
2
+
3
+ export class UploadValidationError extends HttpError {
4
+ public error = "UploadValidationError";
5
+ public debug = { multerError: null };
6
+
7
+ constructor(
8
+ public statusCode = 400,
9
+ debug: object = {},
10
+ ) {
11
+ super("Os dados enviados são inválidos.");
12
+
13
+ this.debug = { ...this.debug, ...debug };
14
+ }
15
+ }
@@ -0,0 +1,45 @@
1
+ import { ValidationError } from "@/core/errors/errors";
2
+ import { env } from "@/infra/env";
3
+ import { ErrorPresenter } from "@/infra/presenters/error.presenter";
4
+ import { FastifyError, FastifyReply, FastifyRequest } from "fastify";
5
+ import { ZodError } from "zod";
6
+ import { HttpError } from "../errors/http-error";
7
+
8
+ export async function errorHandlerPlugin(
9
+ error: FastifyError,
10
+ _req: FastifyRequest,
11
+ res: FastifyReply,
12
+ ) {
13
+ console.error(error);
14
+
15
+ let httpResponse: ReturnType<typeof ErrorPresenter.toHttp> =
16
+ ErrorPresenter.toHttp(500, {
17
+ error: "InternalServerError",
18
+ message: "Desculpe, um erro inesperado ocorreu.",
19
+ debug: error.message,
20
+ });
21
+
22
+ if (error.statusCode)
23
+ httpResponse = ErrorPresenter.toHttp(error.statusCode, {
24
+ error: error.name,
25
+ message: error.message,
26
+ debug: null,
27
+ });
28
+
29
+ if (error instanceof ValidationError)
30
+ httpResponse = ErrorPresenter.toHttp(400, error);
31
+
32
+ if (error instanceof ZodError)
33
+ httpResponse = ErrorPresenter.toHttp(
34
+ 400,
35
+ new ValidationError(error.flatten().fieldErrors),
36
+ );
37
+
38
+ if (error instanceof HttpError)
39
+ httpResponse = ErrorPresenter.toHttp(error.statusCode, error);
40
+
41
+ if (env.NODE_ENV === "production" && "debug" in httpResponse)
42
+ delete httpResponse.debug;
43
+
44
+ res.status(httpResponse.statusCode).send(httpResponse);
45
+ }
@@ -2,15 +2,14 @@ import {
2
2
  FastifyZodInstance,
3
3
  FastifyZodReply,
4
4
  FastifyZodRequest,
5
- } from "@/@types/fastify-zod-type-provider";
6
- import { RequestFormatError, UploadError } from "@/errors/exceptions";
5
+ } from "@/infra/http/@types/fastify-zod-type-provider";
7
6
  import { randomUUID } from "crypto";
8
- import { HookHandlerDoneFunction } from "fastify";
9
7
  import multer, { MulterError } from "fastify-multer";
10
8
  import { Options } from "fastify-multer/lib/interfaces";
11
9
  import { extension } from "mime-types";
12
10
  import path, { extname } from "path";
13
11
  import prettyBytes from "pretty-bytes";
12
+ import { UploadValidationError } from "../errors/upload-validation.error";
14
13
 
15
14
  interface MemoryStorage {
16
15
  storage?: "memory";
@@ -82,15 +81,12 @@ export function requireUpload(
82
81
  if (allowedExtensions.includes(fileExtension)) {
83
82
  cb(null, true);
84
83
  } else {
85
- const validExtensions = allowedExtensions
86
- .map(ext => `"${ext}"`)
87
- .join(", ");
88
-
89
84
  cb(
90
- new UploadError(
91
- 400,
92
- `Formato de arquivo inválido. Use apenas extensões: ${validExtensions}.`,
93
- ),
85
+ new UploadValidationError(400, {
86
+ message: "Invalid file format.",
87
+ fieldName,
88
+ allowedExtensions,
89
+ }),
94
90
  );
95
91
  }
96
92
  },
@@ -98,74 +94,75 @@ export function requireUpload(
98
94
  const isMultipleUpload = limits.files > 1;
99
95
 
100
96
  return [
101
- async (req: FastifyZodRequest) => {
102
- if (!req.headers["content-type"]?.includes("multipart/form-data")) {
103
- throw new RequestFormatError(
104
- "A solicitação deve ser do tipo multipart/form-data.",
105
- );
106
- }
107
- },
108
- function (
97
+ async function (
109
98
  this: FastifyZodInstance,
110
99
  req: FastifyZodRequest,
111
100
  res: FastifyZodReply,
112
- done: HookHandlerDoneFunction,
113
101
  ) {
114
102
  let middleware = upload.single(fieldName);
115
103
 
116
104
  if (isMultipleUpload) middleware = upload.array(fieldName);
117
105
 
118
- middleware.bind(this)(req, res, error => {
119
- const sendError = (statusCode: number, message: string) => {
120
- done(new UploadError(statusCode, message));
121
- };
122
-
123
- if (error && error instanceof MulterError) {
124
- switch (error.code) {
125
- case "LIMIT_FILE_SIZE":
126
- return sendError(
127
- 413,
128
- `O tamanho máximo de cada arquivo é ${prettyBytes(
129
- limits.fileSize,
130
- )}.`,
131
- );
132
-
133
- case "LIMIT_FILE_COUNT":
134
- return sendError(
135
- 413,
136
- `A contagem máxima de arquivos é ${limits.files}.`,
137
- );
138
-
139
- case "LIMIT_UNEXPECTED_FILE":
140
- return sendError(
141
- 400,
142
- `O campo de arquivo '${error.field}' não é permitido.`,
143
- );
144
-
145
- default:
146
- return done(error);
106
+ await new Promise<void>((resolve, reject) => {
107
+ middleware.bind(this)(req, res, error => {
108
+ if (error && error instanceof MulterError) {
109
+ switch (error.code) {
110
+ case "LIMIT_FILE_COUNT":
111
+ reject(
112
+ new UploadValidationError(413, {
113
+ multerError: error.code,
114
+ message: error.message,
115
+ maxFileCount: limits.files!,
116
+ }),
117
+ );
118
+ break;
119
+
120
+ case "LIMIT_FILE_SIZE":
121
+ reject(
122
+ new UploadValidationError(413, {
123
+ multerError: error.code,
124
+ message: error.message,
125
+ fieldName: error.field,
126
+ maxFileSize: prettyBytes(limits.fileSize!),
127
+ }),
128
+ );
129
+ break;
130
+
131
+ default:
132
+ reject(
133
+ new UploadValidationError(
134
+ error.code === "LIMIT_UNEXPECTED_FILE" ? 400 : 413,
135
+ {
136
+ multerError: error.code,
137
+ message: error.message,
138
+ fieldName: error.field,
139
+ },
140
+ ),
141
+ );
142
+ break;
143
+ }
147
144
  }
148
- }
149
145
 
150
- if (error) return done(error);
146
+ if (error) return reject(error);
151
147
 
152
- done();
148
+ resolve();
149
+ });
153
150
  });
154
151
  },
155
152
  async (req: FastifyZodRequest) => {
156
153
  if (isRequiredUpload) {
157
154
  if (!isMultipleUpload && !req.file) {
158
- throw new UploadError(
159
- 400,
160
- `O campo '${fieldName}' é obrigatório com um arquivo.`,
161
- );
155
+ throw new UploadValidationError(400, {
156
+ multerError: null,
157
+ message: `Field "${fieldName}" is required with a file.`,
158
+ });
162
159
  }
163
160
 
164
161
  if (isMultipleUpload && (!req.files || !req.files.length)) {
165
- throw new UploadError(
166
- 400,
167
- `O campo '${fieldName}' é obrigatório com no mínimo um arquivo.`,
168
- );
162
+ throw new UploadValidationError(400, {
163
+ multerError: null,
164
+ message: `Field "${fieldName}" is required with at least 1 file.`,
165
+ });
169
166
  }
170
167
  }
171
168
 
@@ -0,0 +1,8 @@
1
+ import { helloMultipartController } from "@/infra/http/controllers/hello/hello-multipart.controller";
2
+ import { helloController } from "@/infra/http/controllers/hello/hello.controller";
3
+ import { FastifyInstance } from "fastify";
4
+
5
+ export async function helloRoutes(app: FastifyInstance) {
6
+ app.register(helloController);
7
+ app.register(helloMultipartController);
8
+ }
@@ -0,0 +1,14 @@
1
+ import { DomainError } from "@/core/errors/domain-error";
2
+
3
+ type CustomError = Pick<DomainError, "error" | "message" | "debug">;
4
+
5
+ export class ErrorPresenter {
6
+ public static toHttp(statusCode: number, error: DomainError | CustomError) {
7
+ return {
8
+ error: error.error,
9
+ message: error.message,
10
+ statusCode,
11
+ debug: error.debug,
12
+ };
13
+ }
14
+ }
@@ -1,5 +1,5 @@
1
- import { app } from "./app";
2
1
  import { env } from "./env";
2
+ import { app } from "./http/app";
3
3
 
4
4
  (async () => {
5
5
  await app.ready();
@@ -1,4 +1,4 @@
1
- import { app } from "@/app";
1
+ import { app } from "@/infra/http/app";
2
2
  import { afterAll, beforeAll } from "vitest";
3
3
 
4
4
  beforeAll(async () => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ESNext",
4
- "module": "Commonjs",
4
+ "module": "CommonJS",
5
5
  "esModuleInterop": true,
6
6
  "resolveJsonModule": true,
7
7
  "forceConsistentCasingInFileNames": true,
@@ -4,6 +4,6 @@ import defaultConfig from "./vitest.config.mjs";
4
4
  export default mergeConfig(defaultConfig, {
5
5
  test: {
6
6
  include: ["./**/*.e2e-spec.ts"],
7
- setupFiles: ["./test/e2e-setup.ts"],
7
+ setupFiles: ["./test/e2e/setup.ts"],
8
8
  },
9
9
  });
@@ -298,11 +298,14 @@ export class ExecuteUploadInterceptor implements NestInterceptor {
298
298
 
299
299
  default:
300
300
  reject(
301
- new UploadValidationError(413, {
302
- multerError: error.code,
303
- message: error.message,
304
- fieldName: error.field,
305
- }),
301
+ new UploadValidationError(
302
+ error.code === "LIMIT_UNEXPECTED_FILE" ? 400 : 413,
303
+ {
304
+ multerError: error.code,
305
+ message: error.message,
306
+ fieldName: error.field,
307
+ },
308
+ ),
306
309
  );
307
310
  break;
308
311
  }
@@ -1,32 +0,0 @@
1
- import { app } from "@/app";
2
- import { faker } from "@faker-js/faker";
3
- import { extension } from "mime-types";
4
- import request from "supertest";
5
- import { describe, expect, it } from "vitest";
6
-
7
- describe("[Controller] Hello", () => {
8
- it("[POST] /hello/multipart", async () => {
9
- const imageResponse = await fetch(faker.image.urlLoremFlickr());
10
- const imageArrayBuffer = await imageResponse.arrayBuffer();
11
- const imageContentType = imageResponse.headers.get("content-type")!;
12
- const imageExtension = extension(imageContentType) as string;
13
-
14
- const description = faker.lorem.sentence();
15
- const response = await request(app.server)
16
- .post("/hello/multipart")
17
- .attach("attachment", Buffer.from(imageArrayBuffer), {
18
- contentType: imageContentType,
19
- filename: faker.system.commonFileName(imageExtension),
20
- })
21
- .field({ description });
22
-
23
- expect(response.statusCode).toEqual(200);
24
- expect(response.body).toEqual(
25
- expect.objectContaining({
26
- message: "Hello world!",
27
- description,
28
- file: expect.any(Object),
29
- }),
30
- );
31
- });
32
- });
@@ -1,87 +0,0 @@
1
- import { capitalizeWord } from "@/utils/capitalize-word";
2
-
3
- export abstract class BaseError extends Error {
4
- public abstract error: string;
5
- public abstract statusCode: number;
6
- public debug: unknown = null;
7
-
8
- constructor(public message: string) {
9
- super(message);
10
- }
11
- }
12
-
13
- export class HTTPError extends BaseError {
14
- public error = "HTTPGenericError";
15
-
16
- constructor(
17
- public statusCode: number,
18
- text: string,
19
- ) {
20
- super(text);
21
- }
22
- }
23
-
24
- export class RequestFormatError extends BaseError {
25
- public error = "RequestFormatError";
26
- public statusCode = 406;
27
-
28
- constructor(text: string) {
29
- super(text);
30
- }
31
- }
32
-
33
- export class UnauthorizedError extends BaseError {
34
- public error = "UnauthorizedError";
35
- public statusCode = 401;
36
-
37
- constructor() {
38
- super("Não autorizado.");
39
- }
40
- }
41
-
42
- export class UploadError extends BaseError {
43
- public error = "UploadError";
44
-
45
- constructor(
46
- public statusCode: number,
47
- public message: string,
48
- ) {
49
- super(message);
50
- }
51
- }
52
-
53
- export class ResourceAlreadyExistsError extends BaseError {
54
- public error = "ResourceAlreadyExistsError";
55
- public statusCode = 409;
56
-
57
- constructor(resource: string) {
58
- super(`${capitalizeWord(resource)} já existente.`);
59
- }
60
- }
61
-
62
- export class ResourceNotFoundError extends BaseError {
63
- public error = "ResourceNotFoundError";
64
- public statusCode = 400;
65
-
66
- constructor(resource: string) {
67
- super(`${capitalizeWord(resource)} inexistente.`);
68
- }
69
- }
70
-
71
- export class InvalidCredentialsError extends BaseError {
72
- public error = "InvalidCredentialsError";
73
- public statusCode = 401;
74
-
75
- constructor() {
76
- super("Credenciais inválidas.");
77
- }
78
- }
79
-
80
- export class BadRequestError extends BaseError {
81
- public error = "BadRequestError";
82
- public statusCode = 400;
83
-
84
- constructor(public message: string) {
85
- super(message);
86
- }
87
- }
@@ -1,111 +0,0 @@
1
- import { env } from "@/env";
2
- import { FastifyError, FastifyReply } from "fastify";
3
- import { MulterError } from "fastify-multer";
4
- import { SetOptional } from "type-fest";
5
- import { ZodError } from "zod";
6
- import { fromZodError } from "zod-validation-error";
7
- import {
8
- BaseError,
9
- HTTPError,
10
- UnauthorizedError,
11
- UploadError,
12
- } from "./exceptions";
13
-
14
- export class HTTPErrorHandler {
15
- constructor(
16
- protected error: FastifyError,
17
- protected response: FastifyReply,
18
- ) {}
19
-
20
- protected send({
21
- statusCode,
22
- error,
23
- message,
24
- debug,
25
- }: SetOptional<BaseError, "debug" | "name">) {
26
- const parsedDebug = () => {
27
- if (!debug || env.NODE_ENV !== "development") return {};
28
-
29
- return { debug };
30
- };
31
-
32
- this.response
33
- .status(statusCode)
34
- .send({ statusCode, error, message, ...parsedDebug() });
35
-
36
- return true;
37
- }
38
-
39
- async customHTTPErrorHandler() {
40
- if (this.error instanceof HTTPError) return this.send(this.error);
41
- }
42
-
43
- async unknownErrorHandler() {
44
- if (this.error instanceof BaseError) return this.send(this.error);
45
-
46
- if (this.error.statusCode) {
47
- return this.send({
48
- statusCode: this.error.statusCode,
49
- error: this.error.name,
50
- message: this.error.message,
51
- });
52
- }
53
-
54
- return this.send({
55
- statusCode: 500,
56
- error: "InternalServerError",
57
- message: "Desculpe, um erro inesperado ocorreu.",
58
- debug: this.error.message,
59
- });
60
- }
61
-
62
- async JWTErrorHandler() {
63
- if (this.error.code && this.error.code.includes("_JWT_")) {
64
- const { statusCode, error, message } = new UnauthorizedError();
65
-
66
- return this.send({
67
- statusCode: this.error.statusCode || statusCode,
68
- error,
69
- message,
70
- debug: this.error.message,
71
- });
72
- }
73
- }
74
-
75
- async multerErrorHandler() {
76
- if (
77
- this.error instanceof MulterError ||
78
- this.error instanceof UploadError
79
- ) {
80
- return this.send({
81
- statusCode: (this.error as UploadError)?.statusCode || 400,
82
- error: (this.error as UploadError).error || "UploadError",
83
- message: this.error.message,
84
- });
85
- }
86
-
87
- if (this.error.message === "Multipart: Boundary not found") {
88
- return this.send({
89
- statusCode: 415,
90
- error: "UnsupportedMultipartMediaTypeError",
91
- message: "Cabeçalho multipart inválido.",
92
- });
93
- }
94
- }
95
-
96
- async zodErrorHandler() {
97
- if (this.error instanceof ZodError) {
98
- const { message } = fromZodError(this.error, {
99
- maxIssuesInMessage: 1,
100
- prefix: null,
101
- });
102
-
103
- return this.send({
104
- statusCode: 400,
105
- error: "ValidationError",
106
- message: message,
107
- debug: this.error.flatten().fieldErrors,
108
- });
109
- }
110
- }
111
- }
@@ -1,27 +0,0 @@
1
- import { HTTPErrorHandler } from "@/errors/http-error-handler";
2
- import { FastifyError, FastifyReply, FastifyRequest } from "fastify";
3
-
4
- export async function errorHandlerPlugin(
5
- error: FastifyError,
6
- _req: FastifyRequest,
7
- response: FastifyReply,
8
- ) {
9
- console.error(error);
10
-
11
- const methodNames = Object.getOwnPropertyNames(HTTPErrorHandler.prototype);
12
- const unknownErrorName = "unknownErrorHandler";
13
- const handlerNames = methodNames.filter(name => {
14
- return name.endsWith("ErrorHandler") && !name.includes(unknownErrorName);
15
- });
16
-
17
- handlerNames.push(unknownErrorName);
18
-
19
- const handlerInstance = new HTTPErrorHandler(error, response);
20
-
21
- for (const handlerName of handlerNames) {
22
- const hasError =
23
- await handlerInstance[handlerName as keyof HTTPErrorHandler]();
24
-
25
- if (hasError) break;
26
- }
27
- }
@@ -1,8 +0,0 @@
1
- import { helloMultipartController } from "@/controllers/hello/hello-multipart.controller";
2
- import { helloController } from "@/controllers/hello/hello.controller";
3
- import { FastifyInstance } from "fastify";
4
-
5
- export async function helloRoutes(app: FastifyInstance) {
6
- app.register(helloController);
7
- app.register(helloMultipartController);
8
- }
@@ -1,3 +0,0 @@
1
- export function capitalizeWord(text: string) {
2
- return text[0].toUpperCase() + text.slice(1);
3
- }
File without changes