@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,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
+ }