@leo-h/create-nodejs-app 1.0.13 → 1.0.15
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 +1 -1
- package/templates/fastify/package.json +3 -3
- package/templates/fastify/src/core/domain-error.ts +17 -0
- package/templates/fastify/src/infra/http/controllers/hello/{hello-multipart.controller.e2e-spec.ts → hello-multipart.controller.integration-spec.ts} +5 -4
- package/templates/fastify/src/infra/http/controllers/hello/hello-multipart.controller.ts +1 -1
- package/templates/fastify/src/infra/http/controllers/hello/hello.controller.integration-spec.ts +38 -0
- package/templates/fastify/src/infra/http/controllers/hello/hello.controller.ts +2 -0
- package/templates/fastify/src/infra/http/errors/bad-request.error.ts +4 -4
- package/templates/fastify/src/infra/http/errors/internal-server.error.ts +13 -0
- package/templates/fastify/src/infra/http/errors/upload-validation.error.ts +12 -6
- package/templates/fastify/src/infra/http/errors/validation.error.ts +13 -0
- package/templates/fastify/src/infra/http/plugins/error-handler.plugin.ts +21 -35
- package/templates/fastify/src/infra/http/routes.ts +8 -0
- package/templates/fastify/src/infra/presenters/error.presenter.ts +4 -5
- package/templates/fastify/{vitest.config.e2e.mts → vitest.config.integration.mts} +2 -2
- package/templates/nest/package.json +4 -3
- package/templates/nest/pnpm-lock.yaml +3 -0
- package/templates/{fastify/src/core/errors → nest/src/core}/domain-error.ts +5 -0
- package/templates/nest/src/infra/http/controllers/hello/{hello-multipart.controller.e2e-spec.ts → hello-multipart.controller.integration-spec.ts} +5 -4
- package/templates/nest/src/infra/http/controllers/hello/hello-multipart.controller.ts +4 -4
- package/templates/nest/src/infra/http/controllers/hello/hello.controller.integration-spec.ts +62 -0
- package/templates/nest/src/infra/http/controllers/hello/hello.controller.ts +5 -12
- package/templates/nest/src/infra/http/errors/bad-request.error.ts +18 -0
- package/templates/nest/src/infra/http/errors/filters/all-exception.filter.ts +11 -11
- package/templates/nest/src/infra/http/errors/internal-server.error.ts +11 -8
- package/templates/nest/src/infra/http/errors/upload-validation.error.ts +16 -10
- package/templates/nest/src/infra/http/errors/validation.error.ts +21 -0
- package/templates/nest/src/infra/http/http.module.ts +0 -5
- package/templates/nest/src/infra/http/middlewares/upload-interceptor.ts +53 -16
- package/templates/nest/src/infra/http/middlewares/zod-validation-pipe.ts +1 -1
- package/templates/nest/src/infra/presenters/error.presenter.ts +2 -4
- package/templates/nest/{vitest.config.e2e.mts → vitest.config.integration.mts} +1 -1
- package/templates/fastify/src/core/errors/errors.ts +0 -9
- package/templates/fastify/src/infra/http/controllers/hello/hello.controller.e2e-spec.ts +0 -14
- package/templates/fastify/src/infra/http/errors/http-error.ts +0 -9
- package/templates/fastify/src/infra/http/routes/hello.routes.ts +0 -8
- package/templates/fastify/src/infra/http/routes/index.ts +0 -6
- package/templates/nest/src/core/errors/domain-error.ts +0 -8
- package/templates/nest/src/core/errors/validation.error.ts +0 -12
- package/templates/nest/src/infra/http/controllers/hello/hello.controller.e2e-spec.ts +0 -38
- package/templates/nest/src/infra/http/errors/filters/domain-exception.filter.ts +0 -39
- /package/templates/fastify/test/{e2e → integration}/sample-upload.jpg +0 -0
- /package/templates/fastify/test/{e2e → integration}/setup.ts +0 -0
- /package/templates/nest/test/{e2e → integration}/sample-upload.jpg +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.15",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"},d={name,version,packageManager,author,description,license,keywords,bin,files,repository,scripts,dependencies,devDependencies};exports.author=author;exports.bin=bin;exports.default=d;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.15",
|
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.",
|
@@ -13,9 +13,9 @@
|
|
13
13
|
"test:unit": "vitest run",
|
14
14
|
"test:unit:watch": "vitest",
|
15
15
|
"test:unit:coverage": "vitest run --coverage.enabled=true",
|
16
|
-
"test:
|
17
|
-
"test:
|
18
|
-
"test:
|
16
|
+
"test:integration": "vitest run --config ./vitest.config.integration.mts",
|
17
|
+
"test:integration:watch": "vitest --config ./vitest.config.integration.mts",
|
18
|
+
"test:integration:coverage": "vitest run --config ./vitest.config.integration.mts --coverage.enabled=true",
|
19
19
|
"prebuild": "rimraf ./dist",
|
20
20
|
"build": "unbuild"
|
21
21
|
},
|
@@ -0,0 +1,17 @@
|
|
1
|
+
export type DomainErrorCoreProperties = Pick<
|
2
|
+
DomainError,
|
3
|
+
"error" | "message" | "debug"
|
4
|
+
>;
|
5
|
+
|
6
|
+
export abstract class DomainError extends Error {
|
7
|
+
public abstract error: string;
|
8
|
+
public abstract debug: unknown;
|
9
|
+
|
10
|
+
constructor(public message: string) {
|
11
|
+
super(message);
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
15
|
+
export abstract class HttpError extends DomainError {
|
16
|
+
public abstract statusCode: number;
|
17
|
+
}
|
@@ -4,18 +4,19 @@ import { lookup } from "mime-types";
|
|
4
4
|
import { basename, extname } from "path";
|
5
5
|
import request from "supertest";
|
6
6
|
import { describe, expect, it } from "vitest";
|
7
|
+
import { HelloMultipartControllerBody } from "./hello-multipart.controller";
|
7
8
|
|
8
|
-
const SAMPLE_UPLOAD_PATH = "./test/
|
9
|
+
const SAMPLE_UPLOAD_PATH = "./test/integration/sample-upload.jpg";
|
9
10
|
|
10
|
-
describe("[Controller]
|
11
|
-
it("
|
11
|
+
describe("[Controller] POST /hello/multipart", () => {
|
12
|
+
it("should be able to upload a image", async () => {
|
12
13
|
const fieldName = "attachment";
|
13
14
|
const description = faker.lorem.sentence();
|
14
15
|
|
15
16
|
const response = await request(app.server)
|
16
17
|
.post("/hello/multipart")
|
17
18
|
.attach(fieldName, SAMPLE_UPLOAD_PATH)
|
18
|
-
.field({ description });
|
19
|
+
.field({ description } satisfies HelloMultipartControllerBody);
|
19
20
|
|
20
21
|
expect(response.statusCode).toEqual(200);
|
21
22
|
expect(response.body).toEqual(
|
package/templates/fastify/src/infra/http/controllers/hello/hello.controller.integration-spec.ts
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
import { app } from "@/infra/http/app";
|
2
|
+
import request from "supertest";
|
3
|
+
import { describe, expect, it } from "vitest";
|
4
|
+
import { HelloControllerQuery } from "./hello.controller";
|
5
|
+
|
6
|
+
describe("[Controller] GET /hello", () => {
|
7
|
+
it("should be able to show hello world when setting true in the query parameter", async () => {
|
8
|
+
const response = await request(app.server)
|
9
|
+
.get("/hello")
|
10
|
+
.query({ show: true } satisfies HelloControllerQuery);
|
11
|
+
|
12
|
+
expect(response.statusCode).toEqual(200);
|
13
|
+
expect(response.body).toStrictEqual({ message: "Hello world!" });
|
14
|
+
});
|
15
|
+
|
16
|
+
it("should not be able to show hello world when setting false in the query parameter", async () => {
|
17
|
+
const response = await request(app.server)
|
18
|
+
.get("/hello")
|
19
|
+
.query({ show: false } satisfies HelloControllerQuery);
|
20
|
+
|
21
|
+
expect(response.statusCode).toEqual(400);
|
22
|
+
expect(response.body.error).toEqual("BAD_REQUEST_ERROR");
|
23
|
+
});
|
24
|
+
|
25
|
+
describe("Input data validations", () => {
|
26
|
+
it("with invalid show property", async () => {
|
27
|
+
const response = await request(app.server)
|
28
|
+
.get("/hello")
|
29
|
+
.query({
|
30
|
+
// @ts-expect-error the value must be a boolean
|
31
|
+
show: 0,
|
32
|
+
} satisfies HelloControllerQuery);
|
33
|
+
|
34
|
+
expect(response.statusCode).toEqual(400);
|
35
|
+
expect(response.body.error).toEqual("VALIDATION_ERROR");
|
36
|
+
});
|
37
|
+
});
|
38
|
+
});
|
@@ -10,6 +10,8 @@ const helloControllerQuerySchema = z.object({
|
|
10
10
|
.default("true"),
|
11
11
|
});
|
12
12
|
|
13
|
+
export type HelloControllerQuery = z.infer<typeof helloControllerQuerySchema>;
|
14
|
+
|
13
15
|
export async function helloController(app: FastifyInstance) {
|
14
16
|
app.withTypeProvider<ZodTypeProvider>().route({
|
15
17
|
method: "GET",
|
@@ -1,9 +1,9 @@
|
|
1
|
-
import { HttpError } from "
|
1
|
+
import { HttpError } from "@/core/domain-error";
|
2
2
|
|
3
3
|
export class BadRequestError extends HttpError {
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
readonly error = "BAD_REQUEST_ERROR";
|
5
|
+
readonly statusCode = 400;
|
6
|
+
readonly debug = null;
|
7
7
|
|
8
8
|
constructor(public message: string) {
|
9
9
|
super(message);
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import { HttpError } from "@/core/domain-error";
|
2
|
+
|
3
|
+
const message = "Desculpe, um erro inesperado ocorreu.";
|
4
|
+
|
5
|
+
export class InternalServerError extends HttpError {
|
6
|
+
readonly statusCode = 500;
|
7
|
+
readonly error = "INTERNAL_SERVER_ERROR";
|
8
|
+
readonly message = message;
|
9
|
+
|
10
|
+
constructor(public debug: unknown) {
|
11
|
+
super(message);
|
12
|
+
}
|
13
|
+
}
|
@@ -1,15 +1,21 @@
|
|
1
|
-
import { HttpError } from "
|
1
|
+
import { HttpError } from "@/core/domain-error";
|
2
|
+
|
3
|
+
const message = "Os dados enviados são inválidos.";
|
2
4
|
|
3
5
|
export class UploadValidationError extends HttpError {
|
4
|
-
|
5
|
-
|
6
|
+
readonly error = "UPLOAD_VALIDATION_ERROR";
|
7
|
+
readonly message = message;
|
8
|
+
public debug: object;
|
6
9
|
|
7
10
|
constructor(
|
8
11
|
public statusCode = 400,
|
9
|
-
debug
|
12
|
+
debug = {},
|
10
13
|
) {
|
11
|
-
super(
|
14
|
+
super(message);
|
12
15
|
|
13
|
-
this.debug = {
|
16
|
+
this.debug = {
|
17
|
+
multerError: null,
|
18
|
+
...debug,
|
19
|
+
};
|
14
20
|
}
|
15
21
|
}
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import { HttpError } from "@/core/domain-error";
|
2
|
+
|
3
|
+
export class ValidationError extends HttpError {
|
4
|
+
readonly statusCode = 400;
|
5
|
+
readonly error = "VALIDATION_ERROR";
|
6
|
+
|
7
|
+
constructor(
|
8
|
+
public debug: unknown = null,
|
9
|
+
public message = "Os dados enviados são inválidos.",
|
10
|
+
) {
|
11
|
+
super(message);
|
12
|
+
}
|
13
|
+
}
|
@@ -1,45 +1,31 @@
|
|
1
|
-
import {
|
2
|
-
import { env } from "@/infra/env";
|
1
|
+
import { HttpError } from "@/core/domain-error";
|
3
2
|
import { ErrorPresenter } from "@/infra/presenters/error.presenter";
|
4
|
-
import {
|
3
|
+
import { FastifyReply, FastifyRequest } from "fastify";
|
5
4
|
import { ZodError } from "zod";
|
6
|
-
import {
|
5
|
+
import { InternalServerError } from "../errors/internal-server.error";
|
6
|
+
import { ValidationError } from "../errors/validation.error";
|
7
7
|
|
8
8
|
export async function errorHandlerPlugin(
|
9
|
-
error:
|
9
|
+
error: unknown,
|
10
10
|
_req: FastifyRequest,
|
11
11
|
res: FastifyReply,
|
12
12
|
) {
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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;
|
13
|
+
let httpError: HttpError;
|
14
|
+
|
15
|
+
if (error instanceof HttpError) httpError = error;
|
16
|
+
else if (error instanceof ZodError)
|
17
|
+
httpError = new ValidationError(error.flatten().fieldErrors);
|
18
|
+
else {
|
19
|
+
const debugFromUnknownError =
|
20
|
+
error && typeof error === "object" && "message" in error
|
21
|
+
? error.message
|
22
|
+
: null;
|
23
|
+
|
24
|
+
httpError = new InternalServerError(debugFromUnknownError);
|
25
|
+
console.error(httpError);
|
26
|
+
}
|
27
|
+
|
28
|
+
const httpResponse = ErrorPresenter.toHttp(httpError.statusCode, httpError);
|
43
29
|
|
44
30
|
res.status(httpResponse.statusCode).send(httpResponse);
|
45
31
|
}
|
@@ -0,0 +1,8 @@
|
|
1
|
+
import { FastifyInstance } from "fastify";
|
2
|
+
import { helloController } from "./controllers/hello/hello.controller";
|
3
|
+
import { helloMultipartController } from "./controllers/hello/hello-multipart.controller";
|
4
|
+
|
5
|
+
export async function routes(app: FastifyInstance) {
|
6
|
+
app.register(helloController);
|
7
|
+
app.register(helloMultipartController);
|
8
|
+
}
|
@@ -1,14 +1,13 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
type CustomError = Pick<DomainError, "error" | "message" | "debug">;
|
1
|
+
import { DomainErrorCoreProperties } from "@/core/domain-error";
|
2
|
+
import { env } from "../env";
|
4
3
|
|
5
4
|
export class ErrorPresenter {
|
6
|
-
public static toHttp(statusCode: number, error:
|
5
|
+
public static toHttp(statusCode: number, error: DomainErrorCoreProperties) {
|
7
6
|
return {
|
8
7
|
error: error.error,
|
9
8
|
message: error.message,
|
10
9
|
statusCode,
|
11
|
-
debug: error.debug,
|
10
|
+
debug: env.NODE_ENV !== "production" ? error.debug : null,
|
12
11
|
};
|
13
12
|
}
|
14
13
|
}
|
@@ -3,7 +3,7 @@ import defaultConfig from "./vitest.config.mjs";
|
|
3
3
|
|
4
4
|
export default mergeConfig(defaultConfig, {
|
5
5
|
test: {
|
6
|
-
include: ["./**/*.
|
7
|
-
setupFiles: ["./test/
|
6
|
+
include: ["./**/*.integration-spec.ts"],
|
7
|
+
setupFiles: ["./test/integration/setup.ts"],
|
8
8
|
},
|
9
9
|
});
|
@@ -13,9 +13,9 @@
|
|
13
13
|
"test:unit": "vitest run",
|
14
14
|
"test:unit:watch": "vitest",
|
15
15
|
"test:unit:coverage": "vitest run --coverage.enabled=true",
|
16
|
-
"test:
|
17
|
-
"test:
|
18
|
-
"test:
|
16
|
+
"test:integration": "vitest run --config ./vitest.config.integration.mts",
|
17
|
+
"test:integration:watch": "vitest --config ./vitest.config.integration.mts",
|
18
|
+
"test:integration:coverage": "vitest run --config ./vitest.config.integration.mts --coverage.enabled=true",
|
19
19
|
"build": "nest build"
|
20
20
|
},
|
21
21
|
"dependencies": {
|
@@ -30,6 +30,7 @@
|
|
30
30
|
"fastify-multer": "2.0.3",
|
31
31
|
"mime-types": "2.1.35",
|
32
32
|
"pretty-bytes": "5.6.0",
|
33
|
+
"rxjs": "7.8.1",
|
33
34
|
"zod": "3.23.8"
|
34
35
|
},
|
35
36
|
"devDependencies": {
|
@@ -9,10 +9,11 @@ import { lookup } from "mime-types";
|
|
9
9
|
import { basename, extname } from "path";
|
10
10
|
import request from "supertest";
|
11
11
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
12
|
+
import { HelloMultipartControllerBody } from "./hello-multipart.controller";
|
12
13
|
|
13
|
-
const SAMPLE_UPLOAD_PATH = "./test/
|
14
|
+
const SAMPLE_UPLOAD_PATH = "./test/integration/sample-upload.jpg";
|
14
15
|
|
15
|
-
describe("[Controller]
|
16
|
+
describe("[Controller] POST /hello/multipart", () => {
|
16
17
|
let app: NestFastifyApplication;
|
17
18
|
|
18
19
|
beforeAll(async () => {
|
@@ -32,14 +33,14 @@ describe("[Controller] Hello multipart", () => {
|
|
32
33
|
await app.close();
|
33
34
|
});
|
34
35
|
|
35
|
-
it("
|
36
|
+
it("should be able to upload a image", async () => {
|
36
37
|
const fieldName = "attachment";
|
37
38
|
const description = faker.lorem.sentence();
|
38
39
|
|
39
40
|
const response = await request(app.getHttpServer())
|
40
41
|
.post("/hello/multipart")
|
41
42
|
.attach(fieldName, SAMPLE_UPLOAD_PATH)
|
42
|
-
.field({ description });
|
43
|
+
.field({ description } satisfies HelloMultipartControllerBody);
|
43
44
|
|
44
45
|
expect(response.statusCode).toEqual(200);
|
45
46
|
expect(response.body).toEqual(
|
@@ -9,10 +9,6 @@ const helloMultipartControllerBodySchema = z.object({
|
|
9
9
|
description: z.string().min(2),
|
10
10
|
});
|
11
11
|
|
12
|
-
type HelloMultipartControllerBody = z.infer<
|
13
|
-
typeof helloMultipartControllerBodySchema
|
14
|
-
>;
|
15
|
-
|
16
12
|
const helloMultipartControllerResponseSchema = z.object({
|
17
13
|
message: z.literal("Hello world!"),
|
18
14
|
description: z.string().min(2),
|
@@ -25,6 +21,10 @@ const helloMultipartControllerResponseSchema = z.object({
|
|
25
21
|
}),
|
26
22
|
});
|
27
23
|
|
24
|
+
export type HelloMultipartControllerBody = z.infer<
|
25
|
+
typeof helloMultipartControllerBodySchema
|
26
|
+
>;
|
27
|
+
|
28
28
|
type HelloMultipartControllerResponse = z.infer<
|
29
29
|
typeof helloMultipartControllerResponseSchema
|
30
30
|
>;
|
@@ -0,0 +1,62 @@
|
|
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
|
+
import { HelloControllerQuery } from "./hello.controller";
|
10
|
+
|
11
|
+
describe("[Controller] GET /hello", () => {
|
12
|
+
let app: NestFastifyApplication;
|
13
|
+
|
14
|
+
beforeAll(async () => {
|
15
|
+
const moduleRef = await Test.createTestingModule({
|
16
|
+
imports: [AppModule],
|
17
|
+
}).compile();
|
18
|
+
|
19
|
+
app = moduleRef.createNestApplication<NestFastifyApplication>(
|
20
|
+
new FastifyAdapter(),
|
21
|
+
);
|
22
|
+
|
23
|
+
await app.init();
|
24
|
+
await app.getHttpAdapter().getInstance().ready();
|
25
|
+
});
|
26
|
+
|
27
|
+
afterAll(async () => {
|
28
|
+
await app.close();
|
29
|
+
});
|
30
|
+
|
31
|
+
it("should be able to show hello world when setting true in the query parameter", async () => {
|
32
|
+
const response = await request(app.getHttpServer())
|
33
|
+
.get("/hello")
|
34
|
+
.query({ show: true } satisfies HelloControllerQuery);
|
35
|
+
|
36
|
+
expect(response.statusCode).toEqual(200);
|
37
|
+
expect(response.body).toStrictEqual({ message: "Hello world!" });
|
38
|
+
});
|
39
|
+
|
40
|
+
it("should not be able to show hello world when setting false in the query parameter", async () => {
|
41
|
+
const response = await request(app.getHttpServer())
|
42
|
+
.get("/hello")
|
43
|
+
.query({ show: false } satisfies HelloControllerQuery);
|
44
|
+
|
45
|
+
expect(response.statusCode).toEqual(400);
|
46
|
+
expect(response.body.error).toEqual("BAD_REQUEST_ERROR");
|
47
|
+
});
|
48
|
+
|
49
|
+
describe("Input data validations", () => {
|
50
|
+
it("with invalid show property", async () => {
|
51
|
+
const response = await request(app.getHttpServer())
|
52
|
+
.get("/hello")
|
53
|
+
.query({
|
54
|
+
// @ts-expect-error the value must be a boolean
|
55
|
+
show: 0,
|
56
|
+
} satisfies HelloControllerQuery);
|
57
|
+
|
58
|
+
expect(response.statusCode).toEqual(400);
|
59
|
+
expect(response.body.error).toEqual("VALIDATION_ERROR");
|
60
|
+
});
|
61
|
+
});
|
62
|
+
});
|
@@ -1,12 +1,7 @@
|
|
1
|
-
import {
|
2
|
-
BadRequestException,
|
3
|
-
Controller,
|
4
|
-
Get,
|
5
|
-
HttpCode,
|
6
|
-
Query,
|
7
|
-
} from "@nestjs/common";
|
1
|
+
import { Controller, Get, HttpCode, Query } from "@nestjs/common";
|
8
2
|
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
9
3
|
import { z } from "zod";
|
4
|
+
import { BadRequestError } from "../../errors/bad-request.error";
|
10
5
|
import { ZodSchemaPipe } from "../../middlewares/zod-schema-pipe";
|
11
6
|
|
12
7
|
export const helloControllerQuerySchema = z.object({
|
@@ -16,12 +11,12 @@ export const helloControllerQuerySchema = z.object({
|
|
16
11
|
.transform<boolean>(val => JSON.parse(val)),
|
17
12
|
});
|
18
13
|
|
19
|
-
type HelloControllerQuery = z.infer<typeof helloControllerQuerySchema>;
|
20
|
-
|
21
14
|
const helloControllerResponseSchema = z.object({
|
22
15
|
message: z.literal("Hello world!"),
|
23
16
|
});
|
24
17
|
|
18
|
+
export type HelloControllerQuery = z.infer<typeof helloControllerQuerySchema>;
|
19
|
+
|
25
20
|
type HelloControllerResponse = z.infer<typeof helloControllerResponseSchema>;
|
26
21
|
|
27
22
|
@Controller()
|
@@ -42,9 +37,7 @@ export class HelloController {
|
|
42
37
|
const { show } = query;
|
43
38
|
|
44
39
|
if (!show)
|
45
|
-
throw new
|
46
|
-
`You don't want to display the "hello world"!`,
|
47
|
-
);
|
40
|
+
throw new BadRequestError(`You don't want to display the "hello world"!`);
|
48
41
|
|
49
42
|
return { message: "Hello world!" };
|
50
43
|
}
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import { ErrorPresenter } from "@/infra/presenters/error.presenter";
|
2
|
+
import { HttpException } from "@nestjs/common";
|
3
|
+
|
4
|
+
export class BadRequestError extends HttpException {
|
5
|
+
static readonly statusCode = 400;
|
6
|
+
static readonly error = "BAD_REQUEST_ERROR";
|
7
|
+
static readonly debug = null;
|
8
|
+
|
9
|
+
constructor(public message: string) {
|
10
|
+
super(
|
11
|
+
ErrorPresenter.toHttp(BadRequestError.statusCode, {
|
12
|
+
...BadRequestError,
|
13
|
+
message,
|
14
|
+
}),
|
15
|
+
BadRequestError.statusCode,
|
16
|
+
);
|
17
|
+
}
|
18
|
+
}
|
@@ -13,19 +13,19 @@ export class AllExceptionFilter implements ExceptionFilter {
|
|
13
13
|
const ctx = host.switchToHttp();
|
14
14
|
const response = ctx.getResponse<FastifyReply>();
|
15
15
|
|
16
|
-
|
17
|
-
exception && typeof exception === "object" && "message" in exception
|
18
|
-
? exception.message
|
19
|
-
: null;
|
16
|
+
let httpError: HttpException;
|
20
17
|
|
21
|
-
|
18
|
+
if (exception instanceof HttpException) httpError = exception;
|
19
|
+
else {
|
20
|
+
const debugFromUnknownError =
|
21
|
+
exception && typeof exception === "object" && "message" in exception
|
22
|
+
? exception.message
|
23
|
+
: null;
|
22
24
|
|
23
|
-
|
25
|
+
httpError = new InternalServerError(debugFromUnknownError);
|
26
|
+
console.error(httpError);
|
27
|
+
}
|
24
28
|
|
25
|
-
|
26
|
-
|
27
|
-
response
|
28
|
-
.status(httpException.getStatus())
|
29
|
-
.send(httpException.getResponse());
|
29
|
+
response.status(httpError.getStatus()).send(httpError.getResponse());
|
30
30
|
}
|
31
31
|
}
|
@@ -2,14 +2,17 @@ import { ErrorPresenter } from "@/infra/presenters/error.presenter";
|
|
2
2
|
import { HttpException } from "@nestjs/common";
|
3
3
|
|
4
4
|
export class InternalServerError extends HttpException {
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
error: "InternalServerError",
|
9
|
-
message: "Desculpe, um erro inesperado ocorreu.",
|
10
|
-
debug,
|
11
|
-
});
|
5
|
+
static readonly statusCode = 500;
|
6
|
+
static readonly error = "INTERNAL_SERVER_ERROR";
|
7
|
+
static readonly message = "Desculpe, um erro inesperado ocorreu.";
|
12
8
|
|
13
|
-
|
9
|
+
constructor(debug: unknown) {
|
10
|
+
super(
|
11
|
+
ErrorPresenter.toHttp(InternalServerError.statusCode, {
|
12
|
+
...InternalServerError,
|
13
|
+
debug,
|
14
|
+
}),
|
15
|
+
InternalServerError.statusCode,
|
16
|
+
);
|
14
17
|
}
|
15
18
|
}
|
@@ -2,16 +2,22 @@ import { ErrorPresenter } from "@/infra/presenters/error.presenter";
|
|
2
2
|
import { HttpException } from "@nestjs/common";
|
3
3
|
|
4
4
|
export class UploadValidationError extends HttpException {
|
5
|
-
|
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
|
-
});
|
5
|
+
static readonly error = "UPLOAD_VALIDATION_ERROR";
|
14
6
|
|
15
|
-
|
7
|
+
constructor(
|
8
|
+
public statusCode = 400,
|
9
|
+
public debug = {},
|
10
|
+
) {
|
11
|
+
super(
|
12
|
+
ErrorPresenter.toHttp(statusCode, {
|
13
|
+
...UploadValidationError,
|
14
|
+
message: "Os dados enviados são inválidos.",
|
15
|
+
debug: {
|
16
|
+
multerError: null,
|
17
|
+
...debug,
|
18
|
+
},
|
19
|
+
}),
|
20
|
+
statusCode,
|
21
|
+
);
|
16
22
|
}
|
17
23
|
}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import { ErrorPresenter } from "@/infra/presenters/error.presenter";
|
2
|
+
import { HttpException } from "@nestjs/common";
|
3
|
+
|
4
|
+
export class ValidationError extends HttpException {
|
5
|
+
static readonly statusCode = 400;
|
6
|
+
static readonly error = "VALIDATION_ERROR";
|
7
|
+
|
8
|
+
constructor(
|
9
|
+
public debug: unknown = null,
|
10
|
+
public message = "Os dados enviados são inválidos.",
|
11
|
+
) {
|
12
|
+
super(
|
13
|
+
ErrorPresenter.toHttp(ValidationError.statusCode, {
|
14
|
+
...ValidationError,
|
15
|
+
message,
|
16
|
+
debug,
|
17
|
+
}),
|
18
|
+
ValidationError.statusCode,
|
19
|
+
);
|
20
|
+
}
|
21
|
+
}
|
@@ -3,7 +3,6 @@ import { APP_FILTER } from "@nestjs/core";
|
|
3
3
|
import { HelloMultipartController } from "./controllers/hello/hello-multipart.controller";
|
4
4
|
import { HelloController } from "./controllers/hello/hello.controller";
|
5
5
|
import { AllExceptionFilter } from "./errors/filters/all-exception.filter";
|
6
|
-
import { DomainExceptionFilter } from "./errors/filters/domain-exception.filter";
|
7
6
|
import { FastifyMulterEventModule } from "./events/fastify-multer.event.module";
|
8
7
|
|
9
8
|
@Module({
|
@@ -13,10 +12,6 @@ import { FastifyMulterEventModule } from "./events/fastify-multer.event.module";
|
|
13
12
|
provide: APP_FILTER,
|
14
13
|
useClass: AllExceptionFilter,
|
15
14
|
},
|
16
|
-
{
|
17
|
-
provide: APP_FILTER,
|
18
|
-
useClass: DomainExceptionFilter,
|
19
|
-
},
|
20
15
|
],
|
21
16
|
controllers: [HelloController, HelloMultipartController],
|
22
17
|
})
|
@@ -15,9 +15,11 @@ import multer, {
|
|
15
15
|
memoryStorage,
|
16
16
|
} from "fastify-multer";
|
17
17
|
import { File, FileFilter, StorageEngine } from "fastify-multer/lib/interfaces";
|
18
|
+
import { unlink } from "fs/promises";
|
18
19
|
import { extension } from "mime-types";
|
19
20
|
import { extname } from "path";
|
20
21
|
import prettyBytes from "pretty-bytes";
|
22
|
+
import { finalize } from "rxjs";
|
21
23
|
import { ZodObject, ZodRawShape, z } from "zod";
|
22
24
|
import { UploadValidationError } from "../errors/upload-validation.error";
|
23
25
|
import { zodSchemaToSwaggerSchema } from "./zod-schema-pipe";
|
@@ -108,6 +110,12 @@ type UploadInterceptorOptions = Storage & {
|
|
108
110
|
* Infinity
|
109
111
|
*/
|
110
112
|
maxNonFileFieldSize?: number;
|
113
|
+
/**
|
114
|
+
* Removes all uploaded files after handler execution.
|
115
|
+
*
|
116
|
+
* @default true
|
117
|
+
*/
|
118
|
+
removeFilesAfterHandlerExecution?: boolean;
|
111
119
|
};
|
112
120
|
|
113
121
|
/**
|
@@ -123,6 +131,7 @@ export function UploadInterceptor(options: UploadInterceptorOptions) {
|
|
123
131
|
maxFileSize,
|
124
132
|
nonFileFieldsZodSchema,
|
125
133
|
maxNonFileFieldSize,
|
134
|
+
removeFilesAfterHandlerExecution,
|
126
135
|
...restOptions
|
127
136
|
} = {
|
128
137
|
destination: "./tmp",
|
@@ -132,6 +141,7 @@ export function UploadInterceptor(options: UploadInterceptorOptions) {
|
|
132
141
|
maxFileSize: 50,
|
133
142
|
nonFileFieldsZodSchema: z.object({}),
|
134
143
|
maxNonFileFieldSize: 1,
|
144
|
+
removeFilesAfterHandlerExecution: true,
|
135
145
|
...options,
|
136
146
|
};
|
137
147
|
const nonFileFieldsSwaggerSchema = zodSchemaToSwaggerSchema(
|
@@ -211,24 +221,28 @@ export function UploadInterceptor(options: UploadInterceptorOptions) {
|
|
211
221
|
};
|
212
222
|
|
213
223
|
const megabytesToBytes = (mb: number) => mb * 1000 * 1000;
|
224
|
+
const interceptors: NestInterceptor[] = [
|
225
|
+
new ExecuteUploadInterceptor({
|
226
|
+
multerInstance: multer({
|
227
|
+
storage: getMulterStorage(),
|
228
|
+
fileFilter: setMulterFileFilter,
|
229
|
+
limits: {
|
230
|
+
files: maxFileCount,
|
231
|
+
fileSize: megabytesToBytes(maxFileSize),
|
232
|
+
fields: Infinity,
|
233
|
+
fieldSize: megabytesToBytes(maxNonFileFieldSize),
|
234
|
+
},
|
235
|
+
}),
|
236
|
+
fieldName,
|
237
|
+
required,
|
238
|
+
}),
|
239
|
+
];
|
240
|
+
|
241
|
+
if (removeFilesAfterHandlerExecution)
|
242
|
+
interceptors.push(new RemoveUploadsInterceptor());
|
214
243
|
|
215
244
|
return applyDecorators(
|
216
|
-
UseInterceptors(
|
217
|
-
new ExecuteUploadInterceptor({
|
218
|
-
multerInstance: multer({
|
219
|
-
storage: getMulterStorage(),
|
220
|
-
fileFilter: setMulterFileFilter,
|
221
|
-
limits: {
|
222
|
-
files: maxFileCount,
|
223
|
-
fileSize: megabytesToBytes(maxFileSize),
|
224
|
-
fields: Object.keys(nonFileFieldsZodSchema.shape).length,
|
225
|
-
fieldSize: megabytesToBytes(maxNonFileFieldSize),
|
226
|
-
},
|
227
|
-
}),
|
228
|
-
fieldName,
|
229
|
-
required,
|
230
|
-
}),
|
231
|
-
),
|
245
|
+
UseInterceptors(...interceptors),
|
232
246
|
ApiConsumes("multipart/form-data"),
|
233
247
|
ApiBody({
|
234
248
|
schema: {
|
@@ -366,3 +380,26 @@ export class ExecuteUploadInterceptor implements NestInterceptor {
|
|
366
380
|
return next.handle();
|
367
381
|
}
|
368
382
|
}
|
383
|
+
|
384
|
+
export class RemoveUploadsInterceptor implements NestInterceptor {
|
385
|
+
intercept(context: ExecutionContext, next: CallHandler) {
|
386
|
+
const ctx = context.switchToHttp();
|
387
|
+
const request = ctx.getRequest<FastifyRequestWithFile>();
|
388
|
+
|
389
|
+
return next.handle().pipe(
|
390
|
+
finalize(async () => {
|
391
|
+
const { file, files } = request;
|
392
|
+
|
393
|
+
if (file && file.path) await unlink(file.path);
|
394
|
+
|
395
|
+
if (files) {
|
396
|
+
await Promise.all(
|
397
|
+
files.map(async file => {
|
398
|
+
if (file.path) return unlink(file.path);
|
399
|
+
}),
|
400
|
+
);
|
401
|
+
}
|
402
|
+
}),
|
403
|
+
);
|
404
|
+
}
|
405
|
+
}
|
@@ -1,6 +1,6 @@
|
|
1
|
-
import { ValidationError } from "@/core/errors/validation.error";
|
2
1
|
import { ArgumentMetadata, PipeTransform } from "@nestjs/common";
|
3
2
|
import { ZodError, ZodObject, ZodRawShape, ZodType } from "zod";
|
3
|
+
import { ValidationError } from "../errors/validation.error";
|
4
4
|
|
5
5
|
export interface ZodValidationPipeSchemas {
|
6
6
|
routeParams?: ZodObject<ZodRawShape>;
|
@@ -1,10 +1,8 @@
|
|
1
|
-
import {
|
1
|
+
import { DomainErrorCoreProperties } from "@/core/domain-error";
|
2
2
|
import { env } from "../env";
|
3
3
|
|
4
|
-
type CustomError = Pick<DomainError, "error" | "message" | "debug">;
|
5
|
-
|
6
4
|
export class ErrorPresenter {
|
7
|
-
public static toHttp(statusCode: number, error:
|
5
|
+
public static toHttp(statusCode: number, error: DomainErrorCoreProperties) {
|
8
6
|
return {
|
9
7
|
error: error.error,
|
10
8
|
message: error.message,
|
@@ -1,14 +0,0 @@
|
|
1
|
-
import { app } from "@/infra/http/app";
|
2
|
-
import request from "supertest";
|
3
|
-
import { describe, expect, it } from "vitest";
|
4
|
-
|
5
|
-
describe("[Controller] Hello", () => {
|
6
|
-
it("[GET] /hello", async () => {
|
7
|
-
const response = await request(app.server)
|
8
|
-
.get("/hello")
|
9
|
-
.query({ show: true });
|
10
|
-
|
11
|
-
expect(response.statusCode).toEqual(200);
|
12
|
-
expect(response.body).toEqual({ message: "Hello world!" });
|
13
|
-
});
|
14
|
-
});
|
@@ -1,8 +0,0 @@
|
|
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
|
-
}
|
@@ -1,12 +0,0 @@
|
|
1
|
-
import { DomainError } from "./domain-error";
|
2
|
-
|
3
|
-
export class ValidationError extends DomainError {
|
4
|
-
public error = "ValidationError" as const;
|
5
|
-
public debug: object | null;
|
6
|
-
|
7
|
-
constructor(debug?: object) {
|
8
|
-
super("Os dados recebidos são inválidos.");
|
9
|
-
|
10
|
-
this.debug = debug ?? null;
|
11
|
-
}
|
12
|
-
}
|
@@ -1,38 +0,0 @@
|
|
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
|
-
});
|
@@ -1,39 +0,0 @@
|
|
1
|
-
import { DomainError } from "@/core/errors/domain-error";
|
2
|
-
import { ValidationError } from "@/core/errors/validation.error";
|
3
|
-
import { ErrorPresenter } from "@/infra/presenters/error.presenter";
|
4
|
-
import {
|
5
|
-
ArgumentsHost,
|
6
|
-
BadRequestException,
|
7
|
-
Catch,
|
8
|
-
ExceptionFilter,
|
9
|
-
HttpException,
|
10
|
-
} from "@nestjs/common";
|
11
|
-
import { FastifyReply } from "fastify";
|
12
|
-
import { InternalServerError } from "../internal-server.error";
|
13
|
-
|
14
|
-
@Catch(DomainError)
|
15
|
-
export class DomainExceptionFilter implements ExceptionFilter {
|
16
|
-
catch(exception: DomainError, host: ArgumentsHost): void {
|
17
|
-
const ctx = host.switchToHttp();
|
18
|
-
const response = ctx.getResponse<FastifyReply>();
|
19
|
-
|
20
|
-
let httpException: HttpException;
|
21
|
-
|
22
|
-
switch (exception.constructor) {
|
23
|
-
case ValidationError:
|
24
|
-
httpException = new BadRequestException(
|
25
|
-
ErrorPresenter.toHttp(400, exception),
|
26
|
-
);
|
27
|
-
break;
|
28
|
-
|
29
|
-
default:
|
30
|
-
httpException = new InternalServerError(exception.message);
|
31
|
-
}
|
32
|
-
|
33
|
-
console.error(exception);
|
34
|
-
|
35
|
-
response
|
36
|
-
.status(httpException.getStatus())
|
37
|
-
.send(httpException.getResponse());
|
38
|
-
}
|
39
|
-
}
|
File without changes
|
File without changes
|
File without changes
|