azurajs 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.
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "azurajs",
3
+ "version": "1.0.0",
4
+ "description": "Modern TypeScript-first web framework with decorator-based routing, zero dependencies, and built for performance",
5
+ "main": "src/index.ts",
6
+ "module": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./src/index.ts",
12
+ "require": "./src/index.ts",
13
+ "types": "./src/index.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "src",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "prepublishOnly": "echo '✅ Ready to publish'",
23
+ "test": "bun test"
24
+ },
25
+ "keywords": [
26
+ "typescript",
27
+ "framework",
28
+ "web",
29
+ "api",
30
+ "rest",
31
+ "decorators",
32
+ "router",
33
+ "http",
34
+ "server",
35
+ "backend",
36
+ "bun",
37
+ "node",
38
+ "zero-dependency",
39
+ "lightweight",
40
+ "fast"
41
+ ],
42
+ "author": "0xviny.dev@gmail.com",
43
+ "license": "MIT",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/0xviny/azurajs.git"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/0xviny/azurajs/issues"
50
+ },
51
+ "homepage": "https://github.com/0xviny/azurajs#readme",
52
+ "engines": {
53
+ "node": ">=18.0.0",
54
+ "bun": ">=1.0.0"
55
+ },
56
+ "devDependencies": {
57
+ "@types/bun": "latest"
58
+ },
59
+ "peerDependencies": {
60
+ "typescript": "^5.0.0"
61
+ }
62
+ }
@@ -0,0 +1,141 @@
1
+ import type { AzuraClient } from "../infra/Server";
2
+ import type { RequestServer } from "../types/http/request.type";
3
+ import type { ResponseServer } from "../types/http/response.type";
4
+ import type { ParamDefinition, ParamSource, RouteDefinition } from "../types/routes.type";
5
+
6
+ const PREFIX = new WeakMap<Function, string>();
7
+ const ROUTES = new WeakMap<Function, RouteDefinition[]>();
8
+ const PARAMS = new WeakMap<Function, Map<string, ParamDefinition[]>>();
9
+
10
+ export function Controller(prefix = ""): ClassDecorator {
11
+ return (target) => {
12
+ PREFIX.set(target as Function, prefix);
13
+ };
14
+ }
15
+
16
+ function createMethodDecorator(method: string) {
17
+ return function (path = ""): MethodDecorator {
18
+ return (target, propertyKey) => {
19
+ const ctor =
20
+ typeof target === "function" ? (target as Function) : (target as any).constructor;
21
+ const key = String(propertyKey);
22
+ const routes = ROUTES.get(ctor) ?? [];
23
+ const params = PARAMS.get(ctor)?.get(key) ?? [];
24
+
25
+ const exists = routes.some(
26
+ (r) => r.method === method && r.path === path && r.propertyKey === key
27
+ );
28
+ if (!exists) {
29
+ routes.push({
30
+ method,
31
+ path,
32
+ propertyKey: key,
33
+ params,
34
+ });
35
+ ROUTES.set(ctor, routes);
36
+ }
37
+ };
38
+ };
39
+ }
40
+
41
+ function createParamDecorator(type: ParamSource) {
42
+ return function (name?: string): ParameterDecorator {
43
+ return (target, propertyKey, parameterIndex) => {
44
+ const ctor =
45
+ typeof target === "function" ? (target as Function) : (target as any).constructor;
46
+ let map = PARAMS.get(ctor);
47
+ if (!map) {
48
+ map = new Map<string, ParamDefinition[]>();
49
+ PARAMS.set(ctor, map);
50
+ }
51
+
52
+ const key = String(propertyKey);
53
+ const list = map.get(key) ?? [];
54
+ const exists = list.some(
55
+ (p) => p.index === parameterIndex && p.type === type && p.name === name
56
+ );
57
+ if (!exists) {
58
+ list.push({
59
+ index: parameterIndex,
60
+ type,
61
+ name,
62
+ });
63
+ map.set(key, list);
64
+ }
65
+ };
66
+ };
67
+ }
68
+
69
+ export const Get = createMethodDecorator("GET");
70
+ export const Post = createMethodDecorator("POST");
71
+ export const Put = createMethodDecorator("PUT");
72
+ export const Delete = createMethodDecorator("DELETE");
73
+ export const Patch = createMethodDecorator("PATCH");
74
+ export const Head = createMethodDecorator("HEAD");
75
+ export const Options = createMethodDecorator("OPTIONS");
76
+
77
+ export const Req = createParamDecorator("req");
78
+ export const Res = createParamDecorator("res");
79
+ export const Next = createParamDecorator("next");
80
+ export const Param = createParamDecorator("param");
81
+ export const Query = createParamDecorator("query");
82
+ export const Body = createParamDecorator("body");
83
+ export const Headers = createParamDecorator("headers");
84
+ export const Ip = createParamDecorator("ip");
85
+ export const UserAgent = createParamDecorator("useragent");
86
+
87
+ export function applyDecorators(app: AzuraClient, controllers: Array<new () => any>) {
88
+ controllers.forEach((ControllerClass) => {
89
+ const prefix = PREFIX.get(ControllerClass) ?? "";
90
+ const instance = new ControllerClass();
91
+ const routes = ROUTES.get(ControllerClass) ?? [];
92
+
93
+ routes.forEach((r) => {
94
+ const handler = async (
95
+ req: RequestServer,
96
+ res: ResponseServer,
97
+ next: (err?: unknown) => void
98
+ ) => {
99
+ try {
100
+ const params = (r.params ?? []).slice().sort((a, b) => a.index - b.index);
101
+ const args = params.map((p) => {
102
+ switch (p.type) {
103
+ case "req":
104
+ return req;
105
+ case "res":
106
+ return res;
107
+ case "next":
108
+ return next;
109
+ case "param":
110
+ return p.name ? req.params[p.name] : req.params;
111
+ case "query":
112
+ return p.name ? req.query[p.name] : req.query;
113
+ case "body": {
114
+ const body = req.body as Record<string, unknown> | undefined;
115
+ return p.name ? body?.[p.name] : body;
116
+ }
117
+ case "headers":
118
+ return p.name ? req.headers[p.name.toLowerCase()] : req.headers;
119
+ case "ip":
120
+ return req.ip;
121
+ case "useragent":
122
+ return req.headers["user-agent"];
123
+ default:
124
+ return undefined;
125
+ }
126
+ });
127
+
128
+ const fn = (instance as Record<string, unknown>)[r.propertyKey] as (
129
+ ...args: unknown[]
130
+ ) => unknown;
131
+ const result = fn(...args);
132
+ if (result instanceof Promise) await result;
133
+ } catch (err) {
134
+ next(err);
135
+ }
136
+ };
137
+
138
+ app.addRoute(r.method, prefix + r.path, handler);
139
+ });
140
+ });
141
+ }
@@ -0,0 +1,24 @@
1
+ export {
2
+ Controller,
3
+ Get,
4
+ Post,
5
+ Put,
6
+ Delete,
7
+ Patch,
8
+ Head,
9
+ Options,
10
+ Req,
11
+ Res,
12
+ Next,
13
+ Param,
14
+ Query,
15
+ Body,
16
+ Headers,
17
+ Ip,
18
+ UserAgent,
19
+ applyDecorators,
20
+ } from "./Route";
21
+
22
+ export type { RouteDefinition, ParamDefinition, ParamSource } from "../types/routes.type";
23
+ export type { RequestServer } from "../types/http/request.type";
24
+ export type { ResponseServer } from "../types/http/response.type";
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ // Core Server & Router
2
+ export * from "./infra/Server";
3
+ export { Router } from "./infra/Router";
4
+
5
+ // Decorators
6
+ export * from "./decorators";
7
+
8
+ // Middleware
9
+ export * from "./middleware";
10
+
11
+ // Types
12
+ export * from "./types/http/request.type";
13
+ export * from "./types/http/response.type";
14
+ export * from "./types/common.type";
15
+ export * from "./types/routes.type";
16
+ export * from "./types/validations.type";
17
+
18
+ // Configuration
19
+ export * from "./shared/config/ConfigModule";
20
+
21
+ // Utils
22
+ export { logger } from "./utils/Logger";
23
+ export { parseQS } from "./utils/Parser";
24
+ export { HttpError } from "./infra/utils/HttpError";
25
+ export { parseCookiesHeader } from "./utils/cookies/ParserCookie";
26
+ export { serializeCookie } from "./utils/cookies/SerializeCookie";
27
+ export { validateSchema } from "./utils/validators/SchemaValidator";
28
+ export { validateDto, getDtoValidators } from "./utils/validators/DTOValidator";
@@ -0,0 +1,56 @@
1
+ import { HttpError } from "./utils/HttpError";
2
+ import { Node, type Handler } from "./utils/route/Node";
3
+
4
+ interface MatchResult {
5
+ handlers: Handler[];
6
+ params: Record<string, string>;
7
+ }
8
+
9
+ export class Router {
10
+ private root = new Node();
11
+
12
+ add(method: string, path: string, ...handlers: Handler[]) {
13
+ const segments = path.split("/").filter(Boolean);
14
+ let node = this.root;
15
+
16
+ for (const seg of segments) {
17
+ let child: Node;
18
+
19
+ if (seg.startsWith(":")) {
20
+ child = new Node();
21
+ child.isParam = true;
22
+ child.paramName = seg.slice(1);
23
+ } else {
24
+ child = node.children.get(seg) ?? new Node();
25
+ }
26
+
27
+ node.children.set(seg.startsWith(":") ? ":" : seg, child);
28
+ node = child;
29
+ }
30
+
31
+ node.handlers.set(method.toUpperCase(), handlers);
32
+ }
33
+
34
+ find(method: string, path: string): MatchResult {
35
+ const segments = path.split("/").filter(Boolean);
36
+ let node = this.root;
37
+
38
+ const params: Record<string, string> = {};
39
+ for (const seg of segments) {
40
+ if (node.children.has(seg)) {
41
+ node = node.children.get(seg)!;
42
+ } else if (node.children.has(":")) {
43
+ node = node.children.get(":")!;
44
+
45
+ if (node.isParam && node.paramName) {
46
+ params[node.paramName] = seg;
47
+ }
48
+ } else {
49
+ throw new HttpError(404, "Route not found");
50
+ }
51
+ }
52
+
53
+ const handlers = node.handlers.get(method.toUpperCase()) as Handler[];
54
+ return { handlers, params };
55
+ }
56
+ }
@@ -0,0 +1,274 @@
1
+ import http from "node:http";
2
+ import cluster from "node:cluster";
3
+ import os from "node:os";
4
+
5
+ import { ConfigModule } from "../shared/config/ConfigModule";
6
+ import { getOpenPort } from "./utils/GetOpenPort";
7
+ import type { RequestServer } from "../types/http/request.type";
8
+ import type { CookieOptions, ResponseServer } from "../types/http/response.type";
9
+ import { HttpError } from "./utils/HttpError";
10
+ import { logger } from "../utils/Logger";
11
+ import { parseQS } from "../utils/Parser";
12
+ import { serializeCookie } from "../utils/cookies/SerializeCookie";
13
+ import { parseCookiesHeader } from "../utils/cookies/ParserCookie";
14
+ import { adaptRequestHandler } from "./utils/RequestHandler";
15
+ import { Router } from "./Router";
16
+ import type { RequestHandler } from "../types/common.type";
17
+ import { getIP } from "./utils/GetIp";
18
+ import type { Handler } from "./utils/route/Node";
19
+
20
+ export { createLoggingMiddleware } from "../middleware/LoggingMiddleware";
21
+
22
+ export class AzuraClient {
23
+ private opts: ReturnType<ConfigModule["getAll"]>;
24
+
25
+ private server?: http.Server;
26
+ private port: number = 3000;
27
+ private initPromise: Promise<void>;
28
+
29
+ public router = new Router();
30
+ private middlewares: RequestHandler[] = [];
31
+
32
+ constructor() {
33
+ const config = new ConfigModule();
34
+ try {
35
+ config.initSync();
36
+ } catch (error: any) {
37
+ console.error("[Azura] ❌ Falha ao carregar configuração:");
38
+ console.error(" ", error.message);
39
+ process.exit(1);
40
+ }
41
+ this.opts = config.getAll();
42
+ this.initPromise = this.init();
43
+ }
44
+
45
+ public getConfig() {
46
+ return this.opts;
47
+ }
48
+
49
+ private async init() {
50
+ this.port = await getOpenPort(this.opts.server?.port || 3000);
51
+ if (this.opts.server?.cluster && cluster.isPrimary) {
52
+ for (let i = 0; i < os.cpus().length; i++) cluster.fork();
53
+ cluster.on("exit", () => cluster.fork());
54
+ return;
55
+ }
56
+ this.server = http.createServer();
57
+ this.server.on("request", this.handle.bind(this));
58
+ }
59
+
60
+ public use(mw: RequestHandler) {
61
+ this.middlewares.push(mw);
62
+ }
63
+
64
+ public addRoute(method: string, path: string, ...handlers: RequestHandler[]) {
65
+ const adapted = handlers.map(adaptRequestHandler);
66
+ this.router.add(method, path, ...(adapted as unknown as Handler[]));
67
+ }
68
+
69
+ public get = (p: string, ...h: RequestHandler[]) => this.addRoute("GET", p, ...h);
70
+ public post = (p: string, ...h: RequestHandler[]) => this.addRoute("POST", p, ...h);
71
+ public put = (p: string, ...h: RequestHandler[]) => this.addRoute("PUT", p, ...h);
72
+ public delete = (p: string, ...h: RequestHandler[]) => this.addRoute("DELETE", p, ...h);
73
+ public patch = (p: string, ...h: RequestHandler[]) => this.addRoute("PATCH", p, ...h);
74
+
75
+ public async listen(port = this.port) {
76
+ await this.initPromise;
77
+
78
+ try {
79
+ if (!this.server) {
80
+ logger("error", "Server not initialized");
81
+ return;
82
+ }
83
+
84
+ const who = cluster.isPrimary ? "master" : "worker";
85
+ this.server.listen(port, () =>
86
+ logger("info", `[${who}] listening on http://localhost:${port}`)
87
+ );
88
+
89
+ if (this.opts.server?.ipHost) getIP(port);
90
+ } catch (error: Error | any) {
91
+ logger("error", "Server failed to start: " + error?.message || String(error));
92
+ process.exit(1);
93
+ }
94
+ }
95
+
96
+ private async handle(rawReq: RequestServer, rawRes: ResponseServer) {
97
+ rawReq.originalUrl = rawReq.url || "";
98
+ rawReq.protocol = this.opts.server?.https ? "https" : "http";
99
+ rawReq.secure = rawReq.protocol === "https";
100
+ rawReq.hostname = String(rawReq.headers["host"] || "").split(":")[0] || "";
101
+ rawReq.subdomains = rawReq.hostname ? rawReq.hostname.split(".").slice(0, -2) : [];
102
+ const ipsRaw = rawReq.headers["x-forwarded-for"];
103
+ rawReq.ips = typeof ipsRaw === "string" ? ipsRaw.split(/\s*,\s*/) : [];
104
+ rawReq.get = rawReq.header = (name: string) => {
105
+ const v = rawReq.headers[name.toLowerCase()];
106
+ if (Array.isArray(v)) return v[0];
107
+ return typeof v === "string" ? v : undefined;
108
+ };
109
+
110
+ rawRes.status = (code: number) => {
111
+ rawRes.statusCode = code;
112
+ return rawRes;
113
+ };
114
+
115
+ rawRes.set = rawRes.header = (field: string, value: string | number | string[]) => {
116
+ rawRes.setHeader(field, value);
117
+ return rawRes;
118
+ };
119
+
120
+ rawRes.get = (field: string) => {
121
+ const v = rawRes.getHeader(field);
122
+ if (Array.isArray(v)) return v[0];
123
+ return typeof v === "number" ? String(v) : (v as string | undefined);
124
+ };
125
+
126
+ rawRes.type = rawRes.contentType = (t: string) => {
127
+ rawRes.setHeader("Content-Type", t);
128
+ return rawRes;
129
+ };
130
+
131
+ rawRes.location = (u: string) => {
132
+ rawRes.setHeader("Location", u);
133
+ return rawRes;
134
+ };
135
+
136
+ rawRes.redirect = ((a: number | string, b?: string) => {
137
+ if (typeof a === "number") {
138
+ rawRes.statusCode = a;
139
+ rawRes.setHeader("Location", b!);
140
+ } else {
141
+ rawRes.statusCode = 302;
142
+ rawRes.setHeader("Location", a);
143
+ }
144
+ rawRes.end();
145
+ return rawRes;
146
+ }) as ResponseServer["redirect"];
147
+
148
+ rawRes.cookie = (name: string, val: string, opts: CookieOptions = {}) => {
149
+ const s = serializeCookie(name, val, opts);
150
+ const prev = rawRes.getHeader("Set-Cookie");
151
+ if (prev) {
152
+ const list = Array.isArray(prev) ? prev.concat(s) : [String(prev), s];
153
+ rawRes.setHeader("Set-Cookie", list);
154
+ } else {
155
+ rawRes.setHeader("Set-Cookie", s);
156
+ }
157
+ return rawRes;
158
+ };
159
+
160
+ rawRes.clearCookie = (name: string, opts: CookieOptions = {}) => {
161
+ return rawRes.cookie(name, "", { ...opts, expires: new Date(1), maxAge: 0 });
162
+ };
163
+
164
+ rawRes.send = (b: any) => {
165
+ if (b === undefined || b === null) {
166
+ rawRes.end();
167
+ return rawRes;
168
+ }
169
+ if (typeof b === "object") {
170
+ rawRes.setHeader("Content-Type", "application/json");
171
+ rawRes.end(JSON.stringify(b));
172
+ } else {
173
+ rawRes.end(String(b));
174
+ }
175
+ return rawRes;
176
+ };
177
+
178
+ rawRes.json = (b: any) => {
179
+ rawRes.setHeader("Content-Type", "application/json");
180
+ rawRes.end(JSON.stringify(b));
181
+ return rawRes;
182
+ };
183
+
184
+ const [urlPath, qs] = (rawReq.url || "").split("?");
185
+ rawReq.path = urlPath || "/";
186
+ const rawQuery = parseQS(qs || "");
187
+ const safeQuery: Record<string, string> = {};
188
+ for (const k in rawQuery) {
189
+ const v = rawQuery[k];
190
+ safeQuery[k] = Array.isArray(v) ? v[0] || "" : (v as string) || "";
191
+ }
192
+ rawReq.query = safeQuery;
193
+
194
+ rawReq.cookies = parseCookiesHeader((rawReq.headers["cookie"] as string) || "");
195
+ rawReq.params = {};
196
+
197
+ const ipRaw = rawReq.headers["x-forwarded-for"] || rawReq.socket.remoteAddress || "";
198
+ const ipStr = Array.isArray(ipRaw) ? ipRaw[0] : ipRaw;
199
+ rawReq.ip = String(ipStr).split(",")[0]?.trim() || "";
200
+
201
+ rawReq.body = {};
202
+ if (["POST", "PUT", "PATCH"].includes((rawReq.method || "").toUpperCase())) {
203
+ await new Promise<void>((resolve) => {
204
+ let buf = "";
205
+ rawReq.on("data", (chunk: Buffer | string) => {
206
+ buf += chunk;
207
+ });
208
+ rawReq.on("end", () => {
209
+ try {
210
+ const ct = String(rawReq.headers["content-type"] || "");
211
+ if (ct.includes("application/json")) {
212
+ rawReq.body = JSON.parse(buf || "{}");
213
+ } else {
214
+ const parsed = parseQS(buf || "");
215
+ const b: Record<string, string> = {};
216
+ for (const k in parsed) {
217
+ const v = parsed[k];
218
+ b[k] = Array.isArray(v) ? v[0] || "" : (v as string) || "";
219
+ }
220
+ rawReq.body = b;
221
+ }
222
+ } catch {
223
+ rawReq.body = {};
224
+ }
225
+ resolve();
226
+ });
227
+ rawReq.on("error", (err: Error) => {
228
+ logger("error", "Body parse error: " + err.message);
229
+ resolve();
230
+ });
231
+ });
232
+ }
233
+
234
+ const errorHandler = (err: any) => {
235
+ logger("error", err?.message || String(err));
236
+ rawRes
237
+ .status(err instanceof HttpError ? err.status : 500)
238
+ .json(
239
+ err instanceof HttpError
240
+ ? err.payload ?? { error: err.message || "Internal Server Error" }
241
+ : { error: err?.message || "Internal Server Error" }
242
+ );
243
+ };
244
+
245
+ try {
246
+ const { handlers, params } = this.router.find(rawReq.method || "GET", rawReq.path);
247
+ rawReq.params = params || {};
248
+ const chain = [
249
+ ...this.middlewares.map(adaptRequestHandler),
250
+ ...handlers.map(adaptRequestHandler),
251
+ ];
252
+ let idx = 0;
253
+ const next = async (err?: any) => {
254
+ if (err) return errorHandler(err);
255
+ if (idx >= chain.length) return;
256
+ const fn = chain[idx++];
257
+ try {
258
+ await fn({
259
+ request: rawReq,
260
+ response: rawRes,
261
+ req: rawReq,
262
+ res: rawRes,
263
+ next,
264
+ });
265
+ } catch (e) {
266
+ return errorHandler(e);
267
+ }
268
+ };
269
+ await next();
270
+ } catch (err) {
271
+ errorHandler(err);
272
+ }
273
+ }
274
+ }
@@ -0,0 +1,15 @@
1
+ import os from "node:os";
2
+ import cluster from "node:cluster";
3
+ import { logger } from "../../utils/Logger";
4
+
5
+ export function getIP(port: number) {
6
+ const networkInterfaces = os.networkInterfaces();
7
+ Object.values(networkInterfaces).forEach((ifaceList) => {
8
+ ifaceList?.forEach((iface) => {
9
+ if (iface.family === "IPv4" && !iface.internal) {
10
+ const who = cluster.isPrimary ? "master" : "worker";
11
+ logger("info", `[${who}] accessible at http://${iface.address}:${port}`);
12
+ }
13
+ });
14
+ });
15
+ }
@@ -0,0 +1,15 @@
1
+ import net from "node:net";
2
+
3
+ export function getOpenPort(startPort: number): Promise<number> {
4
+ return new Promise((resolve) => {
5
+ const server = net.createServer();
6
+ server.unref();
7
+
8
+ server.on("error", () => {
9
+ resolve(getOpenPort(startPort + 1));
10
+ });
11
+ server.listen(startPort, () => {
12
+ server.close(() => resolve(startPort));
13
+ });
14
+ });
15
+ }
@@ -0,0 +1,9 @@
1
+ export class HttpError extends Error {
2
+ status: number;
3
+ payload?: any;
4
+ constructor(status: number, message?: string, payload?: any) {
5
+ super(message ?? String(message ?? "Error"));
6
+ this.status = status;
7
+ this.payload = payload;
8
+ }
9
+ }
@@ -0,0 +1,33 @@
1
+ import type { RequestServer } from "../../types/http/request.type";
2
+ import type { ResponseServer } from "../../types/http/response.type";
3
+
4
+ export function adaptRequestHandler(mw: any) {
5
+ if (typeof mw !== "function") return mw;
6
+ return async (ctx: {
7
+ request: RequestServer;
8
+ response: ResponseServer;
9
+ next: (err?: any) => Promise<void>;
10
+ }) => {
11
+ try {
12
+ if (mw.length === 3) {
13
+ await new Promise<void>((resolve, reject) => {
14
+ try {
15
+ const maybe = mw(ctx.request, ctx.response, (err?: any) => {
16
+ if (err) reject(err);
17
+ else resolve();
18
+ });
19
+ if (maybe && typeof (maybe as Promise<any>).then === "function") {
20
+ (maybe as Promise<any>).then(() => resolve()).catch(reject);
21
+ }
22
+ } catch (e) {
23
+ reject(e);
24
+ }
25
+ });
26
+ } else {
27
+ await mw(ctx);
28
+ }
29
+ } catch (err) {
30
+ throw err;
31
+ }
32
+ };
33
+ }
@@ -0,0 +1,15 @@
1
+ import type { RequestServer } from "../../../types/http/request.type";
2
+ import type { ResponseServer } from "../../../types/http/response.type";
3
+
4
+ export type Handler = (ctx: {
5
+ req: RequestServer;
6
+ res: ResponseServer;
7
+ next?: (err?: any) => void;
8
+ }) => Promise<void> | void;
9
+
10
+ export class Node {
11
+ children = new Map<string, Node>();
12
+ handlers = new Map<string, Handler[]>();
13
+ paramName?: string;
14
+ isParam?: boolean;
15
+ }
@@ -0,0 +1,108 @@
1
+ import type { RequestServer } from "../types/http/request.type";
2
+ import type { ResponseServer } from "../types/http/response.type";
3
+ import type { ConfigTypes } from "../shared/config/ConfigModule";
4
+ import type { RequestHandler } from "../types/common.type";
5
+
6
+ export function createLoggingMiddleware(config: ConfigTypes): RequestHandler {
7
+ const isEnabled = config.logging?.enabled ?? true;
8
+ const showDetails = config.logging?.showDetails ?? true;
9
+ const env = config.environment || "development";
10
+
11
+ const reset = "\x1b[0m";
12
+ const bold = "\x1b[1m";
13
+ const dim = "\x1b[2m";
14
+ const colors: Record<string, string> = {
15
+ GET: "\x1b[36m",
16
+ POST: "\x1b[32m",
17
+ PUT: "\x1b[33m",
18
+ DELETE: "\x1b[31m",
19
+ PATCH: "\x1b[35m",
20
+ DEFAULT: "\x1b[37m",
21
+ };
22
+
23
+ const fn = async (...args: any[]) => {
24
+ let req: RequestServer | undefined;
25
+ let res: ResponseServer | undefined;
26
+ let next: ((err?: any) => Promise<void> | void) | undefined;
27
+
28
+ if (args.length === 1 && typeof args[0] === "object" && args[0] !== null) {
29
+ const ctx = args[0];
30
+ req = (ctx.request || ctx.req) as RequestServer | undefined;
31
+ res = (ctx.response || ctx.res) as ResponseServer | undefined;
32
+ next = ctx.next;
33
+ } else {
34
+ req = args[0] as RequestServer | undefined;
35
+ res = args[1] as ResponseServer | undefined;
36
+ next = args[2] as typeof next;
37
+ }
38
+
39
+ if (!req || !res) {
40
+ if (typeof next === "function") await next();
41
+ return;
42
+ }
43
+
44
+ if (!isEnabled) {
45
+ if (typeof next === "function") await next();
46
+ return;
47
+ }
48
+
49
+ const start = Date.now();
50
+ const methodRaw = String(req.method || "GET").toUpperCase();
51
+ const methodPad = methodRaw.padEnd(6, " ");
52
+ const methodColor = colors[methodRaw] || colors.DEFAULT;
53
+ const url = String((req as any).originalUrl || req.url || "/");
54
+ const ip = String((req as any).ip || "").replace("::ffff:", "");
55
+ const time = new Date().toLocaleTimeString("pt-BR", { hour12: false });
56
+
57
+ try {
58
+ if (env === "development") {
59
+ const header = `${dim}·${reset} ${bold}${time}${reset} ${methodColor}${methodPad}${reset} ${bold}${url}${reset}`;
60
+ console.log(header);
61
+ if (showDetails) {
62
+ try {
63
+ if (req.query && Object.keys(req.query).length > 0) {
64
+ console.log(`${dim} › query:${reset} ${JSON.stringify(req.query)}`);
65
+ }
66
+ } catch {}
67
+ try {
68
+ const params = (req as any).params;
69
+ if (params && Object.keys(params).length > 0) {
70
+ console.log(`${dim} › params:${reset} ${JSON.stringify(params)}`);
71
+ }
72
+ } catch {}
73
+ try {
74
+ if (req.body && Object.keys(req.body).length > 0) {
75
+ console.log(`${dim} › body:${reset} ${JSON.stringify(req.body)}`);
76
+ }
77
+ } catch {}
78
+ }
79
+ } else {
80
+ console.log(
81
+ `${dim}·${reset} ${time} ${methodColor}${methodPad}${reset} ${url} ${dim}• IP:${ip}${reset}`
82
+ );
83
+ }
84
+ } catch {}
85
+
86
+ try {
87
+ if (typeof (res as any).on === "function") {
88
+ (res as any).on("finish", () => {
89
+ const duration = Date.now() - start;
90
+ const statusCode = Number((res as any).statusCode || 0);
91
+ const statusColor =
92
+ statusCode >= 500 ? "\x1b[31m" : statusCode >= 400 ? "\x1b[33m" : "\x1b[32m";
93
+ const statusText = `${statusColor}${statusCode}${reset}`;
94
+ const durationText = `${dim}(${duration}ms)${reset}`;
95
+ const ipShort = ip || "-";
96
+ const line = `${dim}→${reset} ${statusText} ${durationText} ${dim}• IP:${ipShort}${reset}`;
97
+ console.log(line);
98
+ });
99
+ }
100
+ } catch {}
101
+
102
+ if (typeof next === "function") {
103
+ await next();
104
+ }
105
+ };
106
+
107
+ return fn as unknown as RequestHandler;
108
+ }
@@ -0,0 +1,14 @@
1
+ export * from "./LoggingMiddleware";
2
+
3
+ const MIDDLEWARES = new WeakMap<object, string[]>();
4
+
5
+ export function Middleware() {
6
+ return function (_: Function, context: ClassMethodDecoratorContext) {
7
+ context.addInitializer(function () {
8
+ const list = MIDDLEWARES.get(this as object) ?? [];
9
+ list.push(String(context.name));
10
+
11
+ MIDDLEWARES.set(this as object, list);
12
+ });
13
+ };
14
+ }
@@ -0,0 +1,113 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Config Files Extensions Supported
6
+ * ex: azura.config.*extension
7
+ */
8
+
9
+ type SupportedConfigFile = ".ts" | ".json" | ".yaml" | ".yml";
10
+
11
+ export type ConfigTypes = {
12
+ environment?: "development" | "production";
13
+ server?: {
14
+ port?: number;
15
+ cluster?: boolean;
16
+ ipHost?: boolean;
17
+ https?: boolean;
18
+ };
19
+ plugins?: {
20
+ rateLimit?: {
21
+ enabled: boolean;
22
+ limit: number;
23
+ timeframe: number;
24
+ };
25
+ cors?: {
26
+ enabled: boolean;
27
+ origins: string[];
28
+ };
29
+ };
30
+ logging?: {
31
+ enabled?: boolean;
32
+ showDetails?: boolean;
33
+ };
34
+ };
35
+
36
+ export class ConfigModule {
37
+ private config: ConfigTypes = {};
38
+
39
+ /**
40
+ * Load config files first (azura.config.*)
41
+ * Recivied error if config file not found or invalid format
42
+ * @param configFiles
43
+ */
44
+
45
+ initSync(): void {
46
+ const cdw = process.cwd();
47
+ const configFiles = [
48
+ "azura.config.ts",
49
+ "azura.config.json",
50
+ "azura.config.yaml",
51
+ "azura.config.yml",
52
+ ];
53
+
54
+ let loaded = false;
55
+ for (const fileName of configFiles) {
56
+ const filePath = path.join(cdw, fileName);
57
+ if (!existsSync(filePath)) continue;
58
+
59
+ const extension = path.extname(fileName) as SupportedConfigFile;
60
+ const raw = readFileSync(filePath, "utf8");
61
+
62
+ try {
63
+ let parsed: ConfigTypes;
64
+ switch (extension) {
65
+ case ".ts":
66
+ const mod = require(filePath);
67
+ parsed = mod.default || mod;
68
+ break;
69
+ case ".json":
70
+ parsed = JSON.parse(raw);
71
+ break;
72
+ case ".yaml":
73
+ case ".yml":
74
+ parsed = require("js-yaml").load(raw);
75
+ break;
76
+ default:
77
+ throw new Error(`Invalid config file extension: ${extension}`);
78
+ }
79
+
80
+ this.config = { ...this.config, ...parsed };
81
+ loaded = true;
82
+ break;
83
+ } catch (error: Error | any) {
84
+ throw new Error(`Error loading config file: ${filePath}\n${error.message}`);
85
+ }
86
+ }
87
+
88
+ if (!loaded) {
89
+ throw new Error("Nothing config file found in the current directory.");
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Get all configs from loaded config file
95
+ * @returns ConfigTypes
96
+ */
97
+
98
+ getAll(): ConfigTypes {
99
+ return this.config;
100
+ }
101
+
102
+ /**
103
+ * Return a specific config from loaded config file
104
+ *
105
+ * @template T
106
+ * @param {T} key - key of the config to retrieve
107
+ * @returns {ConfigTypes[T]}
108
+ */
109
+
110
+ get<T extends keyof ConfigTypes>(key: T): ConfigTypes[T] {
111
+ return this.config[key];
112
+ }
113
+ }
@@ -0,0 +1,4 @@
1
+ import type { RequestServer } from "./http/request.type";
2
+ import type { ResponseServer } from "./http/response.type";
3
+
4
+ export type RequestHandler = (req: RequestServer, res: ResponseServer, next: (err?: Error | any) => void) => void;
@@ -0,0 +1,21 @@
1
+ import { IncomingMessage, type IncomingHttpHeaders } from "node:http";
2
+
3
+ export interface RequestServer extends IncomingMessage {
4
+ path: string;
5
+ originalUrl: string;
6
+ method: string;
7
+ protocol: "http" | "https";
8
+ secure: boolean;
9
+ hostname: string;
10
+ subdomains: string[];
11
+ ip: string;
12
+ ips?: string[];
13
+ params: Record<string, string>;
14
+ query: Record<string, string>;
15
+ body: unknown;
16
+ cookies: Record<string, string>;
17
+ headers: IncomingHttpHeaders;
18
+
19
+ get(name: string): string | undefined;
20
+ header(name: string): string | undefined;
21
+ }
@@ -0,0 +1,32 @@
1
+ import { ServerResponse } from "node:http";
2
+
3
+ export interface CookieOptions {
4
+ domain?: string;
5
+ encode?: (value: string) => string;
6
+ expires?: Date;
7
+ httpOnly?: boolean;
8
+ maxAge?: number;
9
+ path?: string;
10
+ sameSite?: "strict" | "lax" | "none";
11
+ secure?: boolean;
12
+ }
13
+
14
+ export interface ResponseServer extends ServerResponse {
15
+ end(cb?: () => void): this;
16
+ end(chunk: any, cb?: () => void): this;
17
+ end(chunk: any, encoding: string, cb?: () => void): this;
18
+ headersSent: boolean;
19
+ status(code: number): this;
20
+ set(field: string, value: string | number | string[]): this;
21
+ header(field: string, value: string | number | string[]): this;
22
+ get(field: string): string | undefined;
23
+ type(type: string): this;
24
+ contentType(type: string): this;
25
+ redirect(url: string): this;
26
+ redirect(status: number, url: string): this;
27
+ location(url: string): this;
28
+ cookie(name: string, value: string, options?: CookieOptions): this;
29
+ clearCookie(name: string, options?: CookieOptions): this;
30
+ send(body: any): this;
31
+ json(body: any): this;
32
+ }
@@ -0,0 +1,23 @@
1
+ export type ParamSource =
2
+ | "param"
3
+ | "query"
4
+ | "body"
5
+ | "headers"
6
+ | "req"
7
+ | "res"
8
+ | "next"
9
+ | "ip"
10
+ | "useragent";
11
+
12
+ export interface ParamDefinition {
13
+ index: number;
14
+ type: ParamSource;
15
+ name?: string;
16
+ }
17
+
18
+ export interface RouteDefinition {
19
+ method: string;
20
+ path: string;
21
+ propertyKey: string;
22
+ params: ParamDefinition[];
23
+ }
@@ -0,0 +1,2 @@
1
+ export type PrimitiveType = "string" | "number" | "boolean";
2
+ export type Schema = PrimitiveType | Schema[] | { [key: string]: Schema };
@@ -0,0 +1,5 @@
1
+ export function logger(level: "info" | "warn" | "error", msg: string) {
2
+ if (level === "error") console.error("[Azura]", msg);
3
+ else if (level === "warn") console.warn("[Azura]", msg);
4
+ else console.log("[Azura]", msg);
5
+ }
@@ -0,0 +1,19 @@
1
+ export function parseQS(qs: string): Record<string, string | string[]> {
2
+ const out: Record<string, string | string[]> = {};
3
+ if (!qs) return out;
4
+ const parts = qs.replace(/^\?/, "").split("&");
5
+ for (const p of parts) {
6
+ if (!p) continue;
7
+ const idx = p.indexOf("=");
8
+ const k = idx === -1 ? decodeURIComponent(p) : decodeURIComponent(p.slice(0, idx));
9
+ const v = idx === -1 ? "" : decodeURIComponent(p.slice(idx + 1));
10
+ if (Object.prototype.hasOwnProperty.call(out, k)) {
11
+ const cur = out[k];
12
+ if (Array.isArray(cur)) cur.push(v);
13
+ else out[k] = [cur as string, v];
14
+ } else {
15
+ out[k] = v;
16
+ }
17
+ }
18
+ return out;
19
+ }
@@ -0,0 +1,9 @@
1
+ export function parseCookiesHeader(header: string | undefined): Record<string, string> {
2
+ if (!header) return {};
3
+ return header.split(";").reduce<Record<string, string>>((acc, pair) => {
4
+ const [k, ...vals] = pair.trim().split("=");
5
+ if (!k) return acc;
6
+ acc[k] = decodeURIComponent(vals.join("="));
7
+ return acc;
8
+ }, {});
9
+ }
@@ -0,0 +1,15 @@
1
+ import type { CookieOptions } from "../../types/http/response.type";
2
+
3
+ export function serializeCookie(name: string, val: string, opts: CookieOptions = {}): string {
4
+ const encode = opts.encode ?? encodeURIComponent;
5
+ let str = `${name}=${encode(val)}`;
6
+ if (opts.maxAge != null && !Number.isNaN(Number(opts.maxAge)))
7
+ str += `; Max-Age=${Math.floor(Number(opts.maxAge))}`;
8
+ if (opts.domain) str += `; Domain=${opts.domain}`;
9
+ if (opts.path) str += `; Path=${opts.path}`;
10
+ if (opts.expires) str += `; Expires=${opts.expires.toUTCString()}`;
11
+ if (opts.httpOnly) str += `; HttpOnly`;
12
+ if (opts.secure) str += `; Secure`;
13
+ if (opts.sameSite) str += `; SameSite=${opts.sameSite}`;
14
+ return str;
15
+ }
@@ -0,0 +1,30 @@
1
+ const DTOS = new WeakMap<Function, Map<string, Array<{ index: number; dto: unknown }>>>();
2
+
3
+ export function validateDto(dto: unknown): ParameterDecorator {
4
+ return (target, propertyKey, parameterIndex) => {
5
+ const ctor = typeof target === "function" ? (target as Function) : (target as any).constructor;
6
+
7
+ let map = DTOS.get(ctor);
8
+ if (!map) {
9
+ map = new Map();
10
+ DTOS.set(ctor, map);
11
+ }
12
+
13
+ const key = String(propertyKey);
14
+ const list = map.get(key) ?? [];
15
+
16
+ list.push({
17
+ index: parameterIndex,
18
+ dto,
19
+ });
20
+
21
+ map.set(key, list);
22
+ };
23
+ }
24
+
25
+ export function getDtoValidators(
26
+ ctor: Function,
27
+ propertyKey: string
28
+ ): Array<{ index: number; dto: unknown }> {
29
+ return DTOS.get(ctor)?.get(propertyKey) ?? [];
30
+ }
@@ -0,0 +1,37 @@
1
+ import { HttpError } from "../../infra/utils/HttpError";
2
+ import type { Schema } from "../../types/validations.type";
3
+
4
+ function isSchemaObject(schema: Schema): schema is Record<string, Schema> {
5
+ return typeof schema === "object" && !Array.isArray(schema);
6
+ }
7
+
8
+ export function validateSchema(schema: Schema, data: unknown): void {
9
+ if (typeof data !== "object" || data === null) {
10
+ throw new HttpError(400, "Payload inválido");
11
+ }
12
+
13
+ if (!isSchemaObject(schema)) {
14
+ throw new Error("Schema inválido: deve ser um objeto no topo");
15
+ }
16
+
17
+ const obj = data as Record<string, unknown>;
18
+
19
+ for (const key of Object.keys(schema)) {
20
+ const rule = schema[key];
21
+ const val = obj[key];
22
+
23
+ if (!rule) continue;
24
+
25
+ if (typeof rule === "string") {
26
+ if (typeof val !== rule) {
27
+ throw new HttpError(400, `${key} deve ser ${rule}`);
28
+ }
29
+ } else if (Array.isArray(rule)) {
30
+ if (!Array.isArray(val)) {
31
+ throw new HttpError(400, `${key} deve ser array`);
32
+ }
33
+ } else if (isSchemaObject(rule)) {
34
+ validateSchema(rule, val);
35
+ }
36
+ }
37
+ }