@zentjs/zentjs 0.0.2
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 +28 -0
- package/README.md +1584 -0
- package/package.json +85 -0
- package/src/core/application.mjs +1039 -0
- package/src/core/context.mjs +33 -0
- package/src/errors/error-handler.mjs +88 -0
- package/src/errors/http-error.mjs +86 -0
- package/src/hooks/lifecycle.mjs +175 -0
- package/src/http/request.mjs +166 -0
- package/src/http/response.mjs +151 -0
- package/src/index.mjs +33 -0
- package/src/middleware/pipeline.mjs +77 -0
- package/src/plugins/body-parser.mjs +165 -0
- package/src/plugins/cors.mjs +183 -0
- package/src/plugins/manager.mjs +112 -0
- package/src/plugins/request-metrics.mjs +74 -0
- package/src/router/index.mjs +198 -0
- package/src/router/node.mjs +72 -0
- package/src/router/radix-tree.mjs +313 -0
- package/src/utils/http-status.mjs +31 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ZentRequest } from '../http/request.mjs';
|
|
2
|
+
import { ZentResponse } from '../http/response.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Objeto de contexto criado por requisição.
|
|
6
|
+
* Responsabilidade única: agregar req, res, estado e referência ao app.
|
|
7
|
+
* Serve como único argumento para handlers e middlewares.
|
|
8
|
+
*/
|
|
9
|
+
export class Context {
|
|
10
|
+
/** @type {ZentRequest} */
|
|
11
|
+
req;
|
|
12
|
+
|
|
13
|
+
/** @type {ZentResponse} */
|
|
14
|
+
res;
|
|
15
|
+
|
|
16
|
+
/** @type {import('./application.mjs').Zent} */
|
|
17
|
+
app;
|
|
18
|
+
|
|
19
|
+
/** @type {Record<string, *>} Espaço livre para middlewares e handlers */
|
|
20
|
+
state;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {import('node:http').IncomingMessage} rawReq
|
|
24
|
+
* @param {import('node:http').ServerResponse} rawRes
|
|
25
|
+
* @param {import('./application.mjs').Zent} app
|
|
26
|
+
*/
|
|
27
|
+
constructor(rawReq, rawRes, app) {
|
|
28
|
+
this.req = new ZentRequest(rawReq);
|
|
29
|
+
this.res = new ZentResponse(rawRes);
|
|
30
|
+
this.app = app;
|
|
31
|
+
this.state = {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ErrorHandler — Handler centralizado de erros para o framework.
|
|
3
|
+
*
|
|
4
|
+
* Responsabilidades:
|
|
5
|
+
* - Converter qualquer erro em uma resposta HTTP coerente
|
|
6
|
+
* - Suportar handler customizado definido pelo usuário
|
|
7
|
+
* - Garantir que erros nunca crashem o processo
|
|
8
|
+
* - Invocar hooks onError do lifecycle
|
|
9
|
+
*
|
|
10
|
+
* @module errors/error-handler
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { HttpError, InternalServerError } from './http-error.mjs';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Gerencia o tratamento de erros do framework.
|
|
17
|
+
* O usuário pode fornecer um handler customizado via setErrorHandler().
|
|
18
|
+
*/
|
|
19
|
+
export class ErrorHandler {
|
|
20
|
+
/** @type {((error: Error, ctx: object) => void | Promise<void>) | null} */
|
|
21
|
+
#customHandler;
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
this.#customHandler = null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Define um handler customizado de erros.
|
|
29
|
+
* O handler recebe (error, ctx) e deve enviar a resposta via ctx.res.
|
|
30
|
+
*
|
|
31
|
+
* @param {(error: Error, ctx: object) => void | Promise<void>} fn
|
|
32
|
+
* @throws {TypeError} Se fn não for uma função
|
|
33
|
+
*/
|
|
34
|
+
setErrorHandler(fn) {
|
|
35
|
+
if (typeof fn !== 'function') {
|
|
36
|
+
throw new TypeError(`Error handler must be a function, got ${typeof fn}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.#customHandler = fn;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Trata um erro, enviando a resposta HTTP apropriada.
|
|
44
|
+
*
|
|
45
|
+
* Fluxo:
|
|
46
|
+
* 1. Se a resposta já foi enviada, não faz nada
|
|
47
|
+
* 2. Se existe handler customizado, delega ao handler
|
|
48
|
+
* 3. Caso contrário, usa o handler padrão do framework
|
|
49
|
+
*
|
|
50
|
+
* @param {Error} error - O erro a ser tratado
|
|
51
|
+
* @param {object} ctx - Contexto da requisição (req, res, app, state)
|
|
52
|
+
* @returns {Promise<void>}
|
|
53
|
+
*/
|
|
54
|
+
async handle(error, ctx) {
|
|
55
|
+
// Se a resposta já foi enviada, não há nada a fazer
|
|
56
|
+
if (ctx.res.sent) return;
|
|
57
|
+
|
|
58
|
+
// Normaliza: erros não-HttpError viram InternalServerError
|
|
59
|
+
const httpError =
|
|
60
|
+
error instanceof HttpError
|
|
61
|
+
? error
|
|
62
|
+
: new InternalServerError(error.message || 'Internal Server Error');
|
|
63
|
+
|
|
64
|
+
if (this.#customHandler) {
|
|
65
|
+
try {
|
|
66
|
+
await this.#customHandler(httpError, ctx);
|
|
67
|
+
} catch {
|
|
68
|
+
// Se o handler customizado falhar, usa o padrão
|
|
69
|
+
this.#defaultHandler(httpError, ctx);
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.#defaultHandler(httpError, ctx);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Handler padrão — envia resposta JSON com statusCode, error e message.
|
|
79
|
+
*
|
|
80
|
+
* @param {HttpError} error
|
|
81
|
+
* @param {object} ctx
|
|
82
|
+
*/
|
|
83
|
+
#defaultHandler(error, ctx) {
|
|
84
|
+
if (ctx.res.sent) return;
|
|
85
|
+
|
|
86
|
+
ctx.res.status(error.statusCode).json(error.toJSON());
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { HttpStatus, HttpStatusText } from '../utils/http-status.mjs';
|
|
2
|
+
|
|
3
|
+
export class HttpError extends Error {
|
|
4
|
+
static showStackTrace = false;
|
|
5
|
+
|
|
6
|
+
constructor(statusCode, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.statusCode = statusCode;
|
|
9
|
+
this.error = HttpStatusText[statusCode] || 'Unknown Error';
|
|
10
|
+
this.name = this.constructor.name;
|
|
11
|
+
|
|
12
|
+
if (HttpError.showStackTrace) {
|
|
13
|
+
Error.captureStackTrace(this, this.constructor);
|
|
14
|
+
} else {
|
|
15
|
+
this.stack = undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
toJSON() {
|
|
20
|
+
const payload = {
|
|
21
|
+
statusCode: this.statusCode,
|
|
22
|
+
error: this.error,
|
|
23
|
+
message: this.message,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (HttpError.showStackTrace && this.stack) {
|
|
27
|
+
payload.stack = this.stack;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return payload;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class BadRequestError extends HttpError {
|
|
35
|
+
constructor(message = 'Bad Request') {
|
|
36
|
+
super(HttpStatus.BAD_REQUEST, message);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class UnauthorizedError extends HttpError {
|
|
41
|
+
constructor(message = 'Unauthorized') {
|
|
42
|
+
super(HttpStatus.UNAUTHORIZED, message);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class ForbiddenError extends HttpError {
|
|
47
|
+
constructor(message = 'Forbidden') {
|
|
48
|
+
super(HttpStatus.FORBIDDEN, message);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class NotFoundError extends HttpError {
|
|
53
|
+
constructor(message = 'Not Found') {
|
|
54
|
+
super(HttpStatus.NOT_FOUND, message);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class MethodNotAllowedError extends HttpError {
|
|
59
|
+
constructor(message = 'Method Not Allowed') {
|
|
60
|
+
super(HttpStatus.METHOD_NOT_ALLOWED, message);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class ConflictError extends HttpError {
|
|
65
|
+
constructor(message = 'Conflict') {
|
|
66
|
+
super(HttpStatus.CONFLICT, message);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class UnprocessableEntityError extends HttpError {
|
|
71
|
+
constructor(message = 'Unprocessable Entity') {
|
|
72
|
+
super(HttpStatus.UNPROCESSABLE_ENTITY, message);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class TooManyRequestsError extends HttpError {
|
|
77
|
+
constructor(message = 'Too Many Requests') {
|
|
78
|
+
super(HttpStatus.TOO_MANY_REQUESTS, message);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class InternalServerError extends HttpError {
|
|
83
|
+
constructor(message = 'Internal Server Error') {
|
|
84
|
+
super(HttpStatus.INTERNAL_SERVER_ERROR, message);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lifecycle — Gerencia hooks de ciclo de vida da requisição.
|
|
3
|
+
*
|
|
4
|
+
* Hooks são executados em fases bem definidas, permitindo
|
|
5
|
+
* interceptar e modificar o fluxo em cada etapa.
|
|
6
|
+
*
|
|
7
|
+
* Fases (ordem de execução):
|
|
8
|
+
* 1. onRequest — Primeira fase, antes de qualquer processamento
|
|
9
|
+
* 2. preParsing — Antes de parsear o body
|
|
10
|
+
* 3. preValidation — Antes de validar input
|
|
11
|
+
* 4. preHandler — Após validação, antes do handler
|
|
12
|
+
* 5. onSend — Antes de enviar a resposta (pode modificar payload)
|
|
13
|
+
* 6. onResponse — Após envio da resposta (cleanup, métricas)
|
|
14
|
+
* 7. onError — Chamado quando ocorre erro em qualquer fase
|
|
15
|
+
*
|
|
16
|
+
* @module hooks/lifecycle
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Fases válidas do lifecycle */
|
|
20
|
+
export const HOOK_PHASES = Object.freeze([
|
|
21
|
+
'onRequest',
|
|
22
|
+
'preParsing',
|
|
23
|
+
'preValidation',
|
|
24
|
+
'preHandler',
|
|
25
|
+
'onSend',
|
|
26
|
+
'onResponse',
|
|
27
|
+
'onError',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Gerenciador de hooks de lifecycle.
|
|
32
|
+
* Responsabilidade única: registrar e executar hooks por fase.
|
|
33
|
+
*/
|
|
34
|
+
export class Lifecycle {
|
|
35
|
+
/** @type {Map<string, Function[]>} */
|
|
36
|
+
#hooks;
|
|
37
|
+
|
|
38
|
+
constructor() {
|
|
39
|
+
this.#hooks = new Map();
|
|
40
|
+
|
|
41
|
+
for (const phase of HOOK_PHASES) {
|
|
42
|
+
this.#hooks.set(phase, []);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Registra um hook para uma fase do lifecycle.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} phase - Nome da fase (ex: 'onRequest', 'preHandler')
|
|
50
|
+
* @param {Function} fn - Função do hook
|
|
51
|
+
* @throws {Error} Se a fase não for válida
|
|
52
|
+
* @throws {TypeError} Se fn não for uma função
|
|
53
|
+
*/
|
|
54
|
+
addHook(phase, fn) {
|
|
55
|
+
if (!this.#hooks.has(phase)) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Invalid hook phase: "${phase}". Valid phases: ${HOOK_PHASES.join(', ')}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (typeof fn !== 'function') {
|
|
62
|
+
throw new TypeError(`Hook must be a function, got ${typeof fn}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.#hooks.get(phase).push(fn);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Retorna os hooks registrados para uma fase.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} phase
|
|
72
|
+
* @returns {Function[]}
|
|
73
|
+
*/
|
|
74
|
+
getHooks(phase) {
|
|
75
|
+
return this.#hooks.get(phase) || [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Verifica se uma fase possui hooks registrados.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} phase
|
|
82
|
+
* @returns {boolean}
|
|
83
|
+
*/
|
|
84
|
+
hasHooks(phase) {
|
|
85
|
+
const hooks = this.#hooks.get(phase);
|
|
86
|
+
return hooks !== undefined && hooks.length > 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Executa todos os hooks de uma fase sequencialmente.
|
|
91
|
+
* Cada hook recebe os argumentos passados (tipicamente ctx, ou ctx + payload).
|
|
92
|
+
*
|
|
93
|
+
* Para a fase 'onSend', o retorno do hook substitui o payload
|
|
94
|
+
* (permite transformação encadeada).
|
|
95
|
+
*
|
|
96
|
+
* @param {string} phase - Fase do lifecycle
|
|
97
|
+
* @param {object} ctx - Contexto da requisição
|
|
98
|
+
* @param {...*} args - Argumentos adicionais (ex: payload para onSend, error para onError)
|
|
99
|
+
* @returns {Promise<*>} O último payload (para onSend) ou undefined
|
|
100
|
+
*/
|
|
101
|
+
async run(phase, ctx, ...args) {
|
|
102
|
+
const hooks = this.getHooks(phase);
|
|
103
|
+
|
|
104
|
+
if (hooks.length === 0) return args[0];
|
|
105
|
+
|
|
106
|
+
if (phase === 'onSend') {
|
|
107
|
+
return this.#runOnSend(hooks, ctx, args[0]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (phase === 'onError') {
|
|
111
|
+
return this.#runOnError(hooks, ctx, args[0]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Fases normais: onRequest, preParsing, preValidation, preHandler, onResponse
|
|
115
|
+
for (const hook of hooks) {
|
|
116
|
+
await hook(ctx);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Executa hooks de onSend encadeando o payload.
|
|
122
|
+
* Cada hook pode retornar um payload modificado.
|
|
123
|
+
*
|
|
124
|
+
* @param {Function[]} hooks
|
|
125
|
+
* @param {object} ctx
|
|
126
|
+
* @param {*} payload
|
|
127
|
+
* @returns {Promise<*>}
|
|
128
|
+
*/
|
|
129
|
+
async #runOnSend(hooks, ctx, payload) {
|
|
130
|
+
let current = payload;
|
|
131
|
+
|
|
132
|
+
for (const hook of hooks) {
|
|
133
|
+
const result = await hook(ctx, current);
|
|
134
|
+
// Se o hook retornar algo, substitui o payload
|
|
135
|
+
if (result !== undefined) {
|
|
136
|
+
current = result;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return current;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Executa hooks de onError sequencialmente.
|
|
145
|
+
* Cada hook recebe (ctx, error).
|
|
146
|
+
*
|
|
147
|
+
* @param {Function[]} hooks
|
|
148
|
+
* @param {object} ctx
|
|
149
|
+
* @param {Error} error
|
|
150
|
+
* @returns {Promise<void>}
|
|
151
|
+
*/
|
|
152
|
+
async #runOnError(hooks, ctx, error) {
|
|
153
|
+
for (const hook of hooks) {
|
|
154
|
+
await hook(ctx, error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Cria uma cópia do lifecycle com os mesmos hooks.
|
|
160
|
+
* Útil para encapsulamento de plugins (herança de escopo pai).
|
|
161
|
+
*
|
|
162
|
+
* @returns {Lifecycle}
|
|
163
|
+
*/
|
|
164
|
+
clone() {
|
|
165
|
+
const cloned = new Lifecycle();
|
|
166
|
+
|
|
167
|
+
for (const [phase, hooks] of this.#hooks) {
|
|
168
|
+
for (const hook of hooks) {
|
|
169
|
+
cloned.addHook(phase, hook);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return cloned;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
function resolvePathFromUrl(rawUrl) {
|
|
2
|
+
if (!rawUrl) return '/';
|
|
3
|
+
|
|
4
|
+
const queryIndex = rawUrl.indexOf('?');
|
|
5
|
+
const path = queryIndex === -1 ? rawUrl : rawUrl.slice(0, queryIndex);
|
|
6
|
+
|
|
7
|
+
return path || '/';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function resolveQueryFromUrl(rawUrl) {
|
|
11
|
+
if (!rawUrl) return {};
|
|
12
|
+
|
|
13
|
+
const queryIndex = rawUrl.indexOf('?');
|
|
14
|
+
if (queryIndex === -1 || queryIndex === rawUrl.length - 1) return {};
|
|
15
|
+
|
|
16
|
+
return Object.fromEntries(new URLSearchParams(rawUrl.slice(queryIndex + 1)));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveHostnameFromHostHeader(host) {
|
|
20
|
+
if (!host) return 'localhost';
|
|
21
|
+
|
|
22
|
+
const rawHost = Array.isArray(host) ? host[0] : host;
|
|
23
|
+
if (!rawHost) return 'localhost';
|
|
24
|
+
|
|
25
|
+
if (rawHost.startsWith('[')) {
|
|
26
|
+
const end = rawHost.indexOf(']');
|
|
27
|
+
if (end !== -1) {
|
|
28
|
+
return rawHost.slice(0, end + 1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const colonIndex = rawHost.indexOf(':');
|
|
33
|
+
if (colonIndex === -1) return rawHost;
|
|
34
|
+
|
|
35
|
+
return rawHost.slice(0, colonIndex);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Wrapper sobre http.IncomingMessage.
|
|
40
|
+
* Responsabilidade única: leitura e parse dos dados da requisição.
|
|
41
|
+
*/
|
|
42
|
+
export class ZentRequest {
|
|
43
|
+
/** @type {import('node:http').IncomingMessage} */
|
|
44
|
+
#raw;
|
|
45
|
+
|
|
46
|
+
/** @type {string | undefined} */
|
|
47
|
+
#pathCache;
|
|
48
|
+
|
|
49
|
+
/** @type {Record<string, string> | undefined} */
|
|
50
|
+
#queryCache;
|
|
51
|
+
|
|
52
|
+
/** @type {string | undefined} */
|
|
53
|
+
#hostnameCache;
|
|
54
|
+
|
|
55
|
+
/** @type {Record<string, string>} */
|
|
56
|
+
#params = {};
|
|
57
|
+
|
|
58
|
+
/** @type {*} */
|
|
59
|
+
#body = undefined;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {import('node:http').IncomingMessage} raw
|
|
63
|
+
*/
|
|
64
|
+
constructor(raw) {
|
|
65
|
+
this.#raw = raw;
|
|
66
|
+
this.#pathCache = undefined;
|
|
67
|
+
this.#queryCache = undefined;
|
|
68
|
+
this.#hostnameCache = undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Objeto IncomingMessage original (escape hatch) */
|
|
72
|
+
get raw() {
|
|
73
|
+
return this.#raw;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @returns {string} Método HTTP em uppercase */
|
|
77
|
+
get method() {
|
|
78
|
+
return this.#raw.method;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** @returns {string} URL completa (path + query) */
|
|
82
|
+
get url() {
|
|
83
|
+
return this.#raw.url;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** @returns {string} Path sem query string */
|
|
87
|
+
get path() {
|
|
88
|
+
if (this.#pathCache === undefined) {
|
|
89
|
+
this.#pathCache = resolvePathFromUrl(this.#raw.url);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return this.#pathCache;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @returns {Record<string, string>} Query params como objeto */
|
|
96
|
+
get query() {
|
|
97
|
+
if (this.#queryCache === undefined) {
|
|
98
|
+
this.#queryCache = resolveQueryFromUrl(this.#raw.url);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return this.#queryCache;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** @returns {import('node:http').IncomingHttpHeaders} */
|
|
105
|
+
get headers() {
|
|
106
|
+
return this.#raw.headers;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** @returns {Record<string, string>} Route params populados pelo router */
|
|
110
|
+
get params() {
|
|
111
|
+
return this.#params;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
set params(value) {
|
|
115
|
+
this.#params = value;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @returns {string} IP do cliente */
|
|
119
|
+
get ip() {
|
|
120
|
+
return this.#raw.socket.remoteAddress;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** @returns {string} Hostname da requisição */
|
|
124
|
+
get hostname() {
|
|
125
|
+
if (this.#hostnameCache === undefined) {
|
|
126
|
+
this.#hostnameCache = resolveHostnameFromHostHeader(
|
|
127
|
+
this.#raw.headers.host
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return this.#hostnameCache;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** @returns {string} 'http' ou 'https' */
|
|
135
|
+
get protocol() {
|
|
136
|
+
return this.#raw.socket.encrypted ? 'https' : 'http';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** @returns {*} Body parseado (definido pelo body-parser middleware) */
|
|
140
|
+
get body() {
|
|
141
|
+
return this.#body;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
set body(value) {
|
|
145
|
+
this.#body = value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Retorna o valor de um header (case-insensitive).
|
|
150
|
+
* @param {string} name
|
|
151
|
+
* @returns {string | undefined}
|
|
152
|
+
*/
|
|
153
|
+
get(name) {
|
|
154
|
+
return this.#raw.headers[name.toLowerCase()];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Verifica se o Content-Type bate com o tipo informado.
|
|
159
|
+
* @param {string} type - Ex: 'json', 'application/json'
|
|
160
|
+
* @returns {boolean}
|
|
161
|
+
*/
|
|
162
|
+
is(type) {
|
|
163
|
+
const contentType = this.get('content-type') || '';
|
|
164
|
+
return contentType.includes(type);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { HttpStatus } from '../utils/http-status.mjs';
|
|
2
|
+
|
|
3
|
+
const CONTENT_TYPE = 'Content-Type';
|
|
4
|
+
const MIME_JSON = 'application/json; charset=utf-8';
|
|
5
|
+
const MIME_HTML = 'text/html; charset=utf-8';
|
|
6
|
+
const MIME_TEXT = 'text/plain; charset=utf-8';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Wrapper sobre http.ServerResponse.
|
|
10
|
+
* Responsabilidade única: construção e envio da resposta HTTP.
|
|
11
|
+
* API fluente (chainable) para status e headers.
|
|
12
|
+
*/
|
|
13
|
+
export class ZentResponse {
|
|
14
|
+
/** @type {import('node:http').ServerResponse} */
|
|
15
|
+
#raw;
|
|
16
|
+
|
|
17
|
+
/** @type {number} */
|
|
18
|
+
#statusCode = HttpStatus.OK;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {import('node:http').ServerResponse} raw
|
|
22
|
+
*/
|
|
23
|
+
constructor(raw) {
|
|
24
|
+
this.#raw = raw;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Objeto ServerResponse original (escape hatch) */
|
|
28
|
+
get raw() {
|
|
29
|
+
return this.#raw;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @returns {boolean} Já enviou a resposta? */
|
|
33
|
+
get sent() {
|
|
34
|
+
return this.#raw.writableEnded;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @returns {number} Status code atual */
|
|
38
|
+
get statusCode() {
|
|
39
|
+
return this.#statusCode;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Define o status code.
|
|
44
|
+
* @param {number} code
|
|
45
|
+
* @returns {this}
|
|
46
|
+
*/
|
|
47
|
+
status(code) {
|
|
48
|
+
if (this.sent) return this;
|
|
49
|
+
|
|
50
|
+
this.#statusCode = code;
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Define um header.
|
|
56
|
+
* @param {string} name
|
|
57
|
+
* @param {string | number} value
|
|
58
|
+
* @returns {this}
|
|
59
|
+
*/
|
|
60
|
+
header(name, value) {
|
|
61
|
+
if (this.sent) return this;
|
|
62
|
+
|
|
63
|
+
this.#raw.setHeader(name, value);
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Atalho para Content-Type.
|
|
69
|
+
* @param {string} contentType
|
|
70
|
+
* @returns {this}
|
|
71
|
+
*/
|
|
72
|
+
type(contentType) {
|
|
73
|
+
return this.header(CONTENT_TYPE, contentType);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Envia resposta JSON.
|
|
78
|
+
* @param {*} data
|
|
79
|
+
*/
|
|
80
|
+
json(data) {
|
|
81
|
+
if (this.sent) return;
|
|
82
|
+
|
|
83
|
+
const body = JSON.stringify(data);
|
|
84
|
+
this.type(MIME_JSON);
|
|
85
|
+
this.#end(body);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Envia string ou Buffer.
|
|
90
|
+
* @param {string | Buffer} data
|
|
91
|
+
*/
|
|
92
|
+
send(data) {
|
|
93
|
+
if (this.sent) return;
|
|
94
|
+
|
|
95
|
+
if (!this.#raw.getHeader(CONTENT_TYPE)) {
|
|
96
|
+
this.type(Buffer.isBuffer(data) ? 'application/octet-stream' : MIME_TEXT);
|
|
97
|
+
}
|
|
98
|
+
this.#end(data);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Envia resposta HTML.
|
|
103
|
+
* @param {string} data
|
|
104
|
+
*/
|
|
105
|
+
html(data) {
|
|
106
|
+
if (this.sent) return;
|
|
107
|
+
|
|
108
|
+
this.type(MIME_HTML);
|
|
109
|
+
this.#end(data);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Redireciona para outra URL.
|
|
114
|
+
* @param {string} url
|
|
115
|
+
* @param {number} [code=HttpStatus.FOUND]
|
|
116
|
+
*/
|
|
117
|
+
redirect(url, code = HttpStatus.FOUND) {
|
|
118
|
+
if (this.sent) return;
|
|
119
|
+
|
|
120
|
+
this.#statusCode = code;
|
|
121
|
+
this.header('Location', url);
|
|
122
|
+
this.#end();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resposta sem body.
|
|
127
|
+
* @param {number} [code=HttpStatus.NO_CONTENT]
|
|
128
|
+
*/
|
|
129
|
+
empty(code = HttpStatus.NO_CONTENT) {
|
|
130
|
+
if (this.sent) return;
|
|
131
|
+
|
|
132
|
+
this.#statusCode = code;
|
|
133
|
+
this.#end();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Finaliza a resposta. Método interno compartilhado.
|
|
138
|
+
* @param {string | Buffer} [body]
|
|
139
|
+
*/
|
|
140
|
+
#end(body) {
|
|
141
|
+
if (this.sent) return;
|
|
142
|
+
|
|
143
|
+
this.#raw.writeHead(this.#statusCode);
|
|
144
|
+
|
|
145
|
+
if (body !== undefined) {
|
|
146
|
+
this.#raw.end(body);
|
|
147
|
+
} else {
|
|
148
|
+
this.#raw.end();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|