@leo-h/create-nodejs-app 1.0.0
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/LICENSE +21 -0
- package/README.md +31 -0
- package/dist/compose-app/copy-template.compose.js +1 -0
- package/dist/compose-app/replace-content-in-file.compose.js +1 -0
- package/dist/config/index.js +1 -0
- package/dist/index.js +2 -0
- package/dist/utils/logs.js +1 -0
- package/dist/utils/on-cancel.js +1 -0
- package/dist/utils/to-pascal-case.js +1 -0
- package/dist/validations/package-name.validation.js +1 -0
- package/package.json +71 -0
- package/templates/clean/.env.example +5 -0
- package/templates/clean/.eslintignore +2 -0
- package/templates/clean/.eslintrc.json +22 -0
- package/templates/clean/.husky/pre-commit +1 -0
- package/templates/clean/.lintstagedrc.json +4 -0
- package/templates/clean/.prettierignore +7 -0
- package/templates/clean/.prettierrc.json +6 -0
- package/templates/clean/.vscode/settings.json +8 -0
- package/templates/clean/build.config.ts +55 -0
- package/templates/clean/package.json +41 -0
- package/templates/clean/pnpm-lock.yaml +3929 -0
- package/templates/clean/src/env.ts +17 -0
- package/templates/clean/src/index.ts +1 -0
- package/templates/clean/tsconfig.json +15 -0
- package/templates/clean/vitest.config.mts +6 -0
- package/templates/fastify/.env.example +11 -0
- package/templates/fastify/.eslintignore +2 -0
- package/templates/fastify/.eslintrc.json +22 -0
- package/templates/fastify/.husky/pre-commit +1 -0
- package/templates/fastify/.lintstagedrc.json +4 -0
- package/templates/fastify/.prettierignore +7 -0
- package/templates/fastify/.prettierrc.json +6 -0
- package/templates/fastify/.vscode/settings.json +8 -0
- package/templates/fastify/build.config.ts +55 -0
- package/templates/fastify/package.json +57 -0
- package/templates/fastify/pnpm-lock.yaml +5353 -0
- package/templates/fastify/src/@types/fastify-zod-type-provider.ts +39 -0
- package/templates/fastify/src/@types/fastify.d.ts +16 -0
- package/templates/fastify/src/app.ts +45 -0
- package/templates/fastify/src/controllers/hello/hello-multipart.controller.e2e-spec.ts +32 -0
- package/templates/fastify/src/controllers/hello/hello-multipart.controller.ts +51 -0
- package/templates/fastify/src/controllers/hello/hello.controller.e2e-spec.ts +14 -0
- package/templates/fastify/src/controllers/hello/hello.controller.ts +36 -0
- package/templates/fastify/src/env.ts +19 -0
- package/templates/fastify/src/errors/exceptions.ts +87 -0
- package/templates/fastify/src/errors/http-error-handler.ts +111 -0
- package/templates/fastify/src/plugins/error-handler.plugin.ts +27 -0
- package/templates/fastify/src/plugins/handle-swagger-multipart.plugin.ts +96 -0
- package/templates/fastify/src/plugins/require-upload.plugin.ts +200 -0
- package/templates/fastify/src/routes/hello.routes.ts +8 -0
- package/templates/fastify/src/routes/index.ts +6 -0
- package/templates/fastify/src/server.ts +12 -0
- package/templates/fastify/src/utils/capitalize-word.ts +3 -0
- package/templates/fastify/test/e2e-setup.ts +10 -0
- package/templates/fastify/tsconfig.json +15 -0
- package/templates/fastify/vitest.config.e2e.mts +9 -0
- package/templates/fastify/vitest.config.mts +6 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
import {
|
2
|
+
ContextConfigDefault,
|
3
|
+
FastifyBaseLogger,
|
4
|
+
FastifyInstance,
|
5
|
+
FastifyReply,
|
6
|
+
FastifyRequest,
|
7
|
+
FastifySchema,
|
8
|
+
RawReplyDefaultExpression,
|
9
|
+
RawRequestDefaultExpression,
|
10
|
+
RawServerDefault,
|
11
|
+
RouteGenericInterface,
|
12
|
+
} from "fastify";
|
13
|
+
import { ZodTypeProvider } from "fastify-type-provider-zod";
|
14
|
+
|
15
|
+
export type FastifyZodInstance = FastifyInstance<
|
16
|
+
RawServerDefault,
|
17
|
+
RawRequestDefaultExpression,
|
18
|
+
RawReplyDefaultExpression,
|
19
|
+
FastifyBaseLogger,
|
20
|
+
ZodTypeProvider
|
21
|
+
>;
|
22
|
+
|
23
|
+
export type FastifyZodRequest = FastifyRequest<
|
24
|
+
RouteGenericInterface,
|
25
|
+
RawServerDefault,
|
26
|
+
RawRequestDefaultExpression,
|
27
|
+
FastifySchema,
|
28
|
+
ZodTypeProvider
|
29
|
+
>;
|
30
|
+
|
31
|
+
export type FastifyZodReply = FastifyReply<
|
32
|
+
RawServerDefault,
|
33
|
+
RawRequestDefaultExpression,
|
34
|
+
RawReplyDefaultExpression,
|
35
|
+
RouteGenericInterface,
|
36
|
+
ContextConfigDefault,
|
37
|
+
FastifySchema,
|
38
|
+
ZodTypeProvider
|
39
|
+
>;
|
@@ -0,0 +1,16 @@
|
|
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
|
+
}
|
@@ -0,0 +1,45 @@
|
|
1
|
+
import fastifyCookie from "@fastify/cookie";
|
2
|
+
import fastifyCors from "@fastify/cors";
|
3
|
+
import fastifySwagger from "@fastify/swagger";
|
4
|
+
import fastifySwaggerUi from "@fastify/swagger-ui";
|
5
|
+
import fastify from "fastify";
|
6
|
+
import multer from "fastify-multer";
|
7
|
+
import {
|
8
|
+
jsonSchemaTransform,
|
9
|
+
serializerCompiler,
|
10
|
+
validatorCompiler,
|
11
|
+
} from "fastify-type-provider-zod";
|
12
|
+
import { env } from "./env";
|
13
|
+
import { errorHandlerPlugin } from "./plugins/error-handler.plugin";
|
14
|
+
import { handleSwaggerMultipart } from "./plugins/handle-swagger-multipart.plugin";
|
15
|
+
import { routes } from "./routes";
|
16
|
+
|
17
|
+
export const app = fastify();
|
18
|
+
|
19
|
+
app.setValidatorCompiler(validatorCompiler);
|
20
|
+
app.setSerializerCompiler(serializerCompiler);
|
21
|
+
|
22
|
+
app.register(fastifySwagger, {
|
23
|
+
openapi: {
|
24
|
+
info: {
|
25
|
+
title: env.API_NAME,
|
26
|
+
description: process.env.npm_package_description ?? "",
|
27
|
+
version: process.env.npm_package_version ?? "",
|
28
|
+
},
|
29
|
+
servers: [],
|
30
|
+
},
|
31
|
+
transform: data => {
|
32
|
+
const jsonSchema = jsonSchemaTransform(data);
|
33
|
+
|
34
|
+
handleSwaggerMultipart(jsonSchema.schema);
|
35
|
+
|
36
|
+
return jsonSchema;
|
37
|
+
},
|
38
|
+
});
|
39
|
+
app.register(fastifySwaggerUi, { routePrefix: "/docs" });
|
40
|
+
app.register(fastifyCors, { origin: env.API_ACCESS_PERMISSION_CLIENT_SIDE });
|
41
|
+
app.register(fastifyCookie);
|
42
|
+
app.register(multer.contentParser);
|
43
|
+
|
44
|
+
app.setErrorHandler(errorHandlerPlugin);
|
45
|
+
app.register(routes);
|
@@ -0,0 +1,32 @@
|
|
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
|
+
});
|
@@ -0,0 +1,51 @@
|
|
1
|
+
import { requireUpload } from "@/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
|
+
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.string(),
|
27
|
+
description: z.string(),
|
28
|
+
file: z.custom(),
|
29
|
+
}),
|
30
|
+
},
|
31
|
+
},
|
32
|
+
preHandler: requireUpload({
|
33
|
+
fieldName: "attachment",
|
34
|
+
allowedExtensions: ["png", "jpg", "jpeg", "webp"],
|
35
|
+
limits: { fileSize: 1000000 * 15, files: 1 },
|
36
|
+
storage: "memory",
|
37
|
+
}),
|
38
|
+
handler: async (req, res) => {
|
39
|
+
// eslint-disable-next-line
|
40
|
+
const { buffer, ...fileInfo } = req.file;
|
41
|
+
const { description } =
|
42
|
+
req.body as unknown as HelloMultipartControllerBody;
|
43
|
+
|
44
|
+
res.status(200).send({
|
45
|
+
message: "Hello world!",
|
46
|
+
description,
|
47
|
+
file: fileInfo,
|
48
|
+
});
|
49
|
+
},
|
50
|
+
});
|
51
|
+
}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import { app } from "@/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
|
+
});
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import { BadRequestError } from "@/errors/exceptions";
|
2
|
+
import { FastifyInstance } from "fastify";
|
3
|
+
import { ZodTypeProvider } from "fastify-type-provider-zod";
|
4
|
+
import { z } from "zod";
|
5
|
+
|
6
|
+
const helloControllerQuerySchema = z.object({
|
7
|
+
show: z
|
8
|
+
.enum(["true", "false"])
|
9
|
+
.transform(val => JSON.parse(val))
|
10
|
+
.default("true"),
|
11
|
+
});
|
12
|
+
|
13
|
+
export async function helloController(app: FastifyInstance) {
|
14
|
+
app.withTypeProvider<ZodTypeProvider>().route({
|
15
|
+
method: "GET",
|
16
|
+
url: "/hello",
|
17
|
+
schema: {
|
18
|
+
tags: ["Hello"],
|
19
|
+
summary: "Hello world!",
|
20
|
+
querystring: helloControllerQuerySchema,
|
21
|
+
response: {
|
22
|
+
200: z.object({ message: z.string() }),
|
23
|
+
},
|
24
|
+
},
|
25
|
+
handler: async (req, res) => {
|
26
|
+
const { show } = req.query;
|
27
|
+
|
28
|
+
if (!show)
|
29
|
+
throw new BadRequestError(
|
30
|
+
'You don\'t want to display the "hello world"!',
|
31
|
+
);
|
32
|
+
|
33
|
+
res.status(200).send({ message: "Hello world!" });
|
34
|
+
},
|
35
|
+
});
|
36
|
+
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import "dotenv/config";
|
2
|
+
import { z } from "zod";
|
3
|
+
|
4
|
+
const schema = z.object({
|
5
|
+
NODE_ENV: z.enum(["test", "development", "production"]),
|
6
|
+
API_NAME: z.string(),
|
7
|
+
API_PORT: z.coerce.number(),
|
8
|
+
API_ACCESS_PERMISSION_CLIENT_SIDE: z.string().default("*"),
|
9
|
+
});
|
10
|
+
|
11
|
+
const parsedEnv = schema.safeParse(process.env);
|
12
|
+
|
13
|
+
if (!parsedEnv.success) {
|
14
|
+
console.error(parsedEnv.error.flatten().fieldErrors);
|
15
|
+
|
16
|
+
throw new Error("Invalid environment variables.");
|
17
|
+
}
|
18
|
+
|
19
|
+
export const env = parsedEnv.data;
|
@@ -0,0 +1,87 @@
|
|
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
|
+
}
|
@@ -0,0 +1,111 @@
|
|
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
|
+
}
|
@@ -0,0 +1,27 @@
|
|
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
|
+
}
|
@@ -0,0 +1,96 @@
|
|
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
|
+
}
|