@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,200 @@
|
|
1
|
+
import {
|
2
|
+
FastifyZodInstance,
|
3
|
+
FastifyZodReply,
|
4
|
+
FastifyZodRequest,
|
5
|
+
} from "@/@types/fastify-zod-type-provider";
|
6
|
+
import { RequestFormatError, UploadError } from "@/errors/exceptions";
|
7
|
+
import { randomUUID } from "crypto";
|
8
|
+
import { HookHandlerDoneFunction } from "fastify";
|
9
|
+
import multer, { MulterError } from "fastify-multer";
|
10
|
+
import { Options } from "fastify-multer/lib/interfaces";
|
11
|
+
import { extension } from "mime-types";
|
12
|
+
import path, { extname } from "path";
|
13
|
+
import prettyBytes from "pretty-bytes";
|
14
|
+
|
15
|
+
interface MemoryStorage {
|
16
|
+
storage?: "memory";
|
17
|
+
}
|
18
|
+
|
19
|
+
interface DiskStorage {
|
20
|
+
storage?: "disk";
|
21
|
+
storageDir?: string;
|
22
|
+
}
|
23
|
+
|
24
|
+
type Storage = MemoryStorage | DiskStorage;
|
25
|
+
|
26
|
+
type RequireUploadOptions = Storage & {
|
27
|
+
fieldName: string;
|
28
|
+
allowedExtensions: string[];
|
29
|
+
limits?: Pick<NonNullable<Options["limits"]>, "fileSize" | "files">;
|
30
|
+
isRequiredUpload?: boolean;
|
31
|
+
};
|
32
|
+
|
33
|
+
const defaultOptions = {
|
34
|
+
storage: "memory",
|
35
|
+
storageDir: "./tmp",
|
36
|
+
limits: {
|
37
|
+
fileSize: 1000000 * 10, // 10 MB
|
38
|
+
files: 1,
|
39
|
+
},
|
40
|
+
isRequiredUpload: true,
|
41
|
+
};
|
42
|
+
|
43
|
+
export function requireUpload(
|
44
|
+
options: RequireUploadOptions = { fieldName: "", allowedExtensions: [] },
|
45
|
+
) {
|
46
|
+
const {
|
47
|
+
storage,
|
48
|
+
storageDir,
|
49
|
+
fieldName,
|
50
|
+
allowedExtensions,
|
51
|
+
limits,
|
52
|
+
isRequiredUpload,
|
53
|
+
} = {
|
54
|
+
...defaultOptions,
|
55
|
+
...options,
|
56
|
+
limits: {
|
57
|
+
...defaultOptions.limits,
|
58
|
+
...(options.limits ? options.limits : {}),
|
59
|
+
},
|
60
|
+
};
|
61
|
+
const handleStorage = () => {
|
62
|
+
if (storage === "disk") {
|
63
|
+
return multer.diskStorage({
|
64
|
+
destination: storageDir,
|
65
|
+
filename: (_req, file, cb) => {
|
66
|
+
const ext = path.extname(file.originalname);
|
67
|
+
|
68
|
+
cb(null, `${randomUUID()}${ext}`);
|
69
|
+
},
|
70
|
+
});
|
71
|
+
}
|
72
|
+
|
73
|
+
return multer.memoryStorage();
|
74
|
+
};
|
75
|
+
|
76
|
+
const upload = multer({
|
77
|
+
limits,
|
78
|
+
storage: handleStorage(),
|
79
|
+
fileFilter: (_req, { mimetype, originalname }, cb) => {
|
80
|
+
const fileExtension = extension(mimetype) || extname(originalname);
|
81
|
+
|
82
|
+
if (allowedExtensions.includes(fileExtension)) {
|
83
|
+
cb(null, true);
|
84
|
+
} else {
|
85
|
+
const validExtensions = allowedExtensions
|
86
|
+
.map(ext => `"${ext}"`)
|
87
|
+
.join(", ");
|
88
|
+
|
89
|
+
cb(
|
90
|
+
new UploadError(
|
91
|
+
400,
|
92
|
+
`Formato de arquivo inválido. Use apenas extensões: ${validExtensions}.`,
|
93
|
+
),
|
94
|
+
);
|
95
|
+
}
|
96
|
+
},
|
97
|
+
});
|
98
|
+
const isMultipleUpload = limits.files > 1;
|
99
|
+
|
100
|
+
return [
|
101
|
+
async (req: FastifyZodRequest) => {
|
102
|
+
if (!req.headers["content-type"]?.includes("multipart/form-data")) {
|
103
|
+
throw new RequestFormatError(
|
104
|
+
"A solicitação deve ser do tipo multipart/form-data.",
|
105
|
+
);
|
106
|
+
}
|
107
|
+
},
|
108
|
+
function (
|
109
|
+
this: FastifyZodInstance,
|
110
|
+
req: FastifyZodRequest,
|
111
|
+
res: FastifyZodReply,
|
112
|
+
done: HookHandlerDoneFunction,
|
113
|
+
) {
|
114
|
+
let middleware = upload.single(fieldName);
|
115
|
+
|
116
|
+
if (isMultipleUpload) middleware = upload.array(fieldName);
|
117
|
+
|
118
|
+
middleware.bind(this)(req, res, error => {
|
119
|
+
const sendError = (statusCode: number, message: string) => {
|
120
|
+
done(new UploadError(statusCode, message));
|
121
|
+
};
|
122
|
+
|
123
|
+
if (error && error instanceof MulterError) {
|
124
|
+
switch (error.code) {
|
125
|
+
case "LIMIT_FILE_SIZE":
|
126
|
+
return sendError(
|
127
|
+
413,
|
128
|
+
`O tamanho máximo de cada arquivo é ${prettyBytes(
|
129
|
+
limits.fileSize,
|
130
|
+
)}.`,
|
131
|
+
);
|
132
|
+
|
133
|
+
case "LIMIT_FILE_COUNT":
|
134
|
+
return sendError(
|
135
|
+
413,
|
136
|
+
`A contagem máxima de arquivos é ${limits.files}.`,
|
137
|
+
);
|
138
|
+
|
139
|
+
case "LIMIT_UNEXPECTED_FILE":
|
140
|
+
return sendError(
|
141
|
+
400,
|
142
|
+
`O campo de arquivo '${error.field}' não é permitido.`,
|
143
|
+
);
|
144
|
+
|
145
|
+
default:
|
146
|
+
return done(error);
|
147
|
+
}
|
148
|
+
}
|
149
|
+
|
150
|
+
if (error) return done(error);
|
151
|
+
|
152
|
+
done();
|
153
|
+
});
|
154
|
+
},
|
155
|
+
async (req: FastifyZodRequest) => {
|
156
|
+
if (isRequiredUpload) {
|
157
|
+
if (!isMultipleUpload && !req.file) {
|
158
|
+
throw new UploadError(
|
159
|
+
400,
|
160
|
+
`O campo '${fieldName}' é obrigatório com um arquivo.`,
|
161
|
+
);
|
162
|
+
}
|
163
|
+
|
164
|
+
if (isMultipleUpload && (!req.files || !req.files.length)) {
|
165
|
+
throw new UploadError(
|
166
|
+
400,
|
167
|
+
`O campo '${fieldName}' é obrigatório com no mínimo um arquivo.`,
|
168
|
+
);
|
169
|
+
}
|
170
|
+
}
|
171
|
+
|
172
|
+
const body = req.body as Record<string, string>;
|
173
|
+
const parsedStringValues = Object.keys(body).reduce(
|
174
|
+
(obj, key) => {
|
175
|
+
const value = body[key];
|
176
|
+
|
177
|
+
try {
|
178
|
+
obj[key] = JSON.parse(value);
|
179
|
+
} catch {
|
180
|
+
obj[key] = value;
|
181
|
+
}
|
182
|
+
|
183
|
+
return obj;
|
184
|
+
},
|
185
|
+
{} as typeof body,
|
186
|
+
);
|
187
|
+
|
188
|
+
if (
|
189
|
+
req.routeOptions.schema &&
|
190
|
+
req.routeOptions.schema.multipartAnotherFieldsSchema
|
191
|
+
) {
|
192
|
+
req.routeOptions.schema.multipartAnotherFieldsSchema.parse(
|
193
|
+
parsedStringValues,
|
194
|
+
);
|
195
|
+
}
|
196
|
+
|
197
|
+
req.body = parsedStringValues;
|
198
|
+
},
|
199
|
+
];
|
200
|
+
}
|
@@ -0,0 +1,8 @@
|
|
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
|
+
}
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import { app } from "./app";
|
2
|
+
import { env } from "./env";
|
3
|
+
|
4
|
+
(async () => {
|
5
|
+
await app.ready();
|
6
|
+
await app.listen({ host: "0.0.0.0", port: env.API_PORT });
|
7
|
+
|
8
|
+
console.log(`Application "${env.API_NAME}" is running!`);
|
9
|
+
|
10
|
+
if (env.NODE_ENV === "development")
|
11
|
+
console.log(`http://localhost:${env.API_PORT}/docs`);
|
12
|
+
})();
|
@@ -0,0 +1,15 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"target": "ESNext",
|
4
|
+
"module": "Commonjs",
|
5
|
+
"esModuleInterop": true,
|
6
|
+
"resolveJsonModule": true,
|
7
|
+
"forceConsistentCasingInFileNames": true,
|
8
|
+
"strict": true,
|
9
|
+
"skipLibCheck": true,
|
10
|
+
"baseUrl": ".",
|
11
|
+
"paths": {
|
12
|
+
"@/*": ["./src/*"]
|
13
|
+
}
|
14
|
+
}
|
15
|
+
}
|