@axeom/core 0.1.1
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 +21 -0
- package/dist/index.cjs +804 -0
- package/dist/index.d.cts +291 -0
- package/dist/index.d.ts +291 -0
- package/dist/index.js +769 -0
- package/package.json +30 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
$SERVER: () => $SERVER,
|
|
24
|
+
Axeom: () => Axeom,
|
|
25
|
+
AxeomError: () => AxeomError,
|
|
26
|
+
BadRequestError: () => BadRequestError,
|
|
27
|
+
ConflictError: () => ConflictError,
|
|
28
|
+
ForbiddenError: () => ForbiddenError,
|
|
29
|
+
InternalServerError: () => InternalServerError,
|
|
30
|
+
NotFoundError: () => NotFoundError,
|
|
31
|
+
UnauthorizedError: () => UnauthorizedError,
|
|
32
|
+
default: () => index_default
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(index_exports);
|
|
35
|
+
|
|
36
|
+
// src/errors.ts
|
|
37
|
+
var AxeomError = class extends Error {
|
|
38
|
+
constructor(message, status = 500, code = "INTERNAL_SERVER_ERROR", details) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.message = message;
|
|
41
|
+
this.status = status;
|
|
42
|
+
this.code = code;
|
|
43
|
+
this.details = details;
|
|
44
|
+
this.name = this.constructor.name;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var NotFoundError = class extends AxeomError {
|
|
48
|
+
constructor(message = "Resource not found", details) {
|
|
49
|
+
super(message, 404, "NOT_FOUND", details);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var UnauthorizedError = class extends AxeomError {
|
|
53
|
+
constructor(message = "Unauthorized access", details) {
|
|
54
|
+
super(message, 401, "UNAUTHORIZED", details);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var BadRequestError = class extends AxeomError {
|
|
58
|
+
constructor(message = "Bad request", details) {
|
|
59
|
+
super(message, 400, "BAD_REQUEST", details);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var ForbiddenError = class extends AxeomError {
|
|
63
|
+
constructor(message = "Forbidden", details) {
|
|
64
|
+
super(message, 403, "FORBIDDEN", details);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var InternalServerError = class extends AxeomError {
|
|
68
|
+
constructor(message = "Internal server error") {
|
|
69
|
+
super(message, 500, "INTERNAL_SERVER_ERROR");
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var ConflictError = class extends AxeomError {
|
|
73
|
+
constructor(message = "Conflict", details) {
|
|
74
|
+
super(message, 409, "CONFLICT", details);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/hooks.ts
|
|
79
|
+
var Hooks = class {
|
|
80
|
+
onRequests = [];
|
|
81
|
+
onBeforeMatch = [];
|
|
82
|
+
onResponses = [];
|
|
83
|
+
beforeHandles = [];
|
|
84
|
+
afterHandles = [];
|
|
85
|
+
addRequestHook(fn) {
|
|
86
|
+
this.onRequests.push(fn);
|
|
87
|
+
}
|
|
88
|
+
addBeforeMatchHook(fn) {
|
|
89
|
+
this.onBeforeMatch.push(fn);
|
|
90
|
+
}
|
|
91
|
+
addResponseHook(fn) {
|
|
92
|
+
this.onResponses.push(fn);
|
|
93
|
+
}
|
|
94
|
+
addBeforeHandleHook(fn) {
|
|
95
|
+
this.beforeHandles.push(fn);
|
|
96
|
+
}
|
|
97
|
+
addAfterHandleHook(fn) {
|
|
98
|
+
this.afterHandles.push(fn);
|
|
99
|
+
}
|
|
100
|
+
getState() {
|
|
101
|
+
return {
|
|
102
|
+
onRequests: this.onRequests,
|
|
103
|
+
onBeforeMatch: this.onBeforeMatch,
|
|
104
|
+
onResponses: this.onResponses,
|
|
105
|
+
beforeHandles: this.beforeHandles,
|
|
106
|
+
afterHandles: this.afterHandles
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
setState(state) {
|
|
110
|
+
this.onRequests = [...state.onRequests];
|
|
111
|
+
this.onBeforeMatch = [...state.onBeforeMatch || []];
|
|
112
|
+
this.onResponses = [...state.onResponses];
|
|
113
|
+
this.beforeHandles = [...state.beforeHandles];
|
|
114
|
+
this.afterHandles = [...state.afterHandles];
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// src/radix.ts
|
|
119
|
+
var RadixNode = class _RadixNode {
|
|
120
|
+
part;
|
|
121
|
+
children = /* @__PURE__ */ new Map();
|
|
122
|
+
wildcardChild = null;
|
|
123
|
+
catchAllChild = null;
|
|
124
|
+
paramName = null;
|
|
125
|
+
handlers = /* @__PURE__ */ new Map();
|
|
126
|
+
constructor(part = "", paramName = null) {
|
|
127
|
+
this.part = part;
|
|
128
|
+
this.paramName = paramName;
|
|
129
|
+
}
|
|
130
|
+
insert(segments, method, route, index = 0) {
|
|
131
|
+
if (index === segments.length) {
|
|
132
|
+
this.handlers.set(method, route);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const currentSegment = segments[index];
|
|
136
|
+
if (currentSegment === "*") {
|
|
137
|
+
if (!this.catchAllChild) {
|
|
138
|
+
this.catchAllChild = new _RadixNode("*");
|
|
139
|
+
}
|
|
140
|
+
this.catchAllChild.handlers.set(method, route);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const isWildcard = currentSegment.startsWith(":");
|
|
144
|
+
if (isWildcard) {
|
|
145
|
+
const paramName = currentSegment.slice(1);
|
|
146
|
+
if (!this.wildcardChild) {
|
|
147
|
+
this.wildcardChild = new _RadixNode(currentSegment, paramName);
|
|
148
|
+
}
|
|
149
|
+
this.wildcardChild.insert(segments, method, route, index + 1);
|
|
150
|
+
} else {
|
|
151
|
+
if (!this.children.has(currentSegment)) {
|
|
152
|
+
this.children.set(currentSegment, new _RadixNode(currentSegment));
|
|
153
|
+
}
|
|
154
|
+
this.children.get(currentSegment).insert(segments, method, route, index + 1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
search(segments, method, index = 0, params = {}) {
|
|
158
|
+
if (index === segments.length) {
|
|
159
|
+
const route = this.handlers.get(method);
|
|
160
|
+
if (route) return { route, params };
|
|
161
|
+
if (this.catchAllChild) {
|
|
162
|
+
const caRoute = this.catchAllChild.handlers.get(method);
|
|
163
|
+
if (caRoute) {
|
|
164
|
+
params["*"] = "";
|
|
165
|
+
return { route: caRoute, params };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const currentSegment = segments[index];
|
|
171
|
+
const child = this.children.get(currentSegment);
|
|
172
|
+
if (child) {
|
|
173
|
+
const result = child.search(segments, method, index + 1, params);
|
|
174
|
+
if (result) return result;
|
|
175
|
+
}
|
|
176
|
+
if (this.wildcardChild && this.wildcardChild.paramName) {
|
|
177
|
+
const newParams = {
|
|
178
|
+
...params,
|
|
179
|
+
[this.wildcardChild.paramName]: currentSegment
|
|
180
|
+
};
|
|
181
|
+
const result = this.wildcardChild.search(
|
|
182
|
+
segments,
|
|
183
|
+
method,
|
|
184
|
+
index + 1,
|
|
185
|
+
newParams
|
|
186
|
+
);
|
|
187
|
+
if (result) return result;
|
|
188
|
+
}
|
|
189
|
+
if (this.catchAllChild) {
|
|
190
|
+
const route = this.catchAllChild.handlers.get(method);
|
|
191
|
+
if (route) {
|
|
192
|
+
params["*"] = segments.slice(index).join("/");
|
|
193
|
+
return { route, params };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
var RadixTree = class {
|
|
200
|
+
root = new RadixNode("");
|
|
201
|
+
add(method, path, route) {
|
|
202
|
+
const segments = this.splitPath(path);
|
|
203
|
+
this.root.insert(segments, method.toUpperCase(), route);
|
|
204
|
+
}
|
|
205
|
+
match(method, path) {
|
|
206
|
+
const segments = this.splitPath(path);
|
|
207
|
+
return this.root.search(segments, method.toUpperCase());
|
|
208
|
+
}
|
|
209
|
+
splitPath(path) {
|
|
210
|
+
return path.split("/").filter(Boolean);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// src/utils.ts
|
|
215
|
+
function createRegex(path, paramNames = []) {
|
|
216
|
+
const regexSource = path.replace(/\//g, "\\/").replace(/:([^/]+)/g, (_, name) => {
|
|
217
|
+
if (!paramNames.includes(name)) paramNames.push(name);
|
|
218
|
+
return "([^/]+)";
|
|
219
|
+
}).replace(/\*/g, () => {
|
|
220
|
+
if (!paramNames.includes("*")) paramNames.push("*");
|
|
221
|
+
return "(.*)";
|
|
222
|
+
});
|
|
223
|
+
return new RegExp(`^${regexSource}$`);
|
|
224
|
+
}
|
|
225
|
+
function formatValidationError(error) {
|
|
226
|
+
const issues = error.issues || [];
|
|
227
|
+
if (Array.isArray(issues) && issues.length > 0) {
|
|
228
|
+
return issues.map((issue) => ({
|
|
229
|
+
path: issue.path,
|
|
230
|
+
message: issue.message,
|
|
231
|
+
code: issue.code,
|
|
232
|
+
metadata: issue.expected ? {
|
|
233
|
+
expected: issue.expected,
|
|
234
|
+
received: issue.received
|
|
235
|
+
} : void 0
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
return [
|
|
239
|
+
{
|
|
240
|
+
path: ["_form"],
|
|
241
|
+
message: error.message || "Unknown validation error",
|
|
242
|
+
code: "custom"
|
|
243
|
+
}
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/router.ts
|
|
248
|
+
var Router = class {
|
|
249
|
+
tree = new RadixTree();
|
|
250
|
+
routes = [];
|
|
251
|
+
/**
|
|
252
|
+
* Registers a new route with its associated state (hooks, derivations, and decorators).
|
|
253
|
+
*/
|
|
254
|
+
add(method, path, handler, state, schema, metadata) {
|
|
255
|
+
const paramNames = [];
|
|
256
|
+
const route = {
|
|
257
|
+
method,
|
|
258
|
+
path,
|
|
259
|
+
regex: createRegex(path, paramNames),
|
|
260
|
+
handler,
|
|
261
|
+
paramNames,
|
|
262
|
+
schema,
|
|
263
|
+
metadata,
|
|
264
|
+
derives: [...state.derives],
|
|
265
|
+
decorators: { ...state.decorators },
|
|
266
|
+
onRequests: [...state.onRequests],
|
|
267
|
+
onResponses: [...state.onResponses],
|
|
268
|
+
beforeHandles: [...state.beforeHandles],
|
|
269
|
+
afterHandles: [...state.afterHandles]
|
|
270
|
+
};
|
|
271
|
+
this.routes.push(route);
|
|
272
|
+
this.tree.add(method, path, route);
|
|
273
|
+
return route;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Finds a matching route for the given method and pathname.
|
|
277
|
+
*/
|
|
278
|
+
match(method, pathname) {
|
|
279
|
+
const result = this.tree.match(method, pathname);
|
|
280
|
+
if (!result) return null;
|
|
281
|
+
const { route, params } = result;
|
|
282
|
+
const matchArray = [pathname];
|
|
283
|
+
route.paramNames.forEach((name) => {
|
|
284
|
+
matchArray.push(params[name]);
|
|
285
|
+
});
|
|
286
|
+
return { route, match: matchArray };
|
|
287
|
+
}
|
|
288
|
+
addRoute(route) {
|
|
289
|
+
this.routes.push(route);
|
|
290
|
+
this.tree.add(route.method, route.path, route);
|
|
291
|
+
}
|
|
292
|
+
getRoutes() {
|
|
293
|
+
return this.routes;
|
|
294
|
+
}
|
|
295
|
+
setRoutes(routes) {
|
|
296
|
+
this.routes = routes;
|
|
297
|
+
this.tree = new RadixTree();
|
|
298
|
+
routes.forEach((r) => this.tree.add(r.method, r.path, r));
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// src/index.ts
|
|
303
|
+
var $SERVER = /* @__PURE__ */ Symbol("Axeom:server");
|
|
304
|
+
var InternalContext = class {
|
|
305
|
+
params = {};
|
|
306
|
+
query = {};
|
|
307
|
+
request;
|
|
308
|
+
time;
|
|
309
|
+
body = void 0;
|
|
310
|
+
_responseHeaders;
|
|
311
|
+
_server;
|
|
312
|
+
constructor(request, server, startTime, decorators) {
|
|
313
|
+
this.request = request;
|
|
314
|
+
this._server = server;
|
|
315
|
+
this.time = startTime;
|
|
316
|
+
this._responseHeaders = {};
|
|
317
|
+
this.setResponseHeader = this.setResponseHeader.bind(this);
|
|
318
|
+
this.getResponseHeaders = this.getResponseHeaders.bind(this);
|
|
319
|
+
this.setDuration = this.setDuration.bind(this);
|
|
320
|
+
Object.assign(this, decorators);
|
|
321
|
+
}
|
|
322
|
+
get headers() {
|
|
323
|
+
return this.request.headers;
|
|
324
|
+
}
|
|
325
|
+
setDuration(label) {
|
|
326
|
+
return performance.now() - this.time;
|
|
327
|
+
}
|
|
328
|
+
setResponseHeader(name, value) {
|
|
329
|
+
this._responseHeaders[name] = value;
|
|
330
|
+
}
|
|
331
|
+
getResponseHeaders() {
|
|
332
|
+
return this._responseHeaders;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
var Axeom = class _Axeom {
|
|
336
|
+
router = new Router();
|
|
337
|
+
hooks = new Hooks();
|
|
338
|
+
server;
|
|
339
|
+
derives = [];
|
|
340
|
+
decorators = {
|
|
341
|
+
logger: {
|
|
342
|
+
info: () => {
|
|
343
|
+
},
|
|
344
|
+
error: () => {
|
|
345
|
+
},
|
|
346
|
+
warn: () => {
|
|
347
|
+
},
|
|
348
|
+
debug: () => {
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
error: {
|
|
352
|
+
NotFound: (msg) => {
|
|
353
|
+
throw new NotFoundError(msg);
|
|
354
|
+
},
|
|
355
|
+
Unauthorized: (msg) => {
|
|
356
|
+
throw new UnauthorizedError(msg);
|
|
357
|
+
},
|
|
358
|
+
BadRequest: (msg) => {
|
|
359
|
+
throw new BadRequestError(msg);
|
|
360
|
+
},
|
|
361
|
+
Forbidden: (msg) => {
|
|
362
|
+
throw new ForbiddenError(msg);
|
|
363
|
+
},
|
|
364
|
+
Conflict: (msg) => {
|
|
365
|
+
throw new ConflictError(msg);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
errorHandler = (error) => {
|
|
370
|
+
if (error instanceof AxeomError) {
|
|
371
|
+
return {
|
|
372
|
+
status: "error",
|
|
373
|
+
code: error.code,
|
|
374
|
+
message: error.message,
|
|
375
|
+
details: error.details
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
console.error(error);
|
|
379
|
+
return { status: "error", message: "Internal Server Error" };
|
|
380
|
+
};
|
|
381
|
+
/**
|
|
382
|
+
* Adds dynamic properties to the context by running a function before the route handler.
|
|
383
|
+
* Derivations can be asynchronous and can return a Response to short-circuit the request.
|
|
384
|
+
*
|
|
385
|
+
* @param fn - A function that receives the current context and returns new properties to merge.
|
|
386
|
+
*/
|
|
387
|
+
derive(fn) {
|
|
388
|
+
this.derives.push(fn);
|
|
389
|
+
return this;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Registers a plugin.
|
|
393
|
+
* Plugins are functions that receive the Axeom instance and can register routes,
|
|
394
|
+
* hooks, or decorators.
|
|
395
|
+
*
|
|
396
|
+
* @param plugin - The plugin function to execute.
|
|
397
|
+
*/
|
|
398
|
+
use(plugin) {
|
|
399
|
+
return plugin(this);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Groups routes under a common path prefix.
|
|
403
|
+
* This provides isolation for derivations and hooks within the group.
|
|
404
|
+
*
|
|
405
|
+
* @param prefix - The path prefix (e.g., "/api/v1").
|
|
406
|
+
* @param run - A function where you define the routes for this group.
|
|
407
|
+
*/
|
|
408
|
+
group(prefix, run) {
|
|
409
|
+
const branch = new _Axeom();
|
|
410
|
+
branch.derives = [...this.derives];
|
|
411
|
+
branch.decorators = { ...this.decorators };
|
|
412
|
+
branch.hooks.setState(this.hooks.getState());
|
|
413
|
+
const result = run(branch);
|
|
414
|
+
result.router.getRoutes().forEach((route) => {
|
|
415
|
+
const fullPath = `${prefix}${route.path}`.replace(/\/+/g, "/");
|
|
416
|
+
const paramNames = [];
|
|
417
|
+
this.router.addRoute({
|
|
418
|
+
...route,
|
|
419
|
+
path: fullPath,
|
|
420
|
+
regex: createRegex(fullPath, paramNames),
|
|
421
|
+
paramNames
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
return this;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Adds static properties to the context.
|
|
428
|
+
* Decorators are available on every request context immediately.
|
|
429
|
+
*
|
|
430
|
+
* @param obj - An object containing properties to add to the context.
|
|
431
|
+
*/
|
|
432
|
+
decorate(obj) {
|
|
433
|
+
this.decorators = { ...this.decorators, ...obj };
|
|
434
|
+
return this;
|
|
435
|
+
}
|
|
436
|
+
addRoute(method, path, handler, schema, metadata) {
|
|
437
|
+
this.router.add(
|
|
438
|
+
method,
|
|
439
|
+
path,
|
|
440
|
+
handler,
|
|
441
|
+
{
|
|
442
|
+
derives: this.derives,
|
|
443
|
+
decorators: this.decorators,
|
|
444
|
+
...this.hooks.getState()
|
|
445
|
+
},
|
|
446
|
+
schema,
|
|
447
|
+
metadata
|
|
448
|
+
);
|
|
449
|
+
return this;
|
|
450
|
+
}
|
|
451
|
+
get(path, handler, schema) {
|
|
452
|
+
return this.addRoute("GET", path, handler, schema);
|
|
453
|
+
}
|
|
454
|
+
post(path, handler, schema) {
|
|
455
|
+
return this.addRoute("POST", path, handler, schema);
|
|
456
|
+
}
|
|
457
|
+
put(path, handler, schema) {
|
|
458
|
+
return this.addRoute("PUT", path, handler, schema);
|
|
459
|
+
}
|
|
460
|
+
patch(path, handler, schema) {
|
|
461
|
+
return this.addRoute("PATCH", path, handler, schema);
|
|
462
|
+
}
|
|
463
|
+
delete(path, handler, schema) {
|
|
464
|
+
return this.addRoute("DELETE", path, handler, schema);
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Main entry point for handling requests.
|
|
468
|
+
*
|
|
469
|
+
* This is the standard WinterTC interface that converts a Request into a Response.
|
|
470
|
+
* It executes the entire request lifecycle including hooks, derivations, and validation.
|
|
471
|
+
*
|
|
472
|
+
* @param incomingRequest - The standard Web Request object.
|
|
473
|
+
* @param options - Optional runtime configuration (e.g. server instance).
|
|
474
|
+
* @returns A promise resolving to a standard Web Response object.
|
|
475
|
+
*/
|
|
476
|
+
async handle(incomingRequest, options) {
|
|
477
|
+
const handshake = await this._handleHandshake(incomingRequest, options);
|
|
478
|
+
const { response } = handshake;
|
|
479
|
+
response._axeom_meta = handshake;
|
|
480
|
+
return response;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Internal method used by adapters to handle both HTTP and WebSocket handshakes.
|
|
484
|
+
* Returns the final response and the context used for the request.
|
|
485
|
+
*/
|
|
486
|
+
async _handleHandshake(incomingRequest, options) {
|
|
487
|
+
const url = new URL(incomingRequest.url);
|
|
488
|
+
const { pathname } = url;
|
|
489
|
+
const method = incomingRequest.method;
|
|
490
|
+
const startTime = performance.now();
|
|
491
|
+
const server = options?.server || this.server;
|
|
492
|
+
const ctx = new InternalContext(
|
|
493
|
+
incomingRequest,
|
|
494
|
+
server,
|
|
495
|
+
startTime,
|
|
496
|
+
this.decorators
|
|
497
|
+
);
|
|
498
|
+
const finalizeResponse = async (res, currentCtx, routeHooks) => {
|
|
499
|
+
let finalRes = res;
|
|
500
|
+
const safeCtx = currentCtx || ctx;
|
|
501
|
+
const hooksToRun = routeHooks?.onResponses || this.hooks.onResponses;
|
|
502
|
+
if (hooksToRun && hooksToRun.length > 0) {
|
|
503
|
+
for (const onResponseFn of hooksToRun) {
|
|
504
|
+
const result = await onResponseFn(finalRes, safeCtx);
|
|
505
|
+
if (result instanceof Response) finalRes = result;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (safeCtx && typeof safeCtx.getResponseHeaders === "function") {
|
|
509
|
+
const ctxHeaders = safeCtx.getResponseHeaders();
|
|
510
|
+
Object.entries(ctxHeaders).forEach(([name, value]) => {
|
|
511
|
+
finalRes.headers.set(name, value);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
return finalRes;
|
|
515
|
+
};
|
|
516
|
+
if (this.hooks.onBeforeMatch.length > 0) {
|
|
517
|
+
for (const hook of this.hooks.onBeforeMatch) {
|
|
518
|
+
const result = await hook(incomingRequest);
|
|
519
|
+
if (result instanceof Response)
|
|
520
|
+
return { response: await finalizeResponse(result, ctx) };
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const matched = this.router.match(method, pathname);
|
|
524
|
+
if (!matched) {
|
|
525
|
+
return {
|
|
526
|
+
response: await finalizeResponse(
|
|
527
|
+
new Response("Route not found", { status: 404 }),
|
|
528
|
+
ctx
|
|
529
|
+
)
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
const { route, match } = matched;
|
|
533
|
+
try {
|
|
534
|
+
const rawParams = {};
|
|
535
|
+
route.paramNames.forEach((name, index) => {
|
|
536
|
+
rawParams[name] = match[index + 1];
|
|
537
|
+
});
|
|
538
|
+
ctx.params = rawParams;
|
|
539
|
+
const queryStart = incomingRequest.url.indexOf("?");
|
|
540
|
+
ctx.query = queryStart === -1 ? {} : Object.fromEntries(
|
|
541
|
+
new URLSearchParams(
|
|
542
|
+
incomingRequest.url.slice(queryStart + 1)
|
|
543
|
+
).entries()
|
|
544
|
+
);
|
|
545
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
546
|
+
const contentType = incomingRequest.headers.get("content-type");
|
|
547
|
+
try {
|
|
548
|
+
if (contentType?.includes("application/json")) {
|
|
549
|
+
ctx.body = await incomingRequest.json();
|
|
550
|
+
} else if (contentType?.includes("multipart/form-data") || contentType?.includes("application/x-www-form-urlencoded")) {
|
|
551
|
+
ctx.body = await incomingRequest.formData();
|
|
552
|
+
}
|
|
553
|
+
} catch {
|
|
554
|
+
ctx.body = null;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (route.derives && route.derives.length > 0) {
|
|
558
|
+
for (const deriveFn of route.derives) {
|
|
559
|
+
const result = await deriveFn(ctx);
|
|
560
|
+
if (result instanceof Response)
|
|
561
|
+
return {
|
|
562
|
+
response: await finalizeResponse(result, ctx, route),
|
|
563
|
+
context: ctx
|
|
564
|
+
};
|
|
565
|
+
if (result) Object.assign(ctx, result);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (route.onRequests && route.onRequests.length > 0) {
|
|
569
|
+
for (const onRequestFn of route.onRequests) {
|
|
570
|
+
await onRequestFn(ctx);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (route.beforeHandles && route.beforeHandles.length > 0) {
|
|
574
|
+
for (const hook of route.beforeHandles) {
|
|
575
|
+
const response2 = await hook(ctx);
|
|
576
|
+
if (response2 instanceof Response)
|
|
577
|
+
return {
|
|
578
|
+
response: await finalizeResponse(response2, ctx, route),
|
|
579
|
+
context: ctx
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (route.schema) {
|
|
584
|
+
try {
|
|
585
|
+
if (route.schema.params)
|
|
586
|
+
ctx.params = await route.schema.params.parse(ctx.params);
|
|
587
|
+
if (route.schema.query)
|
|
588
|
+
ctx.query = await route.schema.query.parse(ctx.query);
|
|
589
|
+
if (route.schema.body) {
|
|
590
|
+
if (ctx.body instanceof FormData) {
|
|
591
|
+
ctx.body = Object.fromEntries(ctx.body.entries());
|
|
592
|
+
}
|
|
593
|
+
ctx.body = await route.schema.body.parse(ctx.body);
|
|
594
|
+
}
|
|
595
|
+
} catch (e) {
|
|
596
|
+
const formattedErrors = formatValidationError(e);
|
|
597
|
+
return {
|
|
598
|
+
response: await finalizeResponse(
|
|
599
|
+
Response.json(
|
|
600
|
+
{
|
|
601
|
+
code: "VALIDATION_FAILED",
|
|
602
|
+
message: "Request validation failed",
|
|
603
|
+
errors: formattedErrors
|
|
604
|
+
},
|
|
605
|
+
{ status: 400 }
|
|
606
|
+
),
|
|
607
|
+
ctx,
|
|
608
|
+
route
|
|
609
|
+
)
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
let response;
|
|
614
|
+
if (route.handler) {
|
|
615
|
+
response = await route.handler(ctx);
|
|
616
|
+
}
|
|
617
|
+
if (!(response instanceof Response)) {
|
|
618
|
+
response = Response.json(response);
|
|
619
|
+
}
|
|
620
|
+
if (route.afterHandles && route.afterHandles.length > 0) {
|
|
621
|
+
for (const hook of route.afterHandles) {
|
|
622
|
+
const result = await hook(ctx);
|
|
623
|
+
if (result instanceof Response)
|
|
624
|
+
return {
|
|
625
|
+
response: await finalizeResponse(result, ctx, route),
|
|
626
|
+
context: ctx
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
response: await finalizeResponse(response, ctx, route),
|
|
632
|
+
context: ctx
|
|
633
|
+
};
|
|
634
|
+
} catch (error) {
|
|
635
|
+
const errorResponse = await this.errorHandler(error, ctx);
|
|
636
|
+
return {
|
|
637
|
+
response: await finalizeResponse(
|
|
638
|
+
Response.json(errorResponse, {
|
|
639
|
+
status: error?.status || 500
|
|
640
|
+
}),
|
|
641
|
+
ctx,
|
|
642
|
+
route
|
|
643
|
+
),
|
|
644
|
+
context: ctx
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Registers a global error handler.
|
|
650
|
+
*/
|
|
651
|
+
onError(fn) {
|
|
652
|
+
this.errorHandler = fn;
|
|
653
|
+
return this;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Registers a global hook that runs as soon as a request is received.
|
|
657
|
+
*/
|
|
658
|
+
onRequest(fn) {
|
|
659
|
+
this.hooks.addRequestHook(fn);
|
|
660
|
+
return this;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Registers a hook that runs before the router attempts to match a path.
|
|
664
|
+
* Useful for global redirects or early security checks.
|
|
665
|
+
*/
|
|
666
|
+
onBeforeMatch(fn) {
|
|
667
|
+
this.hooks.addBeforeMatchHook(fn);
|
|
668
|
+
return this;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Registers a global hook that runs after a Response has been created.
|
|
672
|
+
* Allows modifying headers or the body before it's sent.
|
|
673
|
+
*/
|
|
674
|
+
onResponse(fn) {
|
|
675
|
+
this.hooks.addResponseHook(fn);
|
|
676
|
+
return this;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Registers a hook that runs before the route handler is executed.
|
|
680
|
+
*/
|
|
681
|
+
onBeforeHandle(fn) {
|
|
682
|
+
this.hooks.addBeforeHandleHook(fn);
|
|
683
|
+
return this;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Registers a hook that runs after the route handler is executed.
|
|
687
|
+
*/
|
|
688
|
+
onAfterHandle(fn) {
|
|
689
|
+
this.hooks.addAfterHandleHook(fn);
|
|
690
|
+
return this;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Registers a Server-Sent Events (SSE) route.
|
|
694
|
+
*
|
|
695
|
+
* @param path - The path for the SSE stream.
|
|
696
|
+
* @param handler - A function returning an async iterable that yields data chunks.
|
|
697
|
+
*/
|
|
698
|
+
sse(path, handler) {
|
|
699
|
+
return this.get(path, async (ctx) => {
|
|
700
|
+
if (ctx[$SERVER]?.timeout) {
|
|
701
|
+
ctx[$SERVER].timeout(ctx.request, 0);
|
|
702
|
+
}
|
|
703
|
+
const stream = await handler(ctx);
|
|
704
|
+
const readable = new ReadableStream({
|
|
705
|
+
async start(controller) {
|
|
706
|
+
for await (const chunk of stream) {
|
|
707
|
+
const data = typeof chunk === "string" ? chunk : JSON.stringify(chunk);
|
|
708
|
+
controller.enqueue(`data: ${data}
|
|
709
|
+
|
|
710
|
+
`);
|
|
711
|
+
}
|
|
712
|
+
controller.close();
|
|
713
|
+
}
|
|
714
|
+
}).pipeThrough(new TextEncoderStream());
|
|
715
|
+
return new Response(readable, {
|
|
716
|
+
headers: {
|
|
717
|
+
"Content-Type": "text/event-stream",
|
|
718
|
+
"Cache-Control": "no-cache",
|
|
719
|
+
Connection: "keep-alive"
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Automatically detect the runtime and start a server.
|
|
726
|
+
* Currently supports: Bun, Deno.
|
|
727
|
+
* For Node, use a specific adapter like @axeom/express.
|
|
728
|
+
*
|
|
729
|
+
* @param portOrOptions - The port number or a server options object.
|
|
730
|
+
*/
|
|
731
|
+
listen(portOrOptions = 3e3) {
|
|
732
|
+
const options = typeof portOrOptions === "number" ? { port: portOrOptions } : portOrOptions || {};
|
|
733
|
+
const port = options.port || 3e3;
|
|
734
|
+
if (typeof Bun !== "undefined") {
|
|
735
|
+
this.server = Bun.serve({
|
|
736
|
+
...options,
|
|
737
|
+
port,
|
|
738
|
+
fetch: async (req, server) => {
|
|
739
|
+
const res = await this.handle(req);
|
|
740
|
+
if (res.status === 101) {
|
|
741
|
+
const url = new URL(req.url);
|
|
742
|
+
const matched = this.router.match(req.method, url.pathname);
|
|
743
|
+
if (matched && matched.route.metadata?.ws) {
|
|
744
|
+
const success = server.upgrade(req, {
|
|
745
|
+
data: matched.route.metadata.ws
|
|
746
|
+
});
|
|
747
|
+
if (success) return void 0;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return res;
|
|
751
|
+
},
|
|
752
|
+
websocket: {
|
|
753
|
+
open: (ws) => ws.data.open?.(ws),
|
|
754
|
+
message: (ws, message) => ws.data.message?.(ws, message),
|
|
755
|
+
close: (ws, code, reason) => ws.data.close?.(ws, code, reason),
|
|
756
|
+
error: (ws, error) => ws.data.error?.(ws, error)
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
console.log(`\x1B[32m Axeom listening on ${this.server.url}\x1B[0m`);
|
|
760
|
+
return this.server;
|
|
761
|
+
}
|
|
762
|
+
if (typeof Deno !== "undefined" && typeof Deno.serve === "function") {
|
|
763
|
+
this.server = Deno.serve({ ...options, port }, async (req) => {
|
|
764
|
+
const res = await this.handle(req);
|
|
765
|
+
if (res.status === 101) {
|
|
766
|
+
const url = new URL(req.url);
|
|
767
|
+
const matched = this.router.match(req.method, url.pathname);
|
|
768
|
+
if (matched && matched.route.metadata?.ws) {
|
|
769
|
+
const { socket, response } = Deno.upgradeWebSocket(req);
|
|
770
|
+
const handlers = matched.route.metadata.ws;
|
|
771
|
+
socket.onopen = () => handlers.open?.(socket);
|
|
772
|
+
socket.onmessage = (e) => handlers.message?.(socket, e.data);
|
|
773
|
+
socket.onclose = () => handlers.close?.(socket);
|
|
774
|
+
socket.onerror = (e) => handlers.error?.(socket, e);
|
|
775
|
+
return response;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return res;
|
|
779
|
+
});
|
|
780
|
+
return this.server;
|
|
781
|
+
}
|
|
782
|
+
if (typeof process !== "undefined" && process.release?.name === "node") {
|
|
783
|
+
throw new Error(
|
|
784
|
+
"[Axeom] Automatic runtime detection found Node.js. Axeom requires a Fetch API compatible server. Please use @axeom/express for Node.js environments."
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
throw new Error(
|
|
788
|
+
"[Axeom] Automatic runtime detection failed. Ensure you are running in a supported environment (Bun, Deno) or use an adapter."
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
var index_default = Axeom;
|
|
793
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
794
|
+
0 && (module.exports = {
|
|
795
|
+
$SERVER,
|
|
796
|
+
Axeom,
|
|
797
|
+
AxeomError,
|
|
798
|
+
BadRequestError,
|
|
799
|
+
ConflictError,
|
|
800
|
+
ForbiddenError,
|
|
801
|
+
InternalServerError,
|
|
802
|
+
NotFoundError,
|
|
803
|
+
UnauthorizedError
|
|
804
|
+
});
|