@leo-h/create-nodejs-app 1.0.29 → 1.0.31
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/package.json +14 -14
- package/templates/clean/package.json +12 -12
- package/templates/clean/pnpm-lock.yaml +222 -494
- package/templates/fastify/.env.example +1 -4
- package/templates/fastify/.lintstagedrc.json +2 -2
- package/templates/fastify/.prettierignore +0 -2
- package/templates/fastify/.swcrc +19 -0
- package/templates/fastify/gitignore +8 -111
- package/templates/fastify/package.json +28 -40
- package/templates/fastify/pnpm-lock.yaml +1704 -3725
- package/templates/fastify/src/core/create-controller-response-schema.ts +74 -0
- package/templates/fastify/src/core/schema.ts +17 -0
- package/templates/fastify/src/env.ts +49 -0
- package/templates/fastify/src/{infra/http → http}/app.ts +13 -19
- package/templates/fastify/src/http/controllers/hello.controller.spec.ts +74 -0
- package/templates/fastify/src/http/controllers/hello.controller.ts +48 -0
- package/templates/fastify/src/http/errors.ts +89 -0
- package/templates/fastify/src/http/plugins/error-handler.plugin.ts +29 -0
- package/templates/fastify/src/http/plugins/not-found-error-handler.plugin.ts +5 -0
- package/templates/fastify/src/http/plugins/routes.plugin.ts +53 -0
- package/templates/fastify/src/http/plugins/swagger-ui.plugin.ts +32 -0
- package/templates/fastify/src/infra/http-server.ts +16 -0
- package/templates/fastify/test/integration/create-test-request.ts +30 -0
- package/templates/fastify/vitest.config.integration.mts +3 -2
- package/templates/fastify/vitest.config.mts +13 -2
- package/templates/nest/package.json +19 -19
- package/templates/nest/pnpm-lock.yaml +369 -377
- package/templates/react-vite/package.json +20 -20
- package/templates/react-vite/pnpm-lock.yaml +458 -972
- package/templates/fastify/build.config.ts +0 -56
- package/templates/fastify/src/core/domain-error.ts +0 -17
- package/templates/fastify/src/infra/env.ts +0 -22
- package/templates/fastify/src/infra/http/@types/fastify.d.ts +0 -16
- package/templates/fastify/src/infra/http/controllers/hello/hello-multipart.controller.integration-spec.ts +0 -35
- package/templates/fastify/src/infra/http/controllers/hello/hello-multipart.controller.ts +0 -65
- package/templates/fastify/src/infra/http/controllers/hello/hello.controller.integration-spec.ts +0 -38
- package/templates/fastify/src/infra/http/controllers/hello/hello.controller.ts +0 -40
- package/templates/fastify/src/infra/http/errors/bad-request.error.ts +0 -11
- package/templates/fastify/src/infra/http/errors/internal-server.error.ts +0 -13
- package/templates/fastify/src/infra/http/errors/upload-validation.error.ts +0 -21
- package/templates/fastify/src/infra/http/errors/validation.error.ts +0 -13
- package/templates/fastify/src/infra/http/plugins/error-handler.plugin.ts +0 -31
- package/templates/fastify/src/infra/http/plugins/handle-swagger-multipart.plugin.ts +0 -96
- package/templates/fastify/src/infra/http/plugins/require-upload.plugin.ts +0 -197
- package/templates/fastify/src/infra/http/routes.ts +0 -8
- package/templates/fastify/src/infra/presenters/error.presenter.ts +0 -13
- package/templates/fastify/src/infra/server.ts +0 -12
- package/templates/fastify/test/integration/sample-upload.jpg +0 -0
- package/templates/fastify/test/integration/setup.ts +0 -10
- package/templates/fastify/src/{infra/http/@types/fastify-zod-type-provider.ts → @types/fastify.ts} +1 -1
@@ -1,56 +0,0 @@
|
|
1
|
-
import { readdirSync } from "fs";
|
2
|
-
import { resolve } from "path";
|
3
|
-
import { BuildOptions, defineBuildConfig } from "unbuild";
|
4
|
-
import packageJson from "./package.json";
|
5
|
-
import { compilerOptions } from "./tsconfig.json";
|
6
|
-
|
7
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
8
|
-
const getAllFiles = (path: string) => {
|
9
|
-
return readdirSync(path, {
|
10
|
-
recursive: true,
|
11
|
-
encoding: "utf-8",
|
12
|
-
})
|
13
|
-
.filter(reading => reading.endsWith(".ts"))
|
14
|
-
.map(filename => `${path}/${filename}`);
|
15
|
-
};
|
16
|
-
|
17
|
-
const pathAliases = Object.keys(compilerOptions.paths).reduce(
|
18
|
-
(resolvedAliases, alias) => {
|
19
|
-
const unbuildAlias = alias.replace("/*", "");
|
20
|
-
const unbuildPath = compilerOptions.paths[
|
21
|
-
alias as keyof typeof compilerOptions.paths
|
22
|
-
][0].replace("/*", "");
|
23
|
-
|
24
|
-
resolvedAliases[unbuildAlias] = resolve(
|
25
|
-
compilerOptions.baseUrl,
|
26
|
-
unbuildPath,
|
27
|
-
);
|
28
|
-
|
29
|
-
return resolvedAliases;
|
30
|
-
},
|
31
|
-
{} as BuildOptions["alias"],
|
32
|
-
);
|
33
|
-
|
34
|
-
export default defineBuildConfig({
|
35
|
-
outDir: "./dist",
|
36
|
-
entries: ["./src/infra/server.ts"],
|
37
|
-
clean: true,
|
38
|
-
alias: pathAliases,
|
39
|
-
externals: Object.keys(packageJson.dependencies),
|
40
|
-
rollup: {
|
41
|
-
emitCJS: true,
|
42
|
-
cjsBridge: true,
|
43
|
-
inlineDependencies: true,
|
44
|
-
output: {
|
45
|
-
format: "cjs",
|
46
|
-
entryFileNames: "[name].js",
|
47
|
-
preserveModules: true,
|
48
|
-
strict: false,
|
49
|
-
exports: "named",
|
50
|
-
},
|
51
|
-
esbuild: {
|
52
|
-
minifySyntax: true,
|
53
|
-
treeShaking: true,
|
54
|
-
},
|
55
|
-
},
|
56
|
-
});
|
@@ -1,17 +0,0 @@
|
|
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
|
-
}
|
@@ -1,22 +0,0 @@
|
|
1
|
-
import packageJson from "@/../package.json";
|
2
|
-
import { config } from "dotenv";
|
3
|
-
import { z } from "zod";
|
4
|
-
|
5
|
-
config({ override: true });
|
6
|
-
|
7
|
-
const schema = z.object({
|
8
|
-
NODE_ENV: z.enum(["test", "development", "production"]),
|
9
|
-
API_NAME: z.string().default(packageJson.name),
|
10
|
-
API_PORT: z.coerce.number().default(3333),
|
11
|
-
API_ACCESS_PERMISSION_CLIENT_SIDE: z.string().default("*"),
|
12
|
-
});
|
13
|
-
|
14
|
-
const parsedEnv = schema.safeParse(process.env);
|
15
|
-
|
16
|
-
if (!parsedEnv.success) {
|
17
|
-
console.error(parsedEnv.error.flatten().fieldErrors);
|
18
|
-
|
19
|
-
throw new Error("Invalid environment variables.");
|
20
|
-
}
|
21
|
-
|
22
|
-
export const env = parsedEnv.data;
|
@@ -1,16 +0,0 @@
|
|
1
|
-
import { File } from "fastify-multer/lib/interfaces";
|
2
|
-
import { z } from "zod";
|
3
|
-
|
4
|
-
export type Upload<Base> = Omit<Base, "fields" | "type">;
|
5
|
-
|
6
|
-
declare module "fastify" {
|
7
|
-
interface FastifyRequest {
|
8
|
-
file: File;
|
9
|
-
files: File[];
|
10
|
-
}
|
11
|
-
|
12
|
-
interface FastifySchema {
|
13
|
-
multipartFileFields?: string[];
|
14
|
-
multipartAnotherFieldsSchema?: z.ZodObject<z.ZodRawShape>;
|
15
|
-
}
|
16
|
-
}
|
@@ -1,35 +0,0 @@
|
|
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
|
-
import { HelloMultipartControllerBody } from "./hello-multipart.controller";
|
8
|
-
|
9
|
-
const SAMPLE_UPLOAD_PATH = "./test/integration/sample-upload.jpg";
|
10
|
-
|
11
|
-
describe("[Controller] POST /hello/multipart", () => {
|
12
|
-
it("should be able to upload a image", async () => {
|
13
|
-
const fieldName = "attachment";
|
14
|
-
const description = faker.lorem.sentence();
|
15
|
-
|
16
|
-
const response = await request(app.server)
|
17
|
-
.post("/hello/multipart")
|
18
|
-
.attach(fieldName, SAMPLE_UPLOAD_PATH)
|
19
|
-
.field({ description } satisfies HelloMultipartControllerBody);
|
20
|
-
|
21
|
-
expect(response.statusCode).toEqual(200);
|
22
|
-
expect(response.body).toEqual(
|
23
|
-
expect.objectContaining({
|
24
|
-
message: "Hello world!",
|
25
|
-
description,
|
26
|
-
file: expect.objectContaining({
|
27
|
-
fieldname: fieldName,
|
28
|
-
originalname: basename(SAMPLE_UPLOAD_PATH),
|
29
|
-
mimetype: lookup(extname(SAMPLE_UPLOAD_PATH)),
|
30
|
-
size: expect.any(Number),
|
31
|
-
}),
|
32
|
-
}),
|
33
|
-
);
|
34
|
-
});
|
35
|
-
});
|
@@ -1,65 +0,0 @@
|
|
1
|
-
import { requireUpload } from "@/infra/http/plugins/require-upload.plugin";
|
2
|
-
import { FastifyInstance } from "fastify";
|
3
|
-
import { ZodTypeProvider } from "fastify-type-provider-zod";
|
4
|
-
import { z } from "zod";
|
5
|
-
|
6
|
-
const helloMultipartControllerBodySchema = z.object({
|
7
|
-
description: z.string().min(2),
|
8
|
-
});
|
9
|
-
|
10
|
-
export type HelloMultipartControllerBody = z.infer<
|
11
|
-
typeof helloMultipartControllerBodySchema
|
12
|
-
>;
|
13
|
-
|
14
|
-
export async function helloMultipartController(app: FastifyInstance) {
|
15
|
-
app.withTypeProvider<ZodTypeProvider>().route({
|
16
|
-
method: "POST",
|
17
|
-
url: "/hello/multipart",
|
18
|
-
schema: {
|
19
|
-
tags: ["Hello"],
|
20
|
-
summary: "Hello world with multipart/form-data content-type!",
|
21
|
-
consumes: ["multipart/form-data"],
|
22
|
-
multipartFileFields: ["attachment"],
|
23
|
-
multipartAnotherFieldsSchema: helloMultipartControllerBodySchema,
|
24
|
-
response: {
|
25
|
-
200: z.object({
|
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
|
-
}),
|
35
|
-
}),
|
36
|
-
},
|
37
|
-
},
|
38
|
-
preHandler: requireUpload({
|
39
|
-
fieldName: "attachment",
|
40
|
-
storage: "memory",
|
41
|
-
allowedExtensions: ["jpeg", "png", "webp"],
|
42
|
-
limits: {
|
43
|
-
fileSize: 10 * 1000 * 1000, // 10MB
|
44
|
-
files: 1,
|
45
|
-
},
|
46
|
-
}),
|
47
|
-
handler: async (req, res) => {
|
48
|
-
const { fieldname, originalname, encoding, mimetype, size } = req.file;
|
49
|
-
const { description } =
|
50
|
-
req.body as unknown as HelloMultipartControllerBody;
|
51
|
-
|
52
|
-
res.status(200).send({
|
53
|
-
message: "Hello world!",
|
54
|
-
description,
|
55
|
-
file: {
|
56
|
-
fieldname,
|
57
|
-
originalname,
|
58
|
-
encoding,
|
59
|
-
mimetype,
|
60
|
-
size,
|
61
|
-
},
|
62
|
-
});
|
63
|
-
},
|
64
|
-
});
|
65
|
-
}
|
package/templates/fastify/src/infra/http/controllers/hello/hello.controller.integration-spec.ts
DELETED
@@ -1,38 +0,0 @@
|
|
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
|
-
});
|
@@ -1,40 +0,0 @@
|
|
1
|
-
import { FastifyInstance } from "fastify";
|
2
|
-
import { ZodTypeProvider } from "fastify-type-provider-zod";
|
3
|
-
import { z } from "zod";
|
4
|
-
import { BadRequestError } from "../../errors/bad-request.error";
|
5
|
-
|
6
|
-
const helloControllerQuerySchema = z.object({
|
7
|
-
show: z
|
8
|
-
.enum(["true", "false"])
|
9
|
-
.transform<boolean>(val => JSON.parse(val))
|
10
|
-
.default("true"),
|
11
|
-
});
|
12
|
-
|
13
|
-
export type HelloControllerQuery = z.infer<typeof helloControllerQuerySchema>;
|
14
|
-
|
15
|
-
export async function helloController(app: FastifyInstance) {
|
16
|
-
app.withTypeProvider<ZodTypeProvider>().route({
|
17
|
-
method: "GET",
|
18
|
-
url: "/hello",
|
19
|
-
schema: {
|
20
|
-
tags: ["Hello"],
|
21
|
-
summary: "Hello world!",
|
22
|
-
querystring: helloControllerQuerySchema,
|
23
|
-
response: {
|
24
|
-
200: z.object({
|
25
|
-
message: z.literal("Hello world!"),
|
26
|
-
}),
|
27
|
-
},
|
28
|
-
},
|
29
|
-
handler: async (req, res) => {
|
30
|
-
const { show } = req.query;
|
31
|
-
|
32
|
-
if (!show)
|
33
|
-
throw new BadRequestError(
|
34
|
-
`You don't want to display the "hello world"!`,
|
35
|
-
);
|
36
|
-
|
37
|
-
res.status(200).send({ message: "Hello world!" });
|
38
|
-
},
|
39
|
-
});
|
40
|
-
}
|
@@ -1,11 +0,0 @@
|
|
1
|
-
import { HttpError } from "@/core/domain-error";
|
2
|
-
|
3
|
-
export class BadRequestError extends HttpError {
|
4
|
-
readonly error = "BAD_REQUEST_ERROR";
|
5
|
-
readonly statusCode = 400;
|
6
|
-
readonly debug = null;
|
7
|
-
|
8
|
-
constructor(public message: string) {
|
9
|
-
super(message);
|
10
|
-
}
|
11
|
-
}
|
@@ -1,13 +0,0 @@
|
|
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,21 +0,0 @@
|
|
1
|
-
import { HttpError } from "@/core/domain-error";
|
2
|
-
|
3
|
-
const message = "Os dados enviados são inválidos.";
|
4
|
-
|
5
|
-
export class UploadValidationError extends HttpError {
|
6
|
-
readonly error = "UPLOAD_VALIDATION_ERROR";
|
7
|
-
readonly message = message;
|
8
|
-
public debug: object;
|
9
|
-
|
10
|
-
constructor(
|
11
|
-
public statusCode = 400,
|
12
|
-
debug = {},
|
13
|
-
) {
|
14
|
-
super(message);
|
15
|
-
|
16
|
-
this.debug = {
|
17
|
-
multerError: null,
|
18
|
-
...debug,
|
19
|
-
};
|
20
|
-
}
|
21
|
-
}
|
@@ -1,13 +0,0 @@
|
|
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,31 +0,0 @@
|
|
1
|
-
import { HttpError } from "@/core/domain-error";
|
2
|
-
import { ErrorPresenter } from "@/infra/presenters/error.presenter";
|
3
|
-
import { FastifyReply, FastifyRequest } from "fastify";
|
4
|
-
import { ZodError } from "zod";
|
5
|
-
import { InternalServerError } from "../errors/internal-server.error";
|
6
|
-
import { ValidationError } from "../errors/validation.error";
|
7
|
-
|
8
|
-
export async function errorHandlerPlugin(
|
9
|
-
error: unknown,
|
10
|
-
_req: FastifyRequest,
|
11
|
-
res: FastifyReply,
|
12
|
-
) {
|
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);
|
29
|
-
|
30
|
-
res.status(httpResponse.statusCode).send(httpResponse);
|
31
|
-
}
|
@@ -1,96 +0,0 @@
|
|
1
|
-
// https://github.com/turkerdev/fastify-type-provider-zod/issues/82
|
2
|
-
|
3
|
-
import { Class } from "type-fest";
|
4
|
-
import { z } from "zod";
|
5
|
-
|
6
|
-
const getTypeNameFromZodType = (zodType: z.ZodType) => {
|
7
|
-
const mapping = {
|
8
|
-
string: z.ZodString,
|
9
|
-
number: [z.ZodNumber, z.ZodBigInt],
|
10
|
-
boolean: z.ZodBoolean,
|
11
|
-
object: z.ZodObject,
|
12
|
-
array: z.ZodArray,
|
13
|
-
undefined: z.ZodUndefined,
|
14
|
-
null: z.ZodNull,
|
15
|
-
};
|
16
|
-
const primitiveTypeNames = Object.keys(mapping);
|
17
|
-
|
18
|
-
for (const primitiveTypeName of primitiveTypeNames) {
|
19
|
-
const zodTypeClass = mapping[primitiveTypeName as keyof typeof mapping];
|
20
|
-
|
21
|
-
if (Array.isArray(zodTypeClass)) {
|
22
|
-
for (const _zodTypeClass of zodTypeClass) {
|
23
|
-
if (zodType instanceof _zodTypeClass) return primitiveTypeName;
|
24
|
-
}
|
25
|
-
} else if (zodType instanceof zodTypeClass) {
|
26
|
-
return primitiveTypeName;
|
27
|
-
}
|
28
|
-
}
|
29
|
-
|
30
|
-
return "string";
|
31
|
-
};
|
32
|
-
|
33
|
-
const zodTypeContainsInnerType = (
|
34
|
-
target: z.ZodType,
|
35
|
-
innerType: Class<unknown>,
|
36
|
-
): boolean => {
|
37
|
-
if (target instanceof innerType) return true;
|
38
|
-
|
39
|
-
if ("innerType" in target._def)
|
40
|
-
return zodTypeContainsInnerType(
|
41
|
-
target._def.innerType as z.ZodType,
|
42
|
-
innerType,
|
43
|
-
);
|
44
|
-
|
45
|
-
return false;
|
46
|
-
};
|
47
|
-
|
48
|
-
// eslint-disable-next-line
|
49
|
-
export function handleSwaggerMultipart(schema: Record<string, any>) {
|
50
|
-
let isMultipartFormat = false;
|
51
|
-
|
52
|
-
if ("consumes" in schema && schema.consumes.includes("multipart/form-data"))
|
53
|
-
isMultipartFormat = true;
|
54
|
-
|
55
|
-
for (const key in schema) {
|
56
|
-
if (typeof schema[key] === "object" && schema[key] !== null) {
|
57
|
-
if (key === "consumes" && isMultipartFormat) {
|
58
|
-
// eslint-disable-next-line
|
59
|
-
const fields: Record<string, any> = {};
|
60
|
-
const requiredFields = [];
|
61
|
-
|
62
|
-
if (schema.multipartFileFields) {
|
63
|
-
for (const multipartFileField of schema.multipartFileFields) {
|
64
|
-
fields[multipartFileField] = { type: "file" };
|
65
|
-
requiredFields.push(multipartFileField);
|
66
|
-
}
|
67
|
-
}
|
68
|
-
|
69
|
-
if (schema.multipartAnotherFieldsSchema) {
|
70
|
-
for (const multipartAnotherField in schema
|
71
|
-
.multipartAnotherFieldsSchema.shape) {
|
72
|
-
const zodType =
|
73
|
-
schema.multipartAnotherFieldsSchema.shape[multipartAnotherField];
|
74
|
-
|
75
|
-
fields[multipartAnotherField] = {
|
76
|
-
type: getTypeNameFromZodType(zodType),
|
77
|
-
};
|
78
|
-
|
79
|
-
if (!zodTypeContainsInnerType(zodType, z.ZodOptional))
|
80
|
-
requiredFields.push(multipartAnotherField);
|
81
|
-
}
|
82
|
-
}
|
83
|
-
|
84
|
-
schema.body = {
|
85
|
-
type: "object",
|
86
|
-
properties: fields,
|
87
|
-
required: requiredFields,
|
88
|
-
};
|
89
|
-
} else {
|
90
|
-
handleSwaggerMultipart(schema[key]);
|
91
|
-
}
|
92
|
-
}
|
93
|
-
}
|
94
|
-
|
95
|
-
return schema;
|
96
|
-
}
|
@@ -1,197 +0,0 @@
|
|
1
|
-
import {
|
2
|
-
FastifyZodInstance,
|
3
|
-
FastifyZodReply,
|
4
|
-
FastifyZodRequest,
|
5
|
-
} from "@/infra/http/@types/fastify-zod-type-provider";
|
6
|
-
import { randomUUID } from "crypto";
|
7
|
-
import multer, { MulterError } from "fastify-multer";
|
8
|
-
import { Options } from "fastify-multer/lib/interfaces";
|
9
|
-
import { extension } from "mime-types";
|
10
|
-
import path, { extname } from "path";
|
11
|
-
import prettyBytes from "pretty-bytes";
|
12
|
-
import { UploadValidationError } from "../errors/upload-validation.error";
|
13
|
-
|
14
|
-
interface MemoryStorage {
|
15
|
-
storage?: "memory";
|
16
|
-
}
|
17
|
-
|
18
|
-
interface DiskStorage {
|
19
|
-
storage?: "disk";
|
20
|
-
storageDir?: string;
|
21
|
-
}
|
22
|
-
|
23
|
-
type Storage = MemoryStorage | DiskStorage;
|
24
|
-
|
25
|
-
type RequireUploadOptions = Storage & {
|
26
|
-
fieldName: string;
|
27
|
-
allowedExtensions: string[];
|
28
|
-
limits?: Pick<NonNullable<Options["limits"]>, "fileSize" | "files">;
|
29
|
-
isRequiredUpload?: boolean;
|
30
|
-
};
|
31
|
-
|
32
|
-
const defaultOptions = {
|
33
|
-
storage: "memory",
|
34
|
-
storageDir: "./tmp",
|
35
|
-
limits: {
|
36
|
-
fileSize: 1000000 * 10, // 10 MB
|
37
|
-
files: 1,
|
38
|
-
},
|
39
|
-
isRequiredUpload: true,
|
40
|
-
};
|
41
|
-
|
42
|
-
export function requireUpload(
|
43
|
-
options: RequireUploadOptions = { fieldName: "", allowedExtensions: [] },
|
44
|
-
) {
|
45
|
-
const {
|
46
|
-
storage,
|
47
|
-
storageDir,
|
48
|
-
fieldName,
|
49
|
-
allowedExtensions,
|
50
|
-
limits,
|
51
|
-
isRequiredUpload,
|
52
|
-
} = {
|
53
|
-
...defaultOptions,
|
54
|
-
...options,
|
55
|
-
limits: {
|
56
|
-
...defaultOptions.limits,
|
57
|
-
...(options.limits ? options.limits : {}),
|
58
|
-
},
|
59
|
-
};
|
60
|
-
const handleStorage = () => {
|
61
|
-
if (storage === "disk") {
|
62
|
-
return multer.diskStorage({
|
63
|
-
destination: storageDir,
|
64
|
-
filename: (_req, file, cb) => {
|
65
|
-
const ext = path.extname(file.originalname);
|
66
|
-
|
67
|
-
cb(null, `${randomUUID()}${ext}`);
|
68
|
-
},
|
69
|
-
});
|
70
|
-
}
|
71
|
-
|
72
|
-
return multer.memoryStorage();
|
73
|
-
};
|
74
|
-
|
75
|
-
const upload = multer({
|
76
|
-
limits,
|
77
|
-
storage: handleStorage(),
|
78
|
-
fileFilter: (_req, { mimetype, originalname }, cb) => {
|
79
|
-
const fileExtension = extension(mimetype) || extname(originalname);
|
80
|
-
|
81
|
-
if (allowedExtensions.includes(fileExtension)) {
|
82
|
-
cb(null, true);
|
83
|
-
} else {
|
84
|
-
cb(
|
85
|
-
new UploadValidationError(400, {
|
86
|
-
message: "Invalid file format.",
|
87
|
-
fieldName,
|
88
|
-
allowedExtensions,
|
89
|
-
}),
|
90
|
-
);
|
91
|
-
}
|
92
|
-
},
|
93
|
-
});
|
94
|
-
const isMultipleUpload = limits.files > 1;
|
95
|
-
|
96
|
-
return [
|
97
|
-
async function (
|
98
|
-
this: FastifyZodInstance,
|
99
|
-
req: FastifyZodRequest,
|
100
|
-
res: FastifyZodReply,
|
101
|
-
) {
|
102
|
-
let middleware = upload.single(fieldName);
|
103
|
-
|
104
|
-
if (isMultipleUpload) middleware = upload.array(fieldName);
|
105
|
-
|
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
|
-
}
|
144
|
-
}
|
145
|
-
|
146
|
-
if (error) return reject(error);
|
147
|
-
|
148
|
-
resolve();
|
149
|
-
});
|
150
|
-
});
|
151
|
-
},
|
152
|
-
async (req: FastifyZodRequest) => {
|
153
|
-
if (isRequiredUpload) {
|
154
|
-
if (!isMultipleUpload && !req.file) {
|
155
|
-
throw new UploadValidationError(400, {
|
156
|
-
multerError: null,
|
157
|
-
message: `Field "${fieldName}" is required with a file.`,
|
158
|
-
});
|
159
|
-
}
|
160
|
-
|
161
|
-
if (isMultipleUpload && (!req.files || !req.files.length)) {
|
162
|
-
throw new UploadValidationError(400, {
|
163
|
-
multerError: null,
|
164
|
-
message: `Field "${fieldName}" is required with at least 1 file.`,
|
165
|
-
});
|
166
|
-
}
|
167
|
-
}
|
168
|
-
|
169
|
-
const body = req.body as Record<string, string>;
|
170
|
-
const parsedStringValues = Object.keys(body).reduce(
|
171
|
-
(obj, key) => {
|
172
|
-
const value = body[key];
|
173
|
-
|
174
|
-
try {
|
175
|
-
obj[key] = JSON.parse(value);
|
176
|
-
} catch {
|
177
|
-
obj[key] = value;
|
178
|
-
}
|
179
|
-
|
180
|
-
return obj;
|
181
|
-
},
|
182
|
-
{} as typeof body,
|
183
|
-
);
|
184
|
-
|
185
|
-
if (
|
186
|
-
req.routeOptions.schema &&
|
187
|
-
req.routeOptions.schema.multipartAnotherFieldsSchema
|
188
|
-
) {
|
189
|
-
req.routeOptions.schema.multipartAnotherFieldsSchema.parse(
|
190
|
-
parsedStringValues,
|
191
|
-
);
|
192
|
-
}
|
193
|
-
|
194
|
-
req.body = parsedStringValues;
|
195
|
-
},
|
196
|
-
];
|
197
|
-
}
|
@@ -1,8 +0,0 @@
|
|
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
|
-
}
|