@leo-h/create-nodejs-app 1.0.70 → 1.0.72
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 +1 -1
- package/templates/elysia/.env.example +1 -1
- package/templates/elysia/.env.test +6 -0
- package/templates/elysia/biome.json +6 -1
- package/templates/elysia/package.json +4 -3
- package/templates/elysia/pnpm-lock.yaml +15 -6
- package/templates/elysia/src/env.ts +39 -5
- package/templates/elysia/src/http/app.ts +5 -3
- package/templates/elysia/src/http/controllers/hello-multipart.controller.spec.ts +5 -5
- package/templates/elysia/src/http/controllers/hello-multipart.controller.ts +59 -44
- package/templates/elysia/src/http/controllers/hello.controller.spec.ts +5 -5
- package/templates/elysia/src/http/controllers/hello.controller.ts +29 -29
- package/templates/elysia/src/http/errors.ts +103 -32
- package/templates/elysia/src/http/plugins/{global-error-handler.plugin.ts → controller-error-handler.plugin.ts} +10 -32
- package/templates/elysia/vitest.config.mts +3 -0
- package/templates/elysia/.env.development +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leo-h/create-nodejs-app",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.72",
|
|
4
4
|
"packageManager": "pnpm@9.15.9",
|
|
5
5
|
"author": "Leonardo Henrique <leonardo0507.business@gmail.com>",
|
|
6
6
|
"description": "Create a modern Node.js app with TypeScript using one command.",
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"version": "1.0.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
|
-
"start": "node
|
|
7
|
-
"start:dev": "tsx watch
|
|
6
|
+
"start": "node ./dist/src/http/server.js",
|
|
7
|
+
"start:dev": "tsx watch ./src/http/server.ts",
|
|
8
8
|
"typecheck": "tsc --noEmit",
|
|
9
9
|
"lint": "biome lint",
|
|
10
10
|
"lint:fix": "biome lint --write",
|
|
@@ -21,12 +21,13 @@
|
|
|
21
21
|
"@elysiajs/cors": "1.4.0",
|
|
22
22
|
"@elysiajs/node": "1.4.2",
|
|
23
23
|
"@elysiajs/openapi": "1.4.11",
|
|
24
|
+
"dotenv": "17.2.3",
|
|
24
25
|
"elysia": "1.4.16",
|
|
25
26
|
"zod": "4.1.12"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"@biomejs/biome": "2.3.5",
|
|
29
|
-
"@elysiajs/eden": "^1.4.
|
|
30
|
+
"@elysiajs/eden": "^1.4.5",
|
|
30
31
|
"@faker-js/faker": "^10.1.0",
|
|
31
32
|
"@swc/cli": "0.7.8",
|
|
32
33
|
"@types/node": "24.9.1",
|
|
@@ -17,6 +17,9 @@ importers:
|
|
|
17
17
|
'@elysiajs/openapi':
|
|
18
18
|
specifier: 1.4.11
|
|
19
19
|
version: 1.4.11(elysia@1.4.16(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@20.5.0)(openapi-types@12.1.3)(typescript@5.9.3))
|
|
20
|
+
dotenv:
|
|
21
|
+
specifier: 17.2.3
|
|
22
|
+
version: 17.2.3
|
|
20
23
|
elysia:
|
|
21
24
|
specifier: 1.4.16
|
|
22
25
|
version: 1.4.16(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@20.5.0)(openapi-types@12.1.3)(typescript@5.9.3)
|
|
@@ -28,8 +31,8 @@ importers:
|
|
|
28
31
|
specifier: 2.3.5
|
|
29
32
|
version: 2.3.5
|
|
30
33
|
'@elysiajs/eden':
|
|
31
|
-
specifier: ^1.4.
|
|
32
|
-
version: 1.4.
|
|
34
|
+
specifier: ^1.4.5
|
|
35
|
+
version: 1.4.5(elysia@1.4.16(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@20.5.0)(openapi-types@12.1.3)(typescript@5.9.3))
|
|
33
36
|
'@faker-js/faker':
|
|
34
37
|
specifier: ^10.1.0
|
|
35
38
|
version: 10.1.0
|
|
@@ -121,10 +124,10 @@ packages:
|
|
|
121
124
|
peerDependencies:
|
|
122
125
|
elysia: '>= 1.4.0'
|
|
123
126
|
|
|
124
|
-
'@elysiajs/eden@1.4.
|
|
125
|
-
resolution: {integrity: sha512
|
|
127
|
+
'@elysiajs/eden@1.4.5':
|
|
128
|
+
resolution: {integrity: sha512-hIOeH+S5NU/84A7+t8yB1JjxqjmzRkBF9fnLn6y+AH8EcF39KumOAnciMhIOkhhThVZvXZ3d+GsizRc+Fxoi8g==}
|
|
126
129
|
peerDependencies:
|
|
127
|
-
elysia: '>= 1.4.0
|
|
130
|
+
elysia: '>= 1.4.0'
|
|
128
131
|
|
|
129
132
|
'@elysiajs/node@1.4.2':
|
|
130
133
|
resolution: {integrity: sha512-zqeBAV4/faCcmIEjCp3g6jRwsbaWsd5HqmlEf3CirD9HkTWQNo4T+GN/qGZi7zgd84D3Kzxsny7ZTMXEfrDSXQ==}
|
|
@@ -848,6 +851,10 @@ packages:
|
|
|
848
851
|
resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==}
|
|
849
852
|
engines: {node: '>=10'}
|
|
850
853
|
|
|
854
|
+
dotenv@17.2.3:
|
|
855
|
+
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
|
|
856
|
+
engines: {node: '>=12'}
|
|
857
|
+
|
|
851
858
|
eastasianwidth@0.2.0:
|
|
852
859
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
|
853
860
|
|
|
@@ -1466,7 +1473,7 @@ snapshots:
|
|
|
1466
1473
|
dependencies:
|
|
1467
1474
|
elysia: 1.4.16(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@20.5.0)(openapi-types@12.1.3)(typescript@5.9.3)
|
|
1468
1475
|
|
|
1469
|
-
'@elysiajs/eden@1.4.
|
|
1476
|
+
'@elysiajs/eden@1.4.5(elysia@1.4.16(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@20.5.0)(openapi-types@12.1.3)(typescript@5.9.3))':
|
|
1470
1477
|
dependencies:
|
|
1471
1478
|
elysia: 1.4.16(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@20.5.0)(openapi-types@12.1.3)(typescript@5.9.3)
|
|
1472
1479
|
|
|
@@ -2049,6 +2056,8 @@ snapshots:
|
|
|
2049
2056
|
|
|
2050
2057
|
defer-to-connect@2.0.1: {}
|
|
2051
2058
|
|
|
2059
|
+
dotenv@17.2.3: {}
|
|
2060
|
+
|
|
2052
2061
|
eastasianwidth@0.2.0: {}
|
|
2053
2062
|
|
|
2054
2063
|
elysia@1.4.16(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@20.5.0)(openapi-types@12.1.3)(typescript@5.9.3):
|
|
@@ -1,14 +1,48 @@
|
|
|
1
1
|
import packageJson from "@/../package.json";
|
|
2
|
+
import { config } from "dotenv";
|
|
2
3
|
import { existsSync, mkdirSync } from "node:fs";
|
|
3
4
|
import { resolve } from "node:path";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
+
const envFilesForEachEnvironment = {
|
|
8
|
+
production: ".env",
|
|
9
|
+
development: ".env.development",
|
|
10
|
+
test: ".env.test",
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
if (process.env?.npm_lifecycle_event === "start")
|
|
14
|
+
process.env.NODE_ENV = "production";
|
|
15
|
+
|
|
16
|
+
if (
|
|
17
|
+
[":dev", "dev:"].some((subcommand) =>
|
|
18
|
+
process.env.npm_lifecycle_event?.includes(subcommand),
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
process.env.NODE_ENV = "development";
|
|
22
|
+
|
|
23
|
+
if (
|
|
24
|
+
[":test", "test:"].some((subcommand) =>
|
|
25
|
+
process.env.npm_lifecycle_event?.includes(subcommand),
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
process.env.NODE_ENV = "test";
|
|
29
|
+
|
|
30
|
+
const nodeEnvEnum = ["production", "development", "test"] as const;
|
|
31
|
+
const nodeEnv = process.env.NODE_ENV as (typeof nodeEnvEnum)[number];
|
|
32
|
+
const envFilePath = resolve(process.cwd(), envFilesForEachEnvironment[nodeEnv]);
|
|
33
|
+
|
|
34
|
+
if (!existsSync(envFilePath))
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Cannot find environment variables file in "${envFilePath}".`,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
config({
|
|
40
|
+
path: envFilePath,
|
|
41
|
+
quiet: nodeEnv === "test",
|
|
42
|
+
});
|
|
7
43
|
|
|
8
44
|
const schema = z.object({
|
|
9
|
-
NODE_ENV: z
|
|
10
|
-
.enum(nodeEnv)
|
|
11
|
-
.default(process.env.NODE_ENV as (typeof nodeEnv)[number]),
|
|
45
|
+
NODE_ENV: z.enum(nodeEnvEnum).default(nodeEnv),
|
|
12
46
|
API_NAME: z.string().default(packageJson.name),
|
|
13
47
|
API_PORT: z.coerce.number().default(3333),
|
|
14
48
|
API_CORS_ALLOW_ORIGIN: z.string().default("*"),
|
|
@@ -27,7 +61,7 @@ const schema = z.object({
|
|
|
27
61
|
const parsedEnv = schema.safeParse(process.env);
|
|
28
62
|
|
|
29
63
|
if (!parsedEnv.success) {
|
|
30
|
-
console.error(parsedEnv.error
|
|
64
|
+
console.error(z.treeifyError(parsedEnv.error).properties);
|
|
31
65
|
|
|
32
66
|
throw new Error("Invalid environment variables.");
|
|
33
67
|
}
|
|
@@ -6,8 +6,7 @@ import openapi, { fromTypes } from "@elysiajs/openapi";
|
|
|
6
6
|
import { Elysia } from "elysia";
|
|
7
7
|
import z from "zod";
|
|
8
8
|
import { controllers } from "./controllers";
|
|
9
|
-
import "./
|
|
10
|
-
import { globalErrorHandlerPlugin } from "./plugins/global-error-handler.plugin";
|
|
9
|
+
import { NotFoundError } from "./errors";
|
|
11
10
|
|
|
12
11
|
export const openApiUrlPathname = "/openapi";
|
|
13
12
|
|
|
@@ -37,5 +36,8 @@ export const app = new Elysia({
|
|
|
37
36
|
references: fromTypes(`.${__filename.replace(process.cwd(), "")}`),
|
|
38
37
|
}),
|
|
39
38
|
)
|
|
40
|
-
.
|
|
39
|
+
.onError(({ code }) => {
|
|
40
|
+
if (code === "NOT_FOUND")
|
|
41
|
+
return new NotFoundError().setCode("NONEXISTENT_ROUTE").toController();
|
|
42
|
+
})
|
|
41
43
|
.use(controllers);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { faker } from "@faker-js/faker";
|
|
2
2
|
import { apiTestClient } from "test/integration/api-test-client";
|
|
3
3
|
import { beforeEach, describe, expect, test } from "vitest";
|
|
4
|
-
import {
|
|
4
|
+
import { UnprocessableEntityError } from "../errors";
|
|
5
5
|
import { helloController } from "./hello.controller";
|
|
6
6
|
|
|
7
7
|
const [route] = helloController.routes;
|
|
@@ -27,7 +27,7 @@ describe(`[Controller] ${route.method} ${route.path}`, () => {
|
|
|
27
27
|
|
|
28
28
|
describe("should not be able to get hello world", () => {
|
|
29
29
|
describe("if invalid input", () => {
|
|
30
|
-
const error = new
|
|
30
|
+
const error = new UnprocessableEntityError().setCode("VALIDATION");
|
|
31
31
|
|
|
32
32
|
test("with invalid property show", async () => {
|
|
33
33
|
const result = await sut(defaultInput, {
|
|
@@ -54,9 +54,9 @@ describe(`[Controller] ${route.method} ${route.path}`, () => {
|
|
|
54
54
|
},
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
const error = new
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
const error = new UnprocessableEntityError()
|
|
58
|
+
.setCode("VALIDATION")
|
|
59
|
+
.setMessage("Você não quer exibir o 'Hello world' :(");
|
|
60
60
|
|
|
61
61
|
expect(result.status).toEqual(error.statusCode);
|
|
62
62
|
expect(result.error).toMatchObject({ value: error.toSerialize() });
|
|
@@ -1,51 +1,66 @@
|
|
|
1
1
|
import Elysia, { status } from "elysia";
|
|
2
2
|
import z from "zod";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { UnprocessableEntityError } from "../errors";
|
|
4
|
+
import { controllerErrorHandlerPlugin } from "../plugins/controller-error-handler.plugin";
|
|
5
5
|
|
|
6
|
-
export const helloMultipartController = new Elysia()
|
|
7
|
-
|
|
8
|
-
(
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
export const helloMultipartController = new Elysia()
|
|
7
|
+
.use(controllerErrorHandlerPlugin.plugin())
|
|
8
|
+
.post(
|
|
9
|
+
"/hello/multipart",
|
|
10
|
+
({ query, body }) => {
|
|
11
|
+
const { show } = query;
|
|
12
|
+
const attachment = body.attachment as z.core.File;
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
if (!show)
|
|
15
|
+
return new UnprocessableEntityError()
|
|
16
|
+
.setCode("VALIDATION")
|
|
17
|
+
.setMessage("Você não quer exibir o 'Hello world' :(")
|
|
18
|
+
.setDebug(
|
|
19
|
+
"Utilize o parâmetro de consulta 'show' com o valor 'true' para exibir o 'Hello world'.",
|
|
20
|
+
)
|
|
21
|
+
.toController();
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
query: z.object({
|
|
31
|
-
show: z
|
|
32
|
-
.enum(["true", "false"])
|
|
33
|
-
.transform<boolean>((val) => JSON.parse(val)),
|
|
34
|
-
}),
|
|
35
|
-
body: z.object({
|
|
36
|
-
attachment: z
|
|
37
|
-
.file()
|
|
38
|
-
.max(1024 * 1024 * 50) // 50MB
|
|
39
|
-
.mime("text/plain"),
|
|
40
|
-
}),
|
|
41
|
-
parse: "multipart/form-data",
|
|
42
|
-
response: {
|
|
43
|
-
...globalErrorHandlerPlugin.getErrorSchemas(),
|
|
23
|
+
return status(200, {
|
|
24
|
+
message: "Hello world!",
|
|
25
|
+
attachment: {
|
|
26
|
+
name: attachment.name,
|
|
27
|
+
mimetype: attachment.type,
|
|
28
|
+
size: attachment.size,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
44
31
|
},
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
32
|
+
{
|
|
33
|
+
query: z.object({
|
|
34
|
+
show: z
|
|
35
|
+
.enum(["true", "false"])
|
|
36
|
+
.transform<boolean>((val) => JSON.parse(val)),
|
|
37
|
+
}),
|
|
38
|
+
body: z.object({
|
|
39
|
+
attachment: z
|
|
40
|
+
.file()
|
|
41
|
+
.max(1024 * 1024 * 50) // 50MB
|
|
42
|
+
.mime("text/plain")
|
|
43
|
+
// BUG(@elysiajs/openapi 1.4.11): Elysia stops generating the open API response with "fromTypes" function for the current route when using z.file()
|
|
44
|
+
// biome-ignore lint/suspicious/noExplicitAny: ↑
|
|
45
|
+
.transform((val) => val as any),
|
|
46
|
+
}),
|
|
47
|
+
response: {
|
|
48
|
+
// BUG(@elysiajs/openapi 1.4.11): Elysia is automatically inserting properties that it shouldn't for this class
|
|
49
|
+
422: new UnprocessableEntityError()
|
|
50
|
+
.setCode("VALIDATION")
|
|
51
|
+
.toZodSchema()
|
|
52
|
+
.or(
|
|
53
|
+
new UnprocessableEntityError()
|
|
54
|
+
.setCode("VALIDATION")
|
|
55
|
+
.setMessage("Você não quer exibir o 'Hello world' :(")
|
|
56
|
+
.toZodSchema(),
|
|
57
|
+
),
|
|
58
|
+
},
|
|
59
|
+
parse: "multipart/form-data",
|
|
60
|
+
detail: {
|
|
61
|
+
operationId: "helloMultipartController",
|
|
62
|
+
tags: ["Hello"],
|
|
63
|
+
summary: "Hello world multipart!",
|
|
64
|
+
},
|
|
49
65
|
},
|
|
50
|
-
|
|
51
|
-
);
|
|
66
|
+
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { apiTestClient } from "test/integration/api-test-client";
|
|
2
2
|
import { describe, expect, test } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import { UnprocessableEntityError } from "../errors";
|
|
4
4
|
import { helloController } from "./hello.controller";
|
|
5
5
|
|
|
6
6
|
const [route] = helloController.routes;
|
|
@@ -9,7 +9,7 @@ const sut = apiTestClient.hello.get;
|
|
|
9
9
|
describe(`[Controller] ${route.method} ${route.path}`, () => {
|
|
10
10
|
describe("should not be able to get hello world", () => {
|
|
11
11
|
describe("if invalid input", () => {
|
|
12
|
-
const error = new
|
|
12
|
+
const error = new UnprocessableEntityError().setCode("VALIDATION");
|
|
13
13
|
|
|
14
14
|
test("with invalid property show", async () => {
|
|
15
15
|
const result = await sut({
|
|
@@ -36,9 +36,9 @@ describe(`[Controller] ${route.method} ${route.path}`, () => {
|
|
|
36
36
|
},
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
const error = new
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
const error = new UnprocessableEntityError()
|
|
40
|
+
.setCode("VALIDATION")
|
|
41
|
+
.setMessage("Você não quer exibir o 'Hello world' :(");
|
|
42
42
|
|
|
43
43
|
expect(result.status).toEqual(error.statusCode);
|
|
44
44
|
expect(result.error).toMatchObject({ value: error.toSerialize() });
|
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
import Elysia, { status } from "elysia";
|
|
2
2
|
import z from "zod";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { UnprocessableEntityError } from "../errors";
|
|
4
|
+
import { controllerErrorHandlerPlugin } from "../plugins/controller-error-handler.plugin";
|
|
5
5
|
|
|
6
|
-
export const helloController = new Elysia()
|
|
7
|
-
|
|
8
|
-
(
|
|
9
|
-
|
|
6
|
+
export const helloController = new Elysia()
|
|
7
|
+
.use(controllerErrorHandlerPlugin.plugin())
|
|
8
|
+
.get(
|
|
9
|
+
"/hello",
|
|
10
|
+
({ query }) => {
|
|
11
|
+
const { show } = query;
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
if (!show)
|
|
14
|
+
return new UnprocessableEntityError()
|
|
15
|
+
.setCode("VALIDATION")
|
|
16
|
+
.setMessage("Você não quer exibir o 'Hello world' :(")
|
|
17
|
+
.setDebug(
|
|
18
|
+
"Utilize o parâmetro de consulta 'show' com o valor 'true' para exibir o 'Hello world'.",
|
|
19
|
+
)
|
|
20
|
+
.toController();
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
query: z.object({
|
|
23
|
-
show: z
|
|
24
|
-
.enum(["true", "false"])
|
|
25
|
-
.transform<boolean>((val) => JSON.parse(val)),
|
|
26
|
-
}),
|
|
27
|
-
response: {
|
|
28
|
-
...globalErrorHandlerPlugin.getErrorSchemas(),
|
|
22
|
+
return status(200, { message: "Hello world!" });
|
|
29
23
|
},
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
{
|
|
25
|
+
query: z.object({
|
|
26
|
+
show: z
|
|
27
|
+
.enum(["true", "false"])
|
|
28
|
+
.transform<boolean>((val) => JSON.parse(val)),
|
|
29
|
+
}),
|
|
30
|
+
detail: {
|
|
31
|
+
operationId: "helloController",
|
|
32
|
+
tags: ["Hello"],
|
|
33
|
+
summary: "Hello world!",
|
|
34
|
+
},
|
|
34
35
|
},
|
|
35
|
-
|
|
36
|
-
);
|
|
36
|
+
);
|
|
@@ -7,16 +7,23 @@ export abstract class BaseError<
|
|
|
7
7
|
Name extends string = string,
|
|
8
8
|
StatusCode extends number = number,
|
|
9
9
|
Message extends string = string, // It cannot be undefined because of the native Error type.
|
|
10
|
+
Code extends string | null = null,
|
|
10
11
|
Debug = undefined,
|
|
11
12
|
> extends Error {
|
|
12
13
|
public abstract name: Name;
|
|
13
14
|
public abstract statusCode: StatusCode;
|
|
14
15
|
public message!: Message;
|
|
16
|
+
public code!: Code;
|
|
15
17
|
public debug!: Debug;
|
|
16
18
|
|
|
19
|
+
// biome-ignore lint/complexity/noUselessConstructor: this is necessary to prevent arguments when instantiating.
|
|
20
|
+
public constructor() {
|
|
21
|
+
super();
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
private getUnbuildInstance<
|
|
18
|
-
StatusCode extends number,
|
|
19
25
|
Message extends string,
|
|
26
|
+
Code extends string | null,
|
|
20
27
|
Debug,
|
|
21
28
|
>() {
|
|
22
29
|
return this as unknown as BaseError<
|
|
@@ -24,34 +31,34 @@ export abstract class BaseError<
|
|
|
24
31
|
Name,
|
|
25
32
|
StatusCode,
|
|
26
33
|
Message,
|
|
34
|
+
Code,
|
|
27
35
|
Debug
|
|
28
36
|
>;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
public
|
|
32
|
-
|
|
33
|
-
) {
|
|
34
|
-
this.statusCode = statusCode as unknown as StatusCode;
|
|
39
|
+
public setCode<const CodeInput extends string>(code: CodeInput) {
|
|
40
|
+
this.code = code as unknown as Code;
|
|
35
41
|
|
|
36
|
-
return this.getUnbuildInstance<
|
|
42
|
+
return this.getUnbuildInstance<Message, CodeInput, Debug>();
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
public setMessage<const MessageInput extends string>(message: MessageInput) {
|
|
40
46
|
this.message = message as unknown as Message;
|
|
41
47
|
|
|
42
|
-
return this.getUnbuildInstance<
|
|
48
|
+
return this.getUnbuildInstance<MessageInput, Code, Debug>();
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
public setDebug<const DebugInput>(debug: DebugInput) {
|
|
46
52
|
this.debug = debug as unknown as Debug;
|
|
47
53
|
|
|
48
|
-
return this.getUnbuildInstance<
|
|
54
|
+
return this.getUnbuildInstance<Message, Code, DebugInput>();
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
public toSerialize() {
|
|
52
58
|
return {
|
|
53
59
|
name: this.name,
|
|
54
60
|
statusCode: this.statusCode,
|
|
61
|
+
code: this.code,
|
|
55
62
|
message: this.message,
|
|
56
63
|
...(env.NODE_ENV !== "production" &&
|
|
57
64
|
this.debug && {
|
|
@@ -65,22 +72,32 @@ export abstract class BaseError<
|
|
|
65
72
|
{
|
|
66
73
|
name: Name;
|
|
67
74
|
statusCode: StatusCode;
|
|
68
|
-
|
|
75
|
+
code: Code;
|
|
76
|
+
message: Message;
|
|
77
|
+
debug?: unknown;
|
|
69
78
|
}
|
|
70
79
|
> {
|
|
71
80
|
return status(this.statusCode, this.toSerialize());
|
|
72
81
|
}
|
|
73
82
|
|
|
74
|
-
public toZodSchema(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
83
|
+
public toZodSchema(
|
|
84
|
+
{ nonLiteral }: { nonLiteral: ("code" | "message")[] } = { nonLiteral: [] },
|
|
85
|
+
) {
|
|
86
|
+
const getCodeSchema = () => {
|
|
87
|
+
if (nonLiteral.includes("code")) return z.string();
|
|
88
|
+
|
|
89
|
+
if (this.code) return z.literal(this.code);
|
|
90
|
+
|
|
91
|
+
return z.null();
|
|
78
92
|
};
|
|
79
93
|
|
|
80
94
|
return z.object({
|
|
81
95
|
name: z.literal(this.name),
|
|
82
96
|
statusCode: z.literal(this.statusCode),
|
|
83
|
-
|
|
97
|
+
code: getCodeSchema(),
|
|
98
|
+
message: nonLiteral.includes("message")
|
|
99
|
+
? z.string()
|
|
100
|
+
: z.literal(this.message),
|
|
84
101
|
});
|
|
85
102
|
}
|
|
86
103
|
}
|
|
@@ -101,32 +118,86 @@ export class InternalServerError extends BaseError<
|
|
|
101
118
|
public readonly message = InternalServerError.message;
|
|
102
119
|
}
|
|
103
120
|
|
|
104
|
-
export class
|
|
105
|
-
|
|
106
|
-
typeof
|
|
107
|
-
typeof
|
|
108
|
-
typeof ValidationError.message
|
|
121
|
+
export class BadRequestError extends BaseError<
|
|
122
|
+
BadRequestError,
|
|
123
|
+
typeof BadRequestError.name,
|
|
124
|
+
typeof BadRequestError.statusCode
|
|
109
125
|
> {
|
|
110
|
-
public static readonly name = "
|
|
126
|
+
public static readonly name = "BAD_REQUEST_ERROR";
|
|
127
|
+
public static readonly statusCode = 400;
|
|
128
|
+
|
|
129
|
+
public readonly name = BadRequestError.name;
|
|
130
|
+
public readonly statusCode = BadRequestError.statusCode;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export class UnprocessableEntityError extends BaseError<
|
|
134
|
+
UnprocessableEntityError,
|
|
135
|
+
typeof UnprocessableEntityError.name,
|
|
136
|
+
typeof UnprocessableEntityError.statusCode,
|
|
137
|
+
typeof UnprocessableEntityError.message
|
|
138
|
+
> {
|
|
139
|
+
public static readonly name = "UNPROCESSABLE_ENTITY";
|
|
111
140
|
public static readonly statusCode = 422;
|
|
112
141
|
public static readonly message = "Os dados enviados são inválidos.";
|
|
113
142
|
|
|
114
|
-
public readonly name =
|
|
115
|
-
public readonly statusCode =
|
|
116
|
-
public readonly message =
|
|
143
|
+
public readonly name = UnprocessableEntityError.name;
|
|
144
|
+
public readonly statusCode = UnprocessableEntityError.statusCode;
|
|
145
|
+
public readonly message = UnprocessableEntityError.message;
|
|
117
146
|
}
|
|
118
147
|
|
|
119
|
-
export class
|
|
120
|
-
|
|
121
|
-
typeof
|
|
122
|
-
typeof
|
|
123
|
-
typeof
|
|
148
|
+
export class NotFoundError extends BaseError<
|
|
149
|
+
NotFoundError,
|
|
150
|
+
typeof NotFoundError.name,
|
|
151
|
+
typeof NotFoundError.statusCode,
|
|
152
|
+
typeof NotFoundError.message
|
|
124
153
|
> {
|
|
125
|
-
public static readonly name = "
|
|
154
|
+
public static readonly name = "NOT_FOUND";
|
|
126
155
|
public static readonly statusCode = 404;
|
|
127
156
|
public static readonly message = "Recurso não encontrado.";
|
|
128
157
|
|
|
129
|
-
public readonly name =
|
|
130
|
-
public readonly statusCode =
|
|
131
|
-
public readonly message =
|
|
158
|
+
public readonly name = NotFoundError.name;
|
|
159
|
+
public readonly statusCode = NotFoundError.statusCode;
|
|
160
|
+
public readonly message = NotFoundError.message;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export class ConflictError extends BaseError<
|
|
164
|
+
ConflictError,
|
|
165
|
+
typeof ConflictError.name,
|
|
166
|
+
typeof ConflictError.statusCode
|
|
167
|
+
> {
|
|
168
|
+
public static readonly name = "CONFLICT";
|
|
169
|
+
public static readonly statusCode = 409;
|
|
170
|
+
|
|
171
|
+
public readonly name = ConflictError.name;
|
|
172
|
+
public readonly statusCode = ConflictError.statusCode;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export class ForbiddenError extends BaseError<
|
|
176
|
+
ForbiddenError,
|
|
177
|
+
typeof ForbiddenError.name,
|
|
178
|
+
typeof ForbiddenError.statusCode,
|
|
179
|
+
typeof ForbiddenError.message
|
|
180
|
+
> {
|
|
181
|
+
public static readonly name = "FORBIDDEN";
|
|
182
|
+
public static readonly statusCode = 403;
|
|
183
|
+
public static readonly message = "Acesso negado.";
|
|
184
|
+
|
|
185
|
+
public readonly name = ForbiddenError.name;
|
|
186
|
+
public readonly statusCode = ForbiddenError.statusCode;
|
|
187
|
+
public readonly message = ForbiddenError.message;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export class UnauthorizedError extends BaseError<
|
|
191
|
+
UnauthorizedError,
|
|
192
|
+
typeof UnauthorizedError.name,
|
|
193
|
+
typeof UnauthorizedError.statusCode,
|
|
194
|
+
typeof UnauthorizedError.message
|
|
195
|
+
> {
|
|
196
|
+
public static readonly name = "UNAUTHORIZED";
|
|
197
|
+
public static readonly statusCode = 401;
|
|
198
|
+
public static readonly message = "Não autorizado.";
|
|
199
|
+
|
|
200
|
+
public readonly name = UnauthorizedError.name;
|
|
201
|
+
public readonly statusCode = UnauthorizedError.statusCode;
|
|
202
|
+
public readonly message = UnauthorizedError.message;
|
|
132
203
|
}
|
|
@@ -1,34 +1,20 @@
|
|
|
1
|
-
import type { ZodRestrictFieldsShape } from "@/@types/zod-utils";
|
|
2
1
|
import Elysia, {
|
|
3
2
|
InternalServerError as ElysiaInternalServerError,
|
|
4
3
|
} from "elysia";
|
|
5
4
|
import {
|
|
5
|
+
BadRequestError,
|
|
6
6
|
InternalServerError,
|
|
7
|
-
|
|
8
|
-
ValidationError,
|
|
7
|
+
UnprocessableEntityError,
|
|
9
8
|
} from "../errors";
|
|
10
9
|
|
|
11
|
-
type MetadataResponse = ReturnType<typeof plugin> extends Elysia<
|
|
12
|
-
// biome-ignore-start lint/suspicious/noExplicitAny: unused generics
|
|
13
|
-
any,
|
|
14
|
-
any,
|
|
15
|
-
any,
|
|
16
|
-
// biome-ignore-end lint/suspicious/noExplicitAny: unused generics
|
|
17
|
-
infer Metadata
|
|
18
|
-
>
|
|
19
|
-
? Metadata["response"]
|
|
20
|
-
: never;
|
|
21
|
-
|
|
22
10
|
function plugin() {
|
|
23
|
-
return new Elysia({ name: "
|
|
24
|
-
{ as: "
|
|
11
|
+
return new Elysia({ name: "controller-error-handler-plugin" }).onError(
|
|
12
|
+
{ as: "scoped" },
|
|
25
13
|
({ code, error }) => {
|
|
26
14
|
switch (code) {
|
|
27
|
-
case "NOT_FOUND": {
|
|
28
|
-
return new ResourceNotFoundError().toController();
|
|
29
|
-
}
|
|
30
15
|
case "VALIDATION": {
|
|
31
|
-
return new
|
|
16
|
+
return new UnprocessableEntityError()
|
|
17
|
+
.setCode("VALIDATION")
|
|
32
18
|
.setDebug({
|
|
33
19
|
elysiaCode: code,
|
|
34
20
|
name: error.name,
|
|
@@ -40,8 +26,9 @@ function plugin() {
|
|
|
40
26
|
case "PARSE":
|
|
41
27
|
case "INVALID_COOKIE_SIGNATURE":
|
|
42
28
|
case "INVALID_FILE_TYPE":
|
|
43
|
-
return new
|
|
44
|
-
.
|
|
29
|
+
return new BadRequestError()
|
|
30
|
+
.setCode("VALIDATION")
|
|
31
|
+
.setMessage(UnprocessableEntityError.message)
|
|
45
32
|
.setDebug({
|
|
46
33
|
elysiaCode: code,
|
|
47
34
|
name: error.name,
|
|
@@ -68,15 +55,6 @@ function plugin() {
|
|
|
68
55
|
);
|
|
69
56
|
}
|
|
70
57
|
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
"422": new ValidationError().toZodSchema(),
|
|
74
|
-
"400": new ValidationError().setStatusCode(400).toZodSchema(),
|
|
75
|
-
"500": new InternalServerError().toZodSchema(),
|
|
76
|
-
} satisfies Omit<ZodRestrictFieldsShape<MetadataResponse>, 404>;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export const globalErrorHandlerPlugin = Object.freeze({
|
|
58
|
+
export const controllerErrorHandlerPlugin = Object.freeze({
|
|
80
59
|
plugin,
|
|
81
|
-
getErrorSchemas,
|
|
82
60
|
});
|