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