@leo-h/create-nodejs-app 1.0.6 → 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.
- package/dist/package.json.js +1 -1
- package/package.json +2 -2
- package/templates/clean/package.json +1 -1
- package/templates/clean/src/env.ts +5 -5
- package/templates/clean/tsconfig.json +1 -1
- package/templates/clean/vitest.config.mts +2 -2
- package/templates/fastify/build.config.ts +1 -1
- package/templates/fastify/package.json +3 -3
- package/templates/fastify/src/core/errors/domain-error.ts +8 -0
- package/templates/fastify/src/core/errors/errors.ts +9 -0
- package/templates/fastify/src/{env.ts → infra/env.ts} +5 -5
- package/templates/fastify/src/{app.ts → infra/http/app.ts} +3 -3
- package/templates/fastify/src/infra/http/controllers/hello/hello-multipart.controller.e2e-spec.ts +34 -0
- package/templates/fastify/src/{controllers → infra/http/controllers}/hello/hello-multipart.controller.ts +23 -9
- package/templates/fastify/src/{controllers → infra/http/controllers}/hello/hello.controller.e2e-spec.ts +1 -1
- package/templates/fastify/src/{controllers → infra/http/controllers}/hello/hello.controller.ts +6 -4
- package/templates/fastify/src/infra/http/errors/bad-request.error.ts +11 -0
- package/templates/fastify/src/infra/http/errors/http-error.ts +9 -0
- package/templates/fastify/src/infra/http/errors/upload-validation.error.ts +15 -0
- package/templates/fastify/src/infra/http/plugins/error-handler.plugin.ts +45 -0
- package/templates/fastify/src/{plugins → infra/http/plugins}/require-upload.plugin.ts +57 -60
- package/templates/fastify/src/infra/http/routes/hello.routes.ts +8 -0
- package/templates/fastify/src/infra/presenters/error.presenter.ts +14 -0
- package/templates/fastify/src/{server.ts → infra/server.ts} +1 -1
- package/templates/fastify/test/e2e/sample-upload.jpg +0 -0
- package/templates/fastify/test/{e2e-setup.ts → e2e/setup.ts} +1 -1
- package/templates/fastify/tsconfig.json +1 -1
- package/templates/fastify/vitest.config.e2e.mts +1 -1
- package/templates/fastify/vitest.config.mts +2 -2
- package/templates/nest/src/infra/http/middlewares/upload-interceptor.ts +8 -5
- package/templates/fastify/src/controllers/hello/hello-multipart.controller.e2e-spec.ts +0 -32
- package/templates/fastify/src/errors/exceptions.ts +0 -87
- package/templates/fastify/src/errors/http-error-handler.ts +0 -111
- package/templates/fastify/src/plugins/error-handler.plugin.ts +0 -27
- package/templates/fastify/src/routes/hello.routes.ts +0 -8
- package/templates/fastify/src/utils/capitalize-word.ts +0 -3
- /package/templates/fastify/src/{@types → infra/http/@types}/fastify-zod-type-provider.ts +0 -0
- /package/templates/fastify/src/{@types → infra/http/@types}/fastify.d.ts +0 -0
- /package/templates/fastify/src/{plugins → infra/http/plugins}/handle-swagger-multipart.plugin.ts +0 -0
- /package/templates/fastify/src/{routes → infra/http/routes}/index.ts +0 -0
package/dist/package.json.js
CHANGED
@@ -1 +1 @@
|
|
1
|
-
Object.defineProperty(exports,"__esModule",{value:!0});const name="@leo-h/create-nodejs-app",version="1.0.
|
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.
|
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.",
|
@@ -34,7 +34,7 @@
|
|
34
34
|
"test:unit:watch": "vitest",
|
35
35
|
"test:e2e": "vitest run --config ./vitest.config.e2e.mts",
|
36
36
|
"test:e2e:watch": "vitest --config ./vitest.config.e2e.mts",
|
37
|
-
"test:coverage": "vitest run --coverage.enabled=true
|
37
|
+
"test:coverage": "vitest run --coverage.enabled=true",
|
38
38
|
"template": "tsx ./scripts/template-cli.ts",
|
39
39
|
"prebuild": "rimraf ./dist",
|
40
40
|
"build": "unbuild",
|
@@ -12,7 +12,7 @@
|
|
12
12
|
"format": "prettier . --write --cache",
|
13
13
|
"test:unit": "vitest run",
|
14
14
|
"test:unit:watch": "vitest",
|
15
|
-
"test:coverage": "vitest run --coverage.enabled=true
|
15
|
+
"test:coverage": "vitest run --coverage.enabled=true",
|
16
16
|
"prebuild": "rimraf ./dist",
|
17
17
|
"build": "unbuild"
|
18
18
|
},
|
@@ -1,12 +1,12 @@
|
|
1
|
-
import "
|
1
|
+
import packageJson from "@/../package.json";
|
2
|
+
import { config } from "dotenv";
|
2
3
|
import { z } from "zod";
|
3
4
|
|
5
|
+
config({ override: true });
|
6
|
+
|
4
7
|
const schema = z.object({
|
5
8
|
NODE_ENV: z.enum(["test", "development", "production"]),
|
6
|
-
APP_NAME: z
|
7
|
-
.string()
|
8
|
-
.nullable()
|
9
|
-
.default(process.env.npm_package_name ?? null),
|
9
|
+
APP_NAME: z.string().default(packageJson.name),
|
10
10
|
});
|
11
11
|
|
12
12
|
const parsedEnv = schema.safeParse(process.env);
|
@@ -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",
|
@@ -14,7 +14,7 @@
|
|
14
14
|
"test:unit:watch": "vitest",
|
15
15
|
"test:e2e": "vitest run --config ./vitest.config.e2e.mts",
|
16
16
|
"test:e2e:watch": "vitest --config ./vitest.config.e2e.mts",
|
17
|
-
"test:coverage": "vitest run --coverage.enabled=true
|
17
|
+
"test:coverage": "vitest run --coverage.enabled=true",
|
18
18
|
"prebuild": "rimraf ./dist",
|
19
19
|
"build": "unbuild"
|
20
20
|
},
|
@@ -1,12 +1,12 @@
|
|
1
|
-
import "
|
1
|
+
import packageJson from "@/../package.json";
|
2
|
+
import { config } from "dotenv";
|
2
3
|
import { z } from "zod";
|
3
4
|
|
5
|
+
config({ override: true });
|
6
|
+
|
4
7
|
const schema = z.object({
|
5
8
|
NODE_ENV: z.enum(["test", "development", "production"]),
|
6
|
-
API_NAME: z
|
7
|
-
.string()
|
8
|
-
.nullable()
|
9
|
-
.default(process.env.npm_package_name ?? null),
|
9
|
+
API_NAME: z.string().default(packageJson.name),
|
10
10
|
API_PORT: z.coerce.number().default(3333),
|
11
11
|
API_ACCESS_PERMISSION_CLIENT_SIDE: z.string().default("*"),
|
12
12
|
});
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import packageJson from "@/../package.json";
|
1
2
|
import fastifyCookie from "@fastify/cookie";
|
2
3
|
import fastifyCors from "@fastify/cors";
|
3
4
|
import fastifySwagger from "@fastify/swagger";
|
@@ -9,7 +10,7 @@ import {
|
|
9
10
|
serializerCompiler,
|
10
11
|
validatorCompiler,
|
11
12
|
} from "fastify-type-provider-zod";
|
12
|
-
import { env } from "
|
13
|
+
import { env } from "../env";
|
13
14
|
import { errorHandlerPlugin } from "./plugins/error-handler.plugin";
|
14
15
|
import { handleSwaggerMultipart } from "./plugins/handle-swagger-multipart.plugin";
|
15
16
|
import { routes } from "./routes";
|
@@ -23,8 +24,7 @@ app.register(fastifySwagger, {
|
|
23
24
|
openapi: {
|
24
25
|
info: {
|
25
26
|
title: env.API_NAME ?? "",
|
26
|
-
|
27
|
-
version: process.env.npm_package_version ?? "",
|
27
|
+
version: packageJson.version,
|
28
28
|
},
|
29
29
|
servers: [],
|
30
30
|
},
|
package/templates/fastify/src/infra/http/controllers/hello/hello-multipart.controller.e2e-spec.ts
ADDED
@@ -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.
|
27
|
-
description: z.string(),
|
28
|
-
file: z.
|
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
|
-
|
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:
|
55
|
+
file: {
|
56
|
+
fieldname,
|
57
|
+
originalname,
|
58
|
+
encoding,
|
59
|
+
mimetype,
|
60
|
+
size,
|
61
|
+
},
|
48
62
|
});
|
49
63
|
},
|
50
64
|
});
|
package/templates/fastify/src/{controllers → infra/http/controllers}/hello/hello.controller.ts
RENAMED
@@ -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({
|
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
|
-
|
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,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 "
|
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
|
91
|
-
|
92
|
-
|
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 (
|
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
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
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
|
-
|
146
|
+
if (error) return reject(error);
|
151
147
|
|
152
|
-
|
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
|
159
|
-
|
160
|
-
`
|
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
|
166
|
-
|
167
|
-
`
|
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
|
+
}
|
Binary file
|
@@ -298,11 +298,14 @@ export class ExecuteUploadInterceptor implements NestInterceptor {
|
|
298
298
|
|
299
299
|
default:
|
300
300
|
reject(
|
301
|
-
new UploadValidationError(
|
302
|
-
|
303
|
-
|
304
|
-
|
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
|
-
}
|
File without changes
|
File without changes
|
/package/templates/fastify/src/{plugins → infra/http/plugins}/handle-swagger-multipart.plugin.ts
RENAMED
File without changes
|
File without changes
|