@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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/dist/compose-app/copy-template.compose.js +1 -0
  4. package/dist/compose-app/replace-content-in-file.compose.js +1 -0
  5. package/dist/config/index.js +1 -0
  6. package/dist/index.js +2 -0
  7. package/dist/utils/logs.js +1 -0
  8. package/dist/utils/on-cancel.js +1 -0
  9. package/dist/utils/to-pascal-case.js +1 -0
  10. package/dist/validations/package-name.validation.js +1 -0
  11. package/package.json +71 -0
  12. package/templates/clean/.env.example +5 -0
  13. package/templates/clean/.eslintignore +2 -0
  14. package/templates/clean/.eslintrc.json +22 -0
  15. package/templates/clean/.husky/pre-commit +1 -0
  16. package/templates/clean/.lintstagedrc.json +4 -0
  17. package/templates/clean/.prettierignore +7 -0
  18. package/templates/clean/.prettierrc.json +6 -0
  19. package/templates/clean/.vscode/settings.json +8 -0
  20. package/templates/clean/build.config.ts +55 -0
  21. package/templates/clean/package.json +41 -0
  22. package/templates/clean/pnpm-lock.yaml +3929 -0
  23. package/templates/clean/src/env.ts +17 -0
  24. package/templates/clean/src/index.ts +1 -0
  25. package/templates/clean/tsconfig.json +15 -0
  26. package/templates/clean/vitest.config.mts +6 -0
  27. package/templates/fastify/.env.example +11 -0
  28. package/templates/fastify/.eslintignore +2 -0
  29. package/templates/fastify/.eslintrc.json +22 -0
  30. package/templates/fastify/.husky/pre-commit +1 -0
  31. package/templates/fastify/.lintstagedrc.json +4 -0
  32. package/templates/fastify/.prettierignore +7 -0
  33. package/templates/fastify/.prettierrc.json +6 -0
  34. package/templates/fastify/.vscode/settings.json +8 -0
  35. package/templates/fastify/build.config.ts +55 -0
  36. package/templates/fastify/package.json +57 -0
  37. package/templates/fastify/pnpm-lock.yaml +5353 -0
  38. package/templates/fastify/src/@types/fastify-zod-type-provider.ts +39 -0
  39. package/templates/fastify/src/@types/fastify.d.ts +16 -0
  40. package/templates/fastify/src/app.ts +45 -0
  41. package/templates/fastify/src/controllers/hello/hello-multipart.controller.e2e-spec.ts +32 -0
  42. package/templates/fastify/src/controllers/hello/hello-multipart.controller.ts +51 -0
  43. package/templates/fastify/src/controllers/hello/hello.controller.e2e-spec.ts +14 -0
  44. package/templates/fastify/src/controllers/hello/hello.controller.ts +36 -0
  45. package/templates/fastify/src/env.ts +19 -0
  46. package/templates/fastify/src/errors/exceptions.ts +87 -0
  47. package/templates/fastify/src/errors/http-error-handler.ts +111 -0
  48. package/templates/fastify/src/plugins/error-handler.plugin.ts +27 -0
  49. package/templates/fastify/src/plugins/handle-swagger-multipart.plugin.ts +96 -0
  50. package/templates/fastify/src/plugins/require-upload.plugin.ts +200 -0
  51. package/templates/fastify/src/routes/hello.routes.ts +8 -0
  52. package/templates/fastify/src/routes/index.ts +6 -0
  53. package/templates/fastify/src/server.ts +12 -0
  54. package/templates/fastify/src/utils/capitalize-word.ts +3 -0
  55. package/templates/fastify/test/e2e-setup.ts +10 -0
  56. package/templates/fastify/tsconfig.json +15 -0
  57. package/templates/fastify/vitest.config.e2e.mts +9 -0
  58. 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,6 @@
1
+ import { FastifyInstance } from "fastify";
2
+ import { helloRoutes } from "./hello.routes";
3
+
4
+ export async function routes(app: FastifyInstance) {
5
+ app.register(helloRoutes);
6
+ }
@@ -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,3 @@
1
+ export function capitalizeWord(text: string) {
2
+ return text[0].toUpperCase() + text.slice(1);
3
+ }
@@ -0,0 +1,10 @@
1
+ import { app } from "@/app";
2
+ import { afterAll, beforeAll } from "vitest";
3
+
4
+ beforeAll(async () => {
5
+ await app.ready();
6
+ });
7
+
8
+ afterAll(async () => {
9
+ await app.close();
10
+ });
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ import { mergeConfig } from "vitest/config";
2
+ import defaultConfig from "./vitest.config.mjs";
3
+
4
+ export default mergeConfig(defaultConfig, {
5
+ test: {
6
+ include: ["./**/*.e2e-spec.ts"],
7
+ setupFiles: ["./test/e2e-setup.ts"],
8
+ },
9
+ });
@@ -0,0 +1,6 @@
1
+ import tsconfigPaths from "vite-tsconfig-paths";
2
+ import { defineConfig } from "vitest/config";
3
+
4
+ export default defineConfig({
5
+ plugins: [tsconfigPaths()],
6
+ });