@raven.js/cli 1.1.2 → 1.2.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/README.md +10 -7
- package/dist/raven +33 -109
- package/dist/raven.map +3 -3
- package/dist/registry.json +1 -9
- package/dist/source/core/GUIDE.md +14 -0
- package/dist/source/core/PLUGIN.md +225 -0
- package/dist/source/core/README.md +427 -0
- package/dist/source/core/index.ts +624 -0
- package/dist/source/core/router.ts +128 -0
- package/dist/source/schema-validator/GUIDE.md +12 -0
- package/dist/source/schema-validator/README.md +229 -0
- package/dist/source/schema-validator/index.ts +139 -0
- package/dist/source/schema-validator/standard-schema.ts +76 -0
- package/dist/source/sql/GUIDE.md +12 -0
- package/dist/source/sql/README.md +271 -0
- package/dist/source/sql/index.ts +14 -0
- package/package.json +2 -2
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// SECTION: Imports
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
+
import { RadixRouter } from "./router.ts";
|
|
7
|
+
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// SECTION: Types & Interfaces
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Additional context information for errors.
|
|
14
|
+
*/
|
|
15
|
+
export interface ErrorContext {
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Core interface representing a Raven application instance.
|
|
21
|
+
*/
|
|
22
|
+
export interface RavenInstance {
|
|
23
|
+
/** Internal storage for application-scoped state. */
|
|
24
|
+
internalStateMap: Map<symbol, any>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Options for creating a new state.
|
|
29
|
+
*/
|
|
30
|
+
export interface StateOptions {
|
|
31
|
+
/** Optional name for the state, useful for debugging. */
|
|
32
|
+
name?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Standard FetchHandler type - a function that takes a Request and returns a Response.
|
|
37
|
+
* Raven.handle implements this interface.
|
|
38
|
+
*/
|
|
39
|
+
export type FetchHandler = (request: Request) => Response | Promise<Response>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Request handler function type.
|
|
43
|
+
*/
|
|
44
|
+
export type Handler = () => Response | Promise<Response>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Hook executed when a request is first received.
|
|
48
|
+
* Returning a Response will short-circuit the request lifecycle.
|
|
49
|
+
*/
|
|
50
|
+
export type OnRequestHook = (
|
|
51
|
+
request: Request,
|
|
52
|
+
) => void | Response | Promise<void | Response>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Hook executed before the main route handler.
|
|
56
|
+
* Returning a Response will short-circuit the request lifecycle.
|
|
57
|
+
*/
|
|
58
|
+
export type BeforeHandleHook = () => void | Response | Promise<void | Response>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Hook executed before the response is sent to the client.
|
|
62
|
+
* Can return a new Response to override the outgoing response.
|
|
63
|
+
*/
|
|
64
|
+
export type BeforeResponseHook = (
|
|
65
|
+
response: Response,
|
|
66
|
+
) => void | Response | Promise<void | Response>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Hook executed when an error occurs during the request lifecycle.
|
|
70
|
+
* Should return a Response to send to the client.
|
|
71
|
+
*/
|
|
72
|
+
export type OnErrorHook = (error: Error) => Response | Promise<Response> | void | Promise<void>;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Hook executed after application plugins are fully loaded.
|
|
76
|
+
* Runs once before the app starts serving requests.
|
|
77
|
+
*/
|
|
78
|
+
export type OnLoadedHook = (app: Raven) => void | Promise<void>;
|
|
79
|
+
|
|
80
|
+
/** Internal route data structure. */
|
|
81
|
+
interface RouteData {
|
|
82
|
+
handler: Handler;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Plugin object type for extending the Raven instance.
|
|
87
|
+
* @template S Tuple of ScopedState instances declared by this plugin.
|
|
88
|
+
*/
|
|
89
|
+
export interface Plugin<S extends readonly ScopedState<any>[] = readonly []> {
|
|
90
|
+
/** Plugin name, shown in error messages for attribution. */
|
|
91
|
+
name: string;
|
|
92
|
+
/** States created inside this plugin factory (returned by register()). */
|
|
93
|
+
states: S;
|
|
94
|
+
/** Called during registration to set up routes, hooks, etc. */
|
|
95
|
+
load(app: Raven): void | Promise<void>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// =============================================================================
|
|
99
|
+
// SECTION: Error Handling
|
|
100
|
+
// =============================================================================
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Custom error class for Raven framework errors.
|
|
104
|
+
*/
|
|
105
|
+
export class RavenError extends Error {
|
|
106
|
+
public readonly code: string;
|
|
107
|
+
public readonly context: ErrorContext;
|
|
108
|
+
public readonly statusCode?: number;
|
|
109
|
+
public override readonly cause?: unknown;
|
|
110
|
+
|
|
111
|
+
private constructor(
|
|
112
|
+
code: string,
|
|
113
|
+
message: string,
|
|
114
|
+
context: ErrorContext,
|
|
115
|
+
statusCode?: number,
|
|
116
|
+
) {
|
|
117
|
+
super(message);
|
|
118
|
+
this.code = code;
|
|
119
|
+
this.context = context;
|
|
120
|
+
this.statusCode = statusCode;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Appends additional context to the error.
|
|
125
|
+
* @param context Additional properties to include.
|
|
126
|
+
* @returns The current RavenError instance.
|
|
127
|
+
*/
|
|
128
|
+
public setContext(context: ErrorContext): this {
|
|
129
|
+
Object.assign(this.context, context);
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public static ERR_STATE_NOT_INITIALIZED(name: string): RavenError {
|
|
134
|
+
return new RavenError(
|
|
135
|
+
"ERR_STATE_NOT_INITIALIZED",
|
|
136
|
+
`State is not initialized. Cannot access state: ${name}`,
|
|
137
|
+
{},
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public static ERR_STATE_CANNOT_SET(name: string): RavenError {
|
|
142
|
+
return new RavenError(
|
|
143
|
+
"ERR_STATE_CANNOT_SET",
|
|
144
|
+
`Cannot set value for state "${name}": Scope is not initialized.`,
|
|
145
|
+
{},
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public static ERR_BAD_REQUEST(message: string): RavenError {
|
|
150
|
+
return new RavenError("ERR_BAD_REQUEST", message, {}, 400);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public static ERR_UNKNOWN_ERROR(message: string): RavenError {
|
|
154
|
+
return new RavenError("ERR_UNKNOWN_ERROR", message, {}, 500);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Serializes the error to a JSON-friendly object.
|
|
159
|
+
*/
|
|
160
|
+
public toJSON(): Record<string, unknown> {
|
|
161
|
+
return {
|
|
162
|
+
name: this.name,
|
|
163
|
+
code: this.code,
|
|
164
|
+
message: this.message,
|
|
165
|
+
context: this.context,
|
|
166
|
+
statusCode: this.statusCode,
|
|
167
|
+
cause: this.cause,
|
|
168
|
+
stack: this.stack,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Converts the error to a standard HTTP Response.
|
|
174
|
+
*/
|
|
175
|
+
public toResponse(): Response {
|
|
176
|
+
const status = this.statusCode ?? 500;
|
|
177
|
+
return new Response(JSON.stringify({ message: this.message }), {
|
|
178
|
+
status,
|
|
179
|
+
headers: { "Content-Type": "application/json" },
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
Object.defineProperty(RavenError.prototype, "name", {
|
|
185
|
+
value: "RavenError",
|
|
186
|
+
writable: true,
|
|
187
|
+
configurable: true,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Type guard to check if a value is a RavenError.
|
|
192
|
+
*/
|
|
193
|
+
export function isRavenError(value: unknown): value is RavenError {
|
|
194
|
+
return value instanceof RavenError;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// =============================================================================
|
|
198
|
+
// SECTION: Context & State Management
|
|
199
|
+
// =============================================================================
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Represents the current request context.
|
|
203
|
+
*/
|
|
204
|
+
export class Context {
|
|
205
|
+
public readonly url: URL;
|
|
206
|
+
|
|
207
|
+
constructor(
|
|
208
|
+
public readonly request: Request,
|
|
209
|
+
public params: Record<string, string> = {},
|
|
210
|
+
public query: Record<string, string> = {},
|
|
211
|
+
) {
|
|
212
|
+
this.url = new URL(request.url);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** HTTP method of the request. */
|
|
216
|
+
get method(): string {
|
|
217
|
+
return this.request.method;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** HTTP headers of the request. */
|
|
221
|
+
get headers(): Headers {
|
|
222
|
+
return this.request.headers;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Body stream of the request. */
|
|
226
|
+
get body(): ReadableStream<Uint8Array> | null {
|
|
227
|
+
return this.request.body;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Storage for the current application instance. */
|
|
232
|
+
export const currentAppStorage = new AsyncLocalStorage<RavenInstance>();
|
|
233
|
+
/** Storage for the current request's state map. */
|
|
234
|
+
export const requestStorage = new AsyncLocalStorage<Map<symbol, any>>();
|
|
235
|
+
|
|
236
|
+
let stateCounter = 0;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Base class for a scoped state container.
|
|
240
|
+
* @template T The type of the state value.
|
|
241
|
+
*/
|
|
242
|
+
export abstract class ScopedState<T> {
|
|
243
|
+
public readonly symbol: symbol;
|
|
244
|
+
public readonly name: string;
|
|
245
|
+
|
|
246
|
+
constructor(options?: StateOptions) {
|
|
247
|
+
this.name = options?.name ?? `state:${++stateCounter}`;
|
|
248
|
+
this.symbol = Symbol(this.name);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Retrieves the state value, returning undefined if not set. */
|
|
252
|
+
public abstract get(): T | undefined;
|
|
253
|
+
|
|
254
|
+
/** Sets the state value in the current scope. */
|
|
255
|
+
public abstract set(value: T): void;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Retrieves the state value, throwing an error if not initialized.
|
|
259
|
+
* @throws {RavenError} If the state is not initialized.
|
|
260
|
+
*/
|
|
261
|
+
public getOrFailed(): T {
|
|
262
|
+
const value = this.get();
|
|
263
|
+
if (value === undefined) {
|
|
264
|
+
throw RavenError.ERR_STATE_NOT_INITIALIZED(this.name);
|
|
265
|
+
}
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Application-scoped state, shared across the entire application instance.
|
|
272
|
+
*/
|
|
273
|
+
export class AppState<T> extends ScopedState<T> {
|
|
274
|
+
constructor(options?: StateOptions) {
|
|
275
|
+
super(options);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
public get(): T | undefined {
|
|
279
|
+
const app = currentAppStorage.getStore();
|
|
280
|
+
return app?.internalStateMap.get(this.symbol);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
public set(value: T): void {
|
|
284
|
+
const app = currentAppStorage.getStore();
|
|
285
|
+
if (!app) {
|
|
286
|
+
throw RavenError.ERR_STATE_CANNOT_SET(this.name);
|
|
287
|
+
}
|
|
288
|
+
app.internalStateMap.set(this.symbol, value);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Request-scoped state, isolated to a single HTTP request lifecycle.
|
|
294
|
+
*/
|
|
295
|
+
export class RequestState<T> extends ScopedState<T> {
|
|
296
|
+
constructor(options?: StateOptions) {
|
|
297
|
+
super(options);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
public get(): T | undefined {
|
|
301
|
+
const store = requestStorage.getStore();
|
|
302
|
+
return store?.get(this.symbol);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
public set(value: T): void {
|
|
306
|
+
const store = requestStorage.getStore();
|
|
307
|
+
if (!store) {
|
|
308
|
+
throw RavenError.ERR_STATE_CANNOT_SET(this.name);
|
|
309
|
+
}
|
|
310
|
+
store.set(this.symbol, value);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Creates a new application-scoped state. */
|
|
315
|
+
export function createAppState<T>(options?: StateOptions): AppState<T> {
|
|
316
|
+
return new AppState<T>(options);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Creates a new request-scoped state. */
|
|
320
|
+
export function createRequestState<T>(options?: StateOptions): RequestState<T> {
|
|
321
|
+
return new RequestState<T>(options);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Predefined Request States
|
|
325
|
+
export const RavenContext = createRequestState<Context>({ name: "raven:context" });
|
|
326
|
+
export const BodyState = createRequestState<unknown>({ name: "raven:body" });
|
|
327
|
+
export const QueryState = createRequestState<Record<string, string>>({ name: "raven:query" });
|
|
328
|
+
export const ParamsState = createRequestState<Record<string, string>>({ name: "raven:params" });
|
|
329
|
+
export const HeadersState = createRequestState<Record<string, string>>({ name: "raven:headers" });
|
|
330
|
+
|
|
331
|
+
// Re-export router types for consumers
|
|
332
|
+
export { RadixRouter, type RouteMatch } from "./router.ts";
|
|
333
|
+
|
|
334
|
+
// =============================================================================
|
|
335
|
+
// SECTION: Server Adapters
|
|
336
|
+
// =============================================================================
|
|
337
|
+
|
|
338
|
+
// =============================================================================
|
|
339
|
+
// SECTION: Core Framework
|
|
340
|
+
// =============================================================================
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Utility function to define a plugin with correct tuple type inference for states.
|
|
344
|
+
* Without this helper, TypeScript infers `states` as `ScopedState<any>[]` (array).
|
|
345
|
+
* With this helper, it infers the precise tuple type, enabling typed register() returns.
|
|
346
|
+
*/
|
|
347
|
+
export function definePlugin<S extends readonly ScopedState<any>[]>(
|
|
348
|
+
plugin: Plugin<S>,
|
|
349
|
+
): Plugin<S> {
|
|
350
|
+
return plugin;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Main application class for the Raven framework.
|
|
355
|
+
*/
|
|
356
|
+
export class Raven implements RavenInstance {
|
|
357
|
+
private router: RadixRouter<RouteData>;
|
|
358
|
+
public readonly internalStateMap = new Map<symbol, any>();
|
|
359
|
+
|
|
360
|
+
private hooks = {
|
|
361
|
+
onRequest: [] as OnRequestHook[],
|
|
362
|
+
beforeHandle: [] as BeforeHandleHook[],
|
|
363
|
+
beforeResponse: [] as BeforeResponseHook[],
|
|
364
|
+
onError: [] as OnErrorHook[],
|
|
365
|
+
onLoaded: [] as OnLoadedHook[],
|
|
366
|
+
};
|
|
367
|
+
private hasRunOnLoadedHooks = false;
|
|
368
|
+
private onLoadedRunPromise: Promise<void> | null = null;
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Initializes a new Raven application instance.
|
|
372
|
+
*/
|
|
373
|
+
constructor() {
|
|
374
|
+
this.router = new RadixRouter<RouteData>();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Registers a plugin with the application instance.
|
|
379
|
+
* @param plugin The plugin object to register.
|
|
380
|
+
* @returns The plugin's declared states tuple, for per-registration state access.
|
|
381
|
+
*/
|
|
382
|
+
async register<S extends readonly ScopedState<any>[]>(
|
|
383
|
+
plugin: Plugin<S>,
|
|
384
|
+
): Promise<S> {
|
|
385
|
+
try {
|
|
386
|
+
await currentAppStorage.run(this, () => plugin.load(this));
|
|
387
|
+
} catch (err) {
|
|
388
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
389
|
+
throw new Error(`[${plugin.name}] Plugin load failed: ${message}`, {
|
|
390
|
+
cause: err,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
return plugin.states;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Internal helper to register a route handler. */
|
|
397
|
+
private addRoute(method: string, path: string, handler: Handler) {
|
|
398
|
+
this.router.add(method, path, { handler });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Registers a GET route. */
|
|
402
|
+
get(path: string, handler: Handler): this {
|
|
403
|
+
this.addRoute("GET", path, handler);
|
|
404
|
+
return this;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Registers a POST route. */
|
|
408
|
+
post(path: string, handler: Handler): this {
|
|
409
|
+
this.addRoute("POST", path, handler);
|
|
410
|
+
return this;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Registers a PUT route. */
|
|
414
|
+
put(path: string, handler: Handler): this {
|
|
415
|
+
this.addRoute("PUT", path, handler);
|
|
416
|
+
return this;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Registers a DELETE route. */
|
|
420
|
+
delete(path: string, handler: Handler): this {
|
|
421
|
+
this.addRoute("DELETE", path, handler);
|
|
422
|
+
return this;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Registers a PATCH route. */
|
|
426
|
+
patch(path: string, handler: Handler): this {
|
|
427
|
+
this.addRoute("PATCH", path, handler);
|
|
428
|
+
return this;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Adds an OnRequest hook. */
|
|
432
|
+
onRequest(hook: OnRequestHook): this {
|
|
433
|
+
this.hooks.onRequest.push(hook);
|
|
434
|
+
return this;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Adds a BeforeHandle hook. */
|
|
438
|
+
beforeHandle(hook: BeforeHandleHook): this {
|
|
439
|
+
this.hooks.beforeHandle.push(hook);
|
|
440
|
+
return this;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Adds a BeforeResponse hook. */
|
|
444
|
+
beforeResponse(hook: BeforeResponseHook): this {
|
|
445
|
+
this.hooks.beforeResponse.push(hook);
|
|
446
|
+
return this;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Adds an OnError hook. */
|
|
450
|
+
onError(hook: OnErrorHook): this {
|
|
451
|
+
this.hooks.onError.push(hook);
|
|
452
|
+
return this;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Adds an OnLoaded hook. */
|
|
456
|
+
onLoaded(hook: OnLoadedHook): this {
|
|
457
|
+
this.hooks.onLoaded.push(hook);
|
|
458
|
+
return this;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Runs onLoaded hooks exactly once, in registration order.
|
|
463
|
+
* If any hook throws, the error is propagated and later hooks are skipped.
|
|
464
|
+
*/
|
|
465
|
+
private async runOnLoadedHooksOnce(): Promise<void> {
|
|
466
|
+
if (this.hasRunOnLoadedHooks) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (this.onLoadedRunPromise) {
|
|
470
|
+
await this.onLoadedRunPromise;
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const runPromise = currentAppStorage.run(this, async () => {
|
|
475
|
+
for (const hook of this.hooks.onLoaded) {
|
|
476
|
+
await hook(this);
|
|
477
|
+
}
|
|
478
|
+
this.hasRunOnLoadedHooks = true;
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
this.onLoadedRunPromise = runPromise;
|
|
482
|
+
try {
|
|
483
|
+
await runPromise;
|
|
484
|
+
} finally {
|
|
485
|
+
this.onLoadedRunPromise = null;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Main request processing entrypoint (FetchHandler).
|
|
491
|
+
* Evaluates hooks, matches routes, and handles responses/errors.
|
|
492
|
+
* Use with HTTP server
|
|
493
|
+
*/
|
|
494
|
+
public async handle(request: Request): Promise<Response> {
|
|
495
|
+
// Ensure app-level initialization hooks complete before serving requests.
|
|
496
|
+
await this.runOnLoadedHooksOnce();
|
|
497
|
+
return currentAppStorage.run(this, () => {
|
|
498
|
+
return requestStorage.run(new Map(), async () => {
|
|
499
|
+
try {
|
|
500
|
+
const globalOnRequest = this.hooks.onRequest;
|
|
501
|
+
for (const hook of globalOnRequest) {
|
|
502
|
+
const result = await hook(request);
|
|
503
|
+
if (result instanceof Response) {
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const url = new URL(request.url);
|
|
509
|
+
const match = this.router.find(request.method, url.pathname);
|
|
510
|
+
if (!match) {
|
|
511
|
+
const ctx = new Context(request);
|
|
512
|
+
RavenContext.set(ctx);
|
|
513
|
+
return this.handleError(new Error("Not Found"), 404);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const { data: routeData, params } = match;
|
|
517
|
+
|
|
518
|
+
const query: Record<string, string> = {};
|
|
519
|
+
url.searchParams.forEach((value, key) => {
|
|
520
|
+
query[key] = value;
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
const ctx = new Context(request, params, query);
|
|
524
|
+
RavenContext.set(ctx);
|
|
525
|
+
|
|
526
|
+
await this.processStates(request, params, query);
|
|
527
|
+
|
|
528
|
+
for (const hook of this.hooks.beforeHandle) {
|
|
529
|
+
const result = await hook();
|
|
530
|
+
if (result instanceof Response) {
|
|
531
|
+
return this.handleResponseHooks(result);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const response = await routeData.handler();
|
|
536
|
+
|
|
537
|
+
return this.handleResponseHooks(response);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
if (!RavenContext.get()) {
|
|
540
|
+
RavenContext.set(new Context(request));
|
|
541
|
+
}
|
|
542
|
+
return this.handleError(
|
|
543
|
+
error instanceof Error
|
|
544
|
+
? error
|
|
545
|
+
: RavenError.ERR_UNKNOWN_ERROR(String(error)),
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Parses and stores request state (parameters, query, headers, body).
|
|
554
|
+
*/
|
|
555
|
+
private async processStates(
|
|
556
|
+
request: Request,
|
|
557
|
+
params: Record<string, string>,
|
|
558
|
+
query: Record<string, string>,
|
|
559
|
+
): Promise<void> {
|
|
560
|
+
const headersObj: Record<string, string> = {};
|
|
561
|
+
request.headers.forEach((value, key) => {
|
|
562
|
+
headersObj[key.toLowerCase()] = value;
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
ParamsState.set(params);
|
|
566
|
+
QueryState.set(query);
|
|
567
|
+
HeadersState.set(headersObj);
|
|
568
|
+
|
|
569
|
+
const contentType = request.headers.get("content-type") || "";
|
|
570
|
+
if (contentType.includes("application/json")) {
|
|
571
|
+
let data: unknown;
|
|
572
|
+
try {
|
|
573
|
+
data = await request.json();
|
|
574
|
+
} catch (err) {
|
|
575
|
+
throw RavenError.ERR_BAD_REQUEST(
|
|
576
|
+
`Invalid JSON body: ${err instanceof Error ? err.message : "parse error"}`,
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
BodyState.set(data);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Runs response modification hooks.
|
|
585
|
+
*/
|
|
586
|
+
private async handleResponseHooks(response: Response): Promise<Response> {
|
|
587
|
+
let currentResponse = response;
|
|
588
|
+
for (const hook of this.hooks.beforeResponse) {
|
|
589
|
+
const result = await hook(currentResponse);
|
|
590
|
+
if (result instanceof Response) {
|
|
591
|
+
currentResponse = result;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return currentResponse;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Evaluates error hooks and formats standard framework errors.
|
|
599
|
+
*/
|
|
600
|
+
private async handleError(
|
|
601
|
+
error: Error,
|
|
602
|
+
status: number = 500,
|
|
603
|
+
): Promise<Response> {
|
|
604
|
+
const onErrorHooks = this.hooks.onError;
|
|
605
|
+
if (onErrorHooks.length > 0) {
|
|
606
|
+
for (const hook of onErrorHooks) {
|
|
607
|
+
const result = await hook(error);
|
|
608
|
+
if (result instanceof Response) {
|
|
609
|
+
return result;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (isRavenError(error)) {
|
|
615
|
+
return error.toResponse();
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (status === 404) {
|
|
619
|
+
return new Response("Not Found", { status: 404 });
|
|
620
|
+
}
|
|
621
|
+
console.error("Unhandled error:", error);
|
|
622
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
623
|
+
}
|
|
624
|
+
}
|