yinzerflow 0.2.10 → 0.3.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/docs/core/context.md +138 -0
- package/docs/core/examples.md +259 -10
- package/docs/core/routes.md +190 -27
- package/index.d.ts +1675 -27
- package/index.js +12 -12
- package/index.js.map +9 -7
- package/package.json +1 -1
- package/example/README.md +0 -11
- package/example/app/handlers/example.ts +0 -27
- package/example/app/index.ts +0 -107
- package/example/app/routes/example.ts +0 -18
- package/example/app/routes/group-example.ts +0 -13
- package/example/app/util/customLogger.ts +0 -166
- package/example/docker-compose.yml +0 -28
- package/example/package.json +0 -16
- package/example/tsconfig.json +0 -54
package/index.d.ts
CHANGED
|
@@ -7,11 +7,158 @@ export type DeepPartial<T> = {
|
|
|
7
7
|
[P in keyof T]?: T[P] extends object ? T[P] extends Array<infer U> ? Array<U> // Keep arrays as-is, don't make array items partial
|
|
8
8
|
: DeepPartial<T[P]> : T[P];
|
|
9
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* Generic types for handler callbacks that define the structure of request data
|
|
12
|
+
*
|
|
13
|
+
* This interface allows you to customize the types of body, response, query,
|
|
14
|
+
* params, and state data that your handlers can work with. Extend this interface
|
|
15
|
+
* to create type-safe contexts for your specific use cases.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* // Basic usage with default types
|
|
20
|
+
* const handler: HandlerCallback = async (ctx) => {
|
|
21
|
+
* // ctx.request.body is unknown
|
|
22
|
+
* // ctx.state is Record<string, unknown>
|
|
23
|
+
* return { message: 'Hello' };
|
|
24
|
+
* };
|
|
25
|
+
*
|
|
26
|
+
* // Custom types for specific endpoints
|
|
27
|
+
* interface UserCreateContext extends InternalHandlerCallbackGenerics {
|
|
28
|
+
* body: { name: string; email: string; age: number };
|
|
29
|
+
* response: { id: string; name: string; email: string };
|
|
30
|
+
* state: { requestId: string; userAgent: string };
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* const createUser: HandlerCallback<UserCreateContext> = async (ctx) => {
|
|
34
|
+
* // Fully typed!
|
|
35
|
+
* const { name, email, age } = ctx.request.body; // Type: { name: string; email: string; age: number }
|
|
36
|
+
* const { requestId, userAgent } = ctx.state; // Type: { requestId: string; userAgent: string }
|
|
37
|
+
*
|
|
38
|
+
* const user = await createUserInDatabase({ name, email, age });
|
|
39
|
+
*
|
|
40
|
+
* return { id: user.id, name: user.name, email: user.email }; // Must match response type
|
|
41
|
+
* };
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
10
44
|
export interface InternalHandlerCallbackGenerics {
|
|
45
|
+
/**
|
|
46
|
+
* The expected type of the request body
|
|
47
|
+
*
|
|
48
|
+
* Defaults to `unknown` for safety. Override with your specific body schema
|
|
49
|
+
* to get full type safety and IntelliSense.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* interface LoginContext extends InternalHandlerCallbackGenerics {
|
|
54
|
+
* body: { username: string; password: string };
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* const login: HandlerCallback<LoginContext> = async (ctx) => {
|
|
58
|
+
* const { username, password } = ctx.request.body; // Fully typed!
|
|
59
|
+
* // ... authentication logic
|
|
60
|
+
* };
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
11
63
|
body?: unknown;
|
|
64
|
+
/**
|
|
65
|
+
* The expected type of the response data
|
|
66
|
+
*
|
|
67
|
+
* Defaults to `unknown`. Override to ensure your handler returns the correct
|
|
68
|
+
* data structure and get compile-time validation.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* interface UserListContext extends InternalHandlerCallbackGenerics {
|
|
73
|
+
* response: { users: Array<{ id: string; name: string }>; total: number };
|
|
74
|
+
* }
|
|
75
|
+
*
|
|
76
|
+
* const listUsers: HandlerCallback<UserListContext> = async (ctx) => {
|
|
77
|
+
* const users = await getUsersFromDatabase();
|
|
78
|
+
*
|
|
79
|
+
* // TypeScript will ensure this matches the response type
|
|
80
|
+
* return {
|
|
81
|
+
* users: users.map(u => ({ id: u.id, name: u.name })),
|
|
82
|
+
* total: users.length
|
|
83
|
+
* };
|
|
84
|
+
* };
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
12
87
|
response?: unknown;
|
|
88
|
+
/**
|
|
89
|
+
* The expected type of query parameters
|
|
90
|
+
*
|
|
91
|
+
* Defaults to `Record<string, string>`. Override for more specific query
|
|
92
|
+
* parameter validation and typing.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* interface SearchContext extends InternalHandlerCallbackGenerics {
|
|
97
|
+
* query: { q: string; limit?: string; page?: string };
|
|
98
|
+
* }
|
|
99
|
+
*
|
|
100
|
+
* const search: HandlerCallback<SearchContext> = async (ctx) => {
|
|
101
|
+
* const { q, limit = '10', page = '1' } = ctx.request.query;
|
|
102
|
+
* const limitNum = parseInt(limit);
|
|
103
|
+
* const pageNum = parseInt(page);
|
|
104
|
+
*
|
|
105
|
+
* // ... search logic
|
|
106
|
+
* };
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
13
109
|
query?: Record<string, string>;
|
|
110
|
+
/**
|
|
111
|
+
* The expected type of route parameters
|
|
112
|
+
*
|
|
113
|
+
* Defaults to `Record<string, string>`. Override for more specific route
|
|
114
|
+
* parameter validation and typing.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```typescript
|
|
118
|
+
* interface UserDetailContext extends InternalHandlerCallbackGenerics {
|
|
119
|
+
* params: { id: string; tab?: string };
|
|
120
|
+
* }
|
|
121
|
+
*
|
|
122
|
+
* const getUser: HandlerCallback<UserDetailContext> = async (ctx) => {
|
|
123
|
+
* const { id, tab = 'profile' } = ctx.request.params;
|
|
124
|
+
*
|
|
125
|
+
* // ... user retrieval logic
|
|
126
|
+
* };
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
14
129
|
params?: Record<string, string>;
|
|
130
|
+
/**
|
|
131
|
+
* User-defined state data that persists throughout the request lifecycle
|
|
132
|
+
*
|
|
133
|
+
* This allows you to store custom data like:
|
|
134
|
+
* - Authenticated user information
|
|
135
|
+
* - Request-scoped variables
|
|
136
|
+
* - Middleware data
|
|
137
|
+
* - Custom context information
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```typescript
|
|
141
|
+
* interface AuthContext extends InternalHandlerCallbackGenerics {
|
|
142
|
+
* state: {
|
|
143
|
+
* user: User;
|
|
144
|
+
* permissions: string[];
|
|
145
|
+
* requestId: string;
|
|
146
|
+
* };
|
|
147
|
+
* }
|
|
148
|
+
*
|
|
149
|
+
* const handler: HandlerCallback<AuthContext> = async (ctx) => {
|
|
150
|
+
* // Fully typed state access
|
|
151
|
+
* const { user, permissions, requestId } = ctx.state;
|
|
152
|
+
*
|
|
153
|
+
* if (permissions.includes('admin')) {
|
|
154
|
+
* return { message: 'Admin access granted', user };
|
|
155
|
+
* }
|
|
156
|
+
*
|
|
157
|
+
* return { error: 'Insufficient permissions' };
|
|
158
|
+
* };
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
state?: Record<string, unknown>;
|
|
15
162
|
}
|
|
16
163
|
declare const httpStatusCode: {
|
|
17
164
|
readonly ok: 200;
|
|
@@ -126,38 +273,861 @@ declare const httpHeaders: {
|
|
|
126
273
|
readonly clearSiteData: "Clear-Site-Data";
|
|
127
274
|
readonly noVarySearch: "No-Vary-Search";
|
|
128
275
|
};
|
|
276
|
+
/**
|
|
277
|
+
* HTTP status code constants for response status.
|
|
278
|
+
*
|
|
279
|
+
* Standard HTTP status codes used in responses to indicate
|
|
280
|
+
* the result of processing a request.
|
|
281
|
+
*
|
|
282
|
+
* ## Status Code Categories
|
|
283
|
+
*
|
|
284
|
+
* - **2xx Success**: Request was successful
|
|
285
|
+
* - **3xx Redirection**: Further action needed
|
|
286
|
+
* - **4xx Client Error**: Request was invalid
|
|
287
|
+
* - **5xx Server Error**: Server failed to process request
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* ```typescript
|
|
291
|
+
* // Setting response status codes
|
|
292
|
+
* ctx.response.setStatusCode(InternalHttpStatusCode.ok); // 200
|
|
293
|
+
* ctx.response.setStatusCode(InternalHttpStatusCode.created); // 201
|
|
294
|
+
* ctx.response.setStatusCode(InternalHttpStatusCode.noContent); // 204
|
|
295
|
+
*
|
|
296
|
+
* // Error handling
|
|
297
|
+
* try {
|
|
298
|
+
* const result = await processRequest(ctx.request);
|
|
299
|
+
* ctx.response.setStatusCode(InternalHttpStatusCode.ok);
|
|
300
|
+
* return result;
|
|
301
|
+
* } catch (error) {
|
|
302
|
+
* if (error.name === 'ValidationError') {
|
|
303
|
+
* ctx.response.setStatusCode(InternalHttpStatusCode.badRequest); // 400
|
|
304
|
+
* return { error: 'Validation failed' };
|
|
305
|
+
* }
|
|
306
|
+
*
|
|
307
|
+
* if (error.name === 'UnauthorizedError') {
|
|
308
|
+
* ctx.response.setStatusCode(InternalHttpStatusCode.unauthorized); // 401
|
|
309
|
+
* return { error: 'Unauthorized' };
|
|
310
|
+
* }
|
|
311
|
+
*
|
|
312
|
+
* ctx.response.setStatusCode(InternalHttpStatusCode.internalServerError); // 500
|
|
313
|
+
* return { error: 'Internal server error' };
|
|
314
|
+
* }
|
|
315
|
+
*
|
|
316
|
+
* // Status code validation
|
|
317
|
+
* const isValidStatusCode = (code: number): boolean => {
|
|
318
|
+
* return Object.values(InternalHttpStatusCode).includes(code as any);
|
|
319
|
+
* };
|
|
320
|
+
* ```
|
|
321
|
+
*
|
|
322
|
+
* @see {@link InternalHttpStatus} for corresponding status text
|
|
323
|
+
* @see {@link httpStatusCode} for the underlying constant values
|
|
324
|
+
*/
|
|
129
325
|
export type InternalHttpStatusCode = CreateEnum<typeof httpStatusCode>;
|
|
326
|
+
/**
|
|
327
|
+
* HTTP method constants for request methods.
|
|
328
|
+
*
|
|
329
|
+
* Standard HTTP methods used in requests to specify the desired
|
|
330
|
+
* action to be performed on the identified resource.
|
|
331
|
+
*
|
|
332
|
+
* ## HTTP Methods
|
|
333
|
+
*
|
|
334
|
+
* - **GET**: Retrieve a resource
|
|
335
|
+
* - **POST**: Create a new resource
|
|
336
|
+
* - **PUT**: Replace an entire resource
|
|
337
|
+
* - **PATCH**: Partially modify a resource
|
|
338
|
+
* - **DELETE**: Remove a resource
|
|
339
|
+
* - **HEAD**: Get resource metadata only
|
|
340
|
+
* - **OPTIONS**: Get available methods
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* ```typescript
|
|
344
|
+
* // Method-based routing
|
|
345
|
+
* const handleRequest = async (ctx: Context) => {
|
|
346
|
+
* switch (ctx.request.method) {
|
|
347
|
+
* case InternalHttpMethod.get:
|
|
348
|
+
* return await getResource(ctx.request.params.id);
|
|
349
|
+
*
|
|
350
|
+
* case InternalHttpMethod.post:
|
|
351
|
+
* return await createResource(ctx.request.body);
|
|
352
|
+
*
|
|
353
|
+
* case InternalHttpMethod.put:
|
|
354
|
+
* return await updateResource(ctx.request.params.id, ctx.request.body);
|
|
355
|
+
*
|
|
356
|
+
* case InternalHttpMethod.patch:
|
|
357
|
+
* return await patchResource(ctx.request.params.id, ctx.request.body);
|
|
358
|
+
*
|
|
359
|
+
* case InternalHttpMethod.delete:
|
|
360
|
+
* return await deleteResource(ctx.request.params.id);
|
|
361
|
+
*
|
|
362
|
+
* case InternalHttpMethod.options:
|
|
363
|
+
* return { methods: ['GET', 'POST', 'PUT', 'DELETE'] };
|
|
364
|
+
*
|
|
365
|
+
* default:
|
|
366
|
+
* throw new Error(`Method ${ctx.request.method} not supported`);
|
|
367
|
+
* }
|
|
368
|
+
* };
|
|
369
|
+
*
|
|
370
|
+
* // Method validation
|
|
371
|
+
* const isReadMethod = (method: string): boolean => {
|
|
372
|
+
* return method === InternalHttpMethod.get || method === InternalHttpMethod.head;
|
|
373
|
+
* };
|
|
374
|
+
*
|
|
375
|
+
* const isWriteMethod = (method: string): boolean => {
|
|
376
|
+
* return [InternalHttpMethod.post, InternalHttpMethod.put,
|
|
377
|
+
* InternalHttpMethod.patch, InternalHttpMethod.delete].includes(method as any);
|
|
378
|
+
* };
|
|
379
|
+
* ```
|
|
380
|
+
*
|
|
381
|
+
* @see {@link httpMethod} for the underlying constant values
|
|
382
|
+
*/
|
|
130
383
|
export type InternalHttpMethod = CreateEnum<typeof httpMethod>;
|
|
384
|
+
/**
|
|
385
|
+
* HTTP header constants for request and response headers.
|
|
386
|
+
*
|
|
387
|
+
* Comprehensive collection of HTTP header names organized by category,
|
|
388
|
+
* including standard headers, security headers, and custom headers.
|
|
389
|
+
*
|
|
390
|
+
* ## Header Categories
|
|
391
|
+
*
|
|
392
|
+
* - **Authentication**: Authorization, WWW-Authenticate
|
|
393
|
+
* - **Caching**: Cache-Control, ETag, Expires
|
|
394
|
+
* - **Content**: Content-Type, Content-Length, Content-Encoding
|
|
395
|
+
* - **CORS**: Access-Control-Allow-*, Access-Control-Request-*
|
|
396
|
+
* - **Security**: Content-Security-Policy, X-Frame-Options
|
|
397
|
+
* - **Custom**: X-Powered-By, X-Request-ID
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* ```typescript
|
|
401
|
+
* // Setting security headers
|
|
402
|
+
* ctx.response.addHeaders({
|
|
403
|
+
* [InternalHttpHeaders.contentSecurityPolicy]: "default-src 'self'",
|
|
404
|
+
* [InternalHttpHeaders.xFrameOptions]: 'DENY',
|
|
405
|
+
* [InternalHttpHeaders.xContentTypeOptions]: 'nosniff',
|
|
406
|
+
* [InternalHttpHeaders.strictTransportSecurity]: 'max-age=31536000'
|
|
407
|
+
* });
|
|
408
|
+
*
|
|
409
|
+
* // CORS headers
|
|
410
|
+
* ctx.response.addHeaders({
|
|
411
|
+
* [InternalHttpHeaders.accessControlAllowOrigin]: '*',
|
|
412
|
+
* [InternalHttpHeaders.accessControlAllowMethods]: 'GET, POST, PUT, DELETE',
|
|
413
|
+
* [InternalHttpHeaders.accessControlAllowHeaders]: 'Content-Type, Authorization'
|
|
414
|
+
* });
|
|
415
|
+
*
|
|
416
|
+
* // Custom headers
|
|
417
|
+
* ctx.response.addHeaders({
|
|
418
|
+
* [InternalHttpHeaders.xRequestId]: generateRequestId(),
|
|
419
|
+
* [InternalHttpHeaders.xProcessingTime]: `${processingTime}ms`
|
|
420
|
+
* });
|
|
421
|
+
*
|
|
422
|
+
* // Header validation
|
|
423
|
+
* const isSecurityHeader = (headerName: string): boolean => {
|
|
424
|
+
* const securityHeaders = [
|
|
425
|
+
* InternalHttpHeaders.contentSecurityPolicy,
|
|
426
|
+
* InternalHttpHeaders.xFrameOptions,
|
|
427
|
+
* InternalHttpHeaders.xContentTypeOptions,
|
|
428
|
+
* InternalHttpHeaders.strictTransportSecurity
|
|
429
|
+
* ];
|
|
430
|
+
* return securityHeaders.includes(headerName as any);
|
|
431
|
+
* };
|
|
432
|
+
*
|
|
433
|
+
* // Request header processing
|
|
434
|
+
* const processRequestHeaders = (headers: Record<string, string>) => {
|
|
435
|
+
* const authToken = headers[InternalHttpHeaders.authorization];
|
|
436
|
+
* const contentType = headers[InternalHttpHeaders.contentType];
|
|
437
|
+
* const userAgent = headers[InternalHttpHeaders.userAgent];
|
|
438
|
+
*
|
|
439
|
+
* // Process headers...
|
|
440
|
+
* return { authToken, contentType, userAgent };
|
|
441
|
+
* };
|
|
442
|
+
* ```
|
|
443
|
+
*
|
|
444
|
+
* @see {@link httpHeaders} for the underlying constant values
|
|
445
|
+
* @see {@link Request} for accessing headers in requests
|
|
446
|
+
* @see {@link Response} for setting headers in responses
|
|
447
|
+
*/
|
|
131
448
|
export type InternalHttpHeaders = Lowercase<CreateEnum<typeof httpHeaders>> | string;
|
|
132
449
|
interface Request$1<T extends InternalHandlerCallbackGenerics = InternalHandlerCallbackGenerics> {
|
|
450
|
+
/**
|
|
451
|
+
* The HTTP protocol version (e.g., "HTTP/1.1").
|
|
452
|
+
*
|
|
453
|
+
* @example
|
|
454
|
+
* ```typescript
|
|
455
|
+
* const handler: HandlerCallback = async (ctx) => {
|
|
456
|
+
* if (ctx.request.protocol === 'HTTP/2.0') {
|
|
457
|
+
* // Use HTTP/2 specific features
|
|
458
|
+
* ctx.response.addHeaders({ 'X-HTTP-Version': '2.0' });
|
|
459
|
+
* }
|
|
460
|
+
*
|
|
461
|
+
* return { protocol: ctx.request.protocol };
|
|
462
|
+
* };
|
|
463
|
+
* ```
|
|
464
|
+
*/
|
|
133
465
|
protocol: string;
|
|
466
|
+
/**
|
|
467
|
+
* The HTTP method of the request (GET, POST, PUT, DELETE, etc.).
|
|
468
|
+
*
|
|
469
|
+
* @example
|
|
470
|
+
* ```typescript
|
|
471
|
+
* const handler: HandlerCallback = async (ctx) => {
|
|
472
|
+
* const { method } = ctx.request;
|
|
473
|
+
*
|
|
474
|
+
* switch (method) {
|
|
475
|
+
* case 'GET':
|
|
476
|
+
* return { action: 'retrieve', data: await getData() };
|
|
477
|
+
* case 'POST':
|
|
478
|
+
* return { action: 'create', data: await createData(ctx.request.body) };
|
|
479
|
+
* case 'PUT':
|
|
480
|
+
* return { action: 'update', data: await updateData(ctx.request.body) };
|
|
481
|
+
* case 'DELETE':
|
|
482
|
+
* return { action: 'delete', data: await deleteData(ctx.request.params.id) };
|
|
483
|
+
* default:
|
|
484
|
+
* throw new Error(`Unsupported method: ${method}`);
|
|
485
|
+
* }
|
|
486
|
+
* };
|
|
487
|
+
* ```
|
|
488
|
+
*
|
|
489
|
+
* @see {@link InternalHttpMethod} for all available HTTP methods
|
|
490
|
+
*/
|
|
134
491
|
method: InternalHttpMethod;
|
|
492
|
+
/**
|
|
493
|
+
* The request path/URL without query parameters.
|
|
494
|
+
*
|
|
495
|
+
* @example
|
|
496
|
+
* ```typescript
|
|
497
|
+
* const handler: HandlerCallback = async (ctx) => {
|
|
498
|
+
* const { path } = ctx.request;
|
|
499
|
+
*
|
|
500
|
+
* // Log the requested path
|
|
501
|
+
* console.log(`Request to: ${path}`);
|
|
502
|
+
*
|
|
503
|
+
* // Route-specific logic based on path
|
|
504
|
+
* if (path.startsWith('/api/v1/')) {
|
|
505
|
+
* ctx.state.apiVersion = 'v1';
|
|
506
|
+
* } else if (path.startsWith('/api/v2/')) {
|
|
507
|
+
* ctx.state.apiVersion = 'v2';
|
|
508
|
+
* }
|
|
509
|
+
*
|
|
510
|
+
* return { requestedPath: path, apiVersion: ctx.state.apiVersion };
|
|
511
|
+
* };
|
|
512
|
+
* ```
|
|
513
|
+
*/
|
|
135
514
|
path: string;
|
|
515
|
+
/**
|
|
516
|
+
* HTTP headers sent with the request.
|
|
517
|
+
*
|
|
518
|
+
* Headers are case-insensitive and commonly include authorization,
|
|
519
|
+
* content-type, user-agent, and custom headers.
|
|
520
|
+
*
|
|
521
|
+
* @example
|
|
522
|
+
* ```typescript
|
|
523
|
+
* const handler: HandlerCallback = async (ctx) => {
|
|
524
|
+
* const { headers } = ctx.request;
|
|
525
|
+
*
|
|
526
|
+
* // Authentication
|
|
527
|
+
* const authToken = headers.authorization;
|
|
528
|
+
* if (!authToken) {
|
|
529
|
+
* throw new Error('Authorization header required');
|
|
530
|
+
* }
|
|
531
|
+
*
|
|
532
|
+
* // Content type validation
|
|
533
|
+
* const contentType = headers['content-type'];
|
|
534
|
+
* if (contentType !== 'application/json') {
|
|
535
|
+
* throw new Error('Content-Type must be application/json');
|
|
536
|
+
* }
|
|
537
|
+
*
|
|
538
|
+
* // Custom headers
|
|
539
|
+
* const requestId = headers['x-request-id'];
|
|
540
|
+
* const userId = headers['x-user-id'];
|
|
541
|
+
*
|
|
542
|
+
* return {
|
|
543
|
+
* hasAuth: !!authToken,
|
|
544
|
+
* contentType,
|
|
545
|
+
* requestId,
|
|
546
|
+
* userId
|
|
547
|
+
* };
|
|
548
|
+
* };
|
|
549
|
+
* ```
|
|
550
|
+
*
|
|
551
|
+
* @see {@link InternalHttpHeaders} for all available header names
|
|
552
|
+
*/
|
|
136
553
|
headers: Partial<Record<InternalHttpHeaders, string>>;
|
|
554
|
+
/**
|
|
555
|
+
* The parsed request body, typed according to the generic parameter.
|
|
556
|
+
*
|
|
557
|
+
* The body is automatically parsed based on Content-Type header.
|
|
558
|
+
* For JSON requests, this will be the parsed JavaScript object.
|
|
559
|
+
*
|
|
560
|
+
* @example
|
|
561
|
+
* ```typescript
|
|
562
|
+
* // Basic body access
|
|
563
|
+
* const handler: HandlerCallback = async (ctx) => {
|
|
564
|
+
* const { body } = ctx.request;
|
|
565
|
+
*
|
|
566
|
+
* // body is typed as 'unknown' by default
|
|
567
|
+
* if (typeof body === 'object' && body !== null) {
|
|
568
|
+
* const userData = body as { name: string; email: string };
|
|
569
|
+
* return { received: userData };
|
|
570
|
+
* }
|
|
571
|
+
*
|
|
572
|
+
* return { error: 'Invalid body format' };
|
|
573
|
+
* };
|
|
574
|
+
*
|
|
575
|
+
* // Typed body with generics
|
|
576
|
+
* interface CreateUserRequest extends InternalHandlerCallbackGenerics {
|
|
577
|
+
* body: { name: string; email: string; age: number };
|
|
578
|
+
* }
|
|
579
|
+
*
|
|
580
|
+
* const createUser: HandlerCallback<CreateUserRequest> = async (ctx) => {
|
|
581
|
+
* const { name, email, age } = ctx.request.body; // Fully typed!
|
|
582
|
+
*
|
|
583
|
+
* // Validate age
|
|
584
|
+
* if (age < 18) {
|
|
585
|
+
* throw new Error('User must be 18 or older');
|
|
586
|
+
* }
|
|
587
|
+
*
|
|
588
|
+
* const user = await createUserInDatabase({ name, email, age });
|
|
589
|
+
* return { success: true, user };
|
|
590
|
+
* };
|
|
591
|
+
* ```
|
|
592
|
+
*
|
|
593
|
+
* @see {@link InternalHandlerCallbackGenerics} for custom body typing
|
|
594
|
+
*/
|
|
137
595
|
body: T["body"];
|
|
596
|
+
/**
|
|
597
|
+
* Query parameters from the URL, typed according to the generic parameter.
|
|
598
|
+
*
|
|
599
|
+
* Query parameters are the key-value pairs after the ? in the URL.
|
|
600
|
+
* They're automatically parsed and made available here.
|
|
601
|
+
*
|
|
602
|
+
* @example
|
|
603
|
+
* ```typescript
|
|
604
|
+
* // Basic query parameter access
|
|
605
|
+
* const handler: HandlerCallback = async (ctx) => {
|
|
606
|
+
* const { query } = ctx.request;
|
|
607
|
+
*
|
|
608
|
+
* // query is typed as 'unknown' by default
|
|
609
|
+
* if (typeof query === 'object' && query !== null) {
|
|
610
|
+
* const { page, limit, search } = query as { page?: string; limit?: string; search?: string };
|
|
611
|
+
*
|
|
612
|
+
* const pageNum = parseInt(page || '1');
|
|
613
|
+
* const limitNum = parseInt(limit || '10');
|
|
614
|
+
*
|
|
615
|
+
* return { pagination: { page: pageNum, limit: limitNum }, search };
|
|
616
|
+
* }
|
|
617
|
+
*
|
|
618
|
+
* return { pagination: { page: 1, limit: 10 } };
|
|
619
|
+
* };
|
|
620
|
+
*
|
|
621
|
+
* // Typed query parameters with generics
|
|
622
|
+
* interface UserListRequest extends InternalHandlerCallbackGenerics {
|
|
623
|
+
* query: { page: string; limit: string; search?: string; sort?: string };
|
|
624
|
+
* }
|
|
625
|
+
*
|
|
626
|
+
* const listUsers: HandlerCallback<UserListRequest> = async (ctx) => {
|
|
627
|
+
* const { page, limit, search, sort } = ctx.request.query; // Fully typed!
|
|
628
|
+
*
|
|
629
|
+
* const pageNum = parseInt(page);
|
|
630
|
+
* const limitNum = parseInt(limit);
|
|
631
|
+
*
|
|
632
|
+
* const users = await getUsersFromDatabase({
|
|
633
|
+
* page: pageNum,
|
|
634
|
+
* limit: limitNum,
|
|
635
|
+
* search: search || '',
|
|
636
|
+
* sort: sort || 'name'
|
|
637
|
+
* });
|
|
638
|
+
*
|
|
639
|
+
* return { users, pagination: { page: pageNum, limit: limitNum } };
|
|
640
|
+
* };
|
|
641
|
+
* ```
|
|
642
|
+
*
|
|
643
|
+
* @see {@link InternalHandlerCallbackGenerics} for custom query typing
|
|
644
|
+
*/
|
|
138
645
|
query: T["query"];
|
|
646
|
+
/**
|
|
647
|
+
* Route parameters extracted from the URL path, typed according to the generic parameter.
|
|
648
|
+
*
|
|
649
|
+
* Route parameters are the dynamic parts of the URL path (e.g., /users/:id).
|
|
650
|
+
* They're automatically extracted and made available here.
|
|
651
|
+
*
|
|
652
|
+
* @example
|
|
653
|
+
* ```typescript
|
|
654
|
+
* // Basic route parameter access
|
|
655
|
+
* const handler: HandlerCallback = async (ctx) => {
|
|
656
|
+
* const { params } = ctx.request;
|
|
657
|
+
*
|
|
658
|
+
* // params is typed as 'unknown' by default
|
|
659
|
+
* if (typeof params === 'object' && params !== null) {
|
|
660
|
+
* const { id, category } = params as { id?: string; category?: string };
|
|
661
|
+
*
|
|
662
|
+
* if (!id) {
|
|
663
|
+
* throw new Error('User ID is required');
|
|
664
|
+
* }
|
|
665
|
+
*
|
|
666
|
+
* return { userId: id, category: category || 'default' };
|
|
667
|
+
* }
|
|
668
|
+
*
|
|
669
|
+
* return { error: 'No parameters found' };
|
|
670
|
+
* };
|
|
671
|
+
*
|
|
672
|
+
* // Typed route parameters with generics
|
|
673
|
+
* interface UserDetailRequest extends InternalHandlerCallbackGenerics {
|
|
674
|
+
* params: { id: string; tab?: string };
|
|
675
|
+
* }
|
|
676
|
+
*
|
|
677
|
+
* const getUser: HandlerCallback<UserDetailRequest> = async (ctx) => {
|
|
678
|
+
* const { id, tab } = ctx.request.params; // Fully typed!
|
|
679
|
+
*
|
|
680
|
+
* const user = await getUserById(id);
|
|
681
|
+
* if (!user) {
|
|
682
|
+
* throw new Error('User not found');
|
|
683
|
+
* }
|
|
684
|
+
*
|
|
685
|
+
* // Return different data based on tab parameter
|
|
686
|
+
* switch (tab) {
|
|
687
|
+
* case 'profile':
|
|
688
|
+
* return { user: { id: user.id, name: user.name, email: user.email } };
|
|
689
|
+
* case 'settings':
|
|
690
|
+
* return { user: { id: user.id, preferences: user.preferences } };
|
|
691
|
+
* default:
|
|
692
|
+
* return { user };
|
|
693
|
+
* }
|
|
694
|
+
* };
|
|
695
|
+
* ```
|
|
696
|
+
*
|
|
697
|
+
* @see {@link InternalHandlerCallbackGenerics} for custom params typing
|
|
698
|
+
*/
|
|
139
699
|
params: T["params"];
|
|
140
|
-
|
|
141
|
-
|
|
700
|
+
/**
|
|
701
|
+
* The IP address of the client making the request.
|
|
702
|
+
*
|
|
703
|
+
* **Important**: If you're behind a proxy, load balancer, or reverse proxy,
|
|
704
|
+
* you may need to configure it to forward the real client IP address.
|
|
705
|
+
*
|
|
706
|
+
* @example
|
|
707
|
+
* ```typescript
|
|
708
|
+
* const handler: HandlerCallback = async (ctx) => {
|
|
709
|
+
* const { ipAddress } = ctx.request;
|
|
710
|
+
*
|
|
711
|
+
* // Log client IP for security and analytics
|
|
712
|
+
* console.log(`Request from IP: ${ipAddress}`);
|
|
713
|
+
*
|
|
714
|
+
* // Rate limiting by IP
|
|
715
|
+
* const requestCount = await getRequestCount(ipAddress);
|
|
716
|
+
* if (requestCount > 100) {
|
|
717
|
+
* throw new Error('Rate limit exceeded');
|
|
718
|
+
* }
|
|
719
|
+
*
|
|
720
|
+
* // Geolocation (if needed)
|
|
721
|
+
* const country = await getCountryFromIP(ipAddress);
|
|
722
|
+
* ctx.state.clientCountry = country;
|
|
723
|
+
*
|
|
724
|
+
* return {
|
|
725
|
+
* message: 'Request processed',
|
|
726
|
+
* clientIP: ipAddress,
|
|
727
|
+
* country
|
|
728
|
+
* };
|
|
729
|
+
* };
|
|
730
|
+
* ```
|
|
731
|
+
*/
|
|
732
|
+
ipAddress: string;
|
|
733
|
+
/**
|
|
734
|
+
* The raw, unparsed request body as a Buffer or string.
|
|
735
|
+
*
|
|
736
|
+
* This is useful when you need to parse the body manually or when
|
|
737
|
+
* the automatic parsing doesn't meet your needs.
|
|
738
|
+
*
|
|
739
|
+
* @example
|
|
740
|
+
* ```typescript
|
|
741
|
+
* const handler: HandlerCallback = async (ctx) => {
|
|
742
|
+
* const { rawBody, headers } = ctx.request;
|
|
743
|
+
*
|
|
744
|
+
* // Manual parsing for specific content types
|
|
745
|
+
* const contentType = headers['content-type'];
|
|
746
|
+
*
|
|
747
|
+
* if (contentType === 'application/xml') {
|
|
748
|
+
* // Parse XML manually
|
|
749
|
+
* const xmlString = rawBody.toString('utf8');
|
|
750
|
+
* const xmlDoc = await parseXML(xmlString);
|
|
751
|
+
* return { parsed: xmlDoc };
|
|
752
|
+
* }
|
|
753
|
+
*
|
|
754
|
+
* if (contentType === 'multipart/form-data') {
|
|
755
|
+
* // Handle file uploads manually
|
|
756
|
+
* const formData = await parseMultipartFormData(rawBody);
|
|
757
|
+
* return { files: formData.files, fields: formData.fields };
|
|
758
|
+
* }
|
|
759
|
+
*
|
|
760
|
+
* // For other types, use the parsed body
|
|
761
|
+
* return { body: ctx.request.body, rawBodyLength: rawBody.length };
|
|
762
|
+
* };
|
|
763
|
+
* ```
|
|
764
|
+
*/
|
|
765
|
+
rawBody: Buffer | string;
|
|
142
766
|
}
|
|
143
767
|
interface Response$1 {
|
|
768
|
+
/**
|
|
769
|
+
* Sets the HTTP status code for the response.
|
|
770
|
+
*
|
|
771
|
+
* Common status codes:
|
|
772
|
+
* - **2xx Success**: 200 (OK), 201 (Created), 204 (No Content)
|
|
773
|
+
* - **3xx Redirection**: 301 (Moved), 302 (Found), 304 (Not Modified)
|
|
774
|
+
* - **4xx Client Error**: 400 (Bad Request), 401 (Unauthorized), 404 (Not Found)
|
|
775
|
+
* - **5xx Server Error**: 500 (Internal Server Error), 502 (Bad Gateway)
|
|
776
|
+
*
|
|
777
|
+
* @param statusCode - The HTTP status code to set
|
|
778
|
+
*
|
|
779
|
+
* @example
|
|
780
|
+
* ```typescript
|
|
781
|
+
* const handler: HandlerCallback = async (ctx) => {
|
|
782
|
+
* const { response, request } = ctx;
|
|
783
|
+
*
|
|
784
|
+
* try {
|
|
785
|
+
* if (request.method === 'POST') {
|
|
786
|
+
* // Resource created successfully
|
|
787
|
+
* response.setStatusCode(201);
|
|
788
|
+
* return { message: 'Resource created' };
|
|
789
|
+
* }
|
|
790
|
+
*
|
|
791
|
+
* if (request.method === 'DELETE') {
|
|
792
|
+
* // Resource deleted successfully
|
|
793
|
+
* response.setStatusCode(204);
|
|
794
|
+
* return; // No content for DELETE
|
|
795
|
+
* }
|
|
796
|
+
*
|
|
797
|
+
* // Default success response
|
|
798
|
+
* response.setStatusCode(200);
|
|
799
|
+
* return { message: 'Operation successful' };
|
|
800
|
+
*
|
|
801
|
+
* } catch (error) {
|
|
802
|
+
* if (error.name === 'ValidationError') {
|
|
803
|
+
* response.setStatusCode(400);
|
|
804
|
+
* return { error: 'Validation failed', details: error.message };
|
|
805
|
+
* }
|
|
806
|
+
*
|
|
807
|
+
* if (error.name === 'UnauthorizedError') {
|
|
808
|
+
* response.setStatusCode(401);
|
|
809
|
+
* return { error: 'Unauthorized' };
|
|
810
|
+
* }
|
|
811
|
+
*
|
|
812
|
+
* // Default error response
|
|
813
|
+
* response.setStatusCode(500);
|
|
814
|
+
* return { error: 'Internal server error' };
|
|
815
|
+
* }
|
|
816
|
+
* };
|
|
817
|
+
* ```
|
|
818
|
+
*
|
|
819
|
+
* @see {@link InternalHttpStatusCode} for all available status codes
|
|
820
|
+
*/
|
|
144
821
|
setStatusCode: (statusCode: InternalHttpStatusCode) => void;
|
|
822
|
+
/**
|
|
823
|
+
* Adds or updates HTTP response headers.
|
|
824
|
+
*
|
|
825
|
+
* Headers are key-value pairs that provide metadata about the response.
|
|
826
|
+
* Common headers include Content-Type, Cache-Control, and custom headers.
|
|
827
|
+
*
|
|
828
|
+
* @param headers - Object containing header names and values
|
|
829
|
+
*
|
|
830
|
+
* @example
|
|
831
|
+
* ```typescript
|
|
832
|
+
* const handler: HandlerCallback = async (ctx) => {
|
|
833
|
+
* const { response, request } = ctx;
|
|
834
|
+
*
|
|
835
|
+
* // Set basic response headers
|
|
836
|
+
* response.addHeaders({
|
|
837
|
+
* 'Content-Type': 'application/json',
|
|
838
|
+
* 'Cache-Control': 'max-age=3600, public'
|
|
839
|
+
* });
|
|
840
|
+
*
|
|
841
|
+
* // Add security headers
|
|
842
|
+
* response.addHeaders({
|
|
843
|
+
* 'X-Content-Type-Options': 'nosniff',
|
|
844
|
+
* 'X-Frame-Options': 'DENY',
|
|
845
|
+
* 'X-XSS-Protection': '1; mode=block',
|
|
846
|
+
* 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains'
|
|
847
|
+
* });
|
|
848
|
+
*
|
|
849
|
+
* // Add custom headers
|
|
850
|
+
* response.addHeaders({
|
|
851
|
+
* 'X-API-Version': 'v1.0.0',
|
|
852
|
+
* 'X-Request-ID': generateRequestId(),
|
|
853
|
+
* 'X-Processing-Time': `${Date.now() - ctx.state.startTime}ms`
|
|
854
|
+
* });
|
|
855
|
+
*
|
|
856
|
+
* // CORS headers for cross-origin requests
|
|
857
|
+
* response.addHeaders({
|
|
858
|
+
* 'Access-Control-Allow-Origin': '*',
|
|
859
|
+
* 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
860
|
+
* 'Access-Control-Allow-Headers': 'Content-Type, Authorization'
|
|
861
|
+
* });
|
|
862
|
+
*
|
|
863
|
+
* return { message: 'Headers set successfully' };
|
|
864
|
+
* };
|
|
865
|
+
* ```
|
|
866
|
+
*
|
|
867
|
+
* @see {@link InternalHttpHeaders} for all available header names
|
|
868
|
+
*/
|
|
145
869
|
addHeaders: (headers: Partial<Record<InternalHttpHeaders, string>>) => void;
|
|
870
|
+
/**
|
|
871
|
+
* Removes specific HTTP response headers by name.
|
|
872
|
+
*
|
|
873
|
+
* This is useful when you want to remove headers that might have been
|
|
874
|
+
* set by default or by previous middleware.
|
|
875
|
+
*
|
|
876
|
+
* @param headerNames - Array of header names to remove
|
|
877
|
+
*
|
|
878
|
+
* @example
|
|
879
|
+
* ```typescript
|
|
880
|
+
* const handler: HandlerCallback = async (ctx) => {
|
|
881
|
+
* const { response } = ctx;
|
|
882
|
+
*
|
|
883
|
+
* // Remove default headers that might interfere
|
|
884
|
+
* response.removeHeaders([
|
|
885
|
+
* 'X-Powered-By',
|
|
886
|
+
* 'Server',
|
|
887
|
+
* 'Date'
|
|
888
|
+
* ]);
|
|
889
|
+
*
|
|
890
|
+
* // Add custom headers
|
|
891
|
+
* response.addHeaders({
|
|
892
|
+
* 'X-Custom-Header': 'Custom Value',
|
|
893
|
+
* 'Cache-Control': 'no-cache, no-store, must-revalidate'
|
|
894
|
+
* });
|
|
895
|
+
*
|
|
896
|
+
* return { message: 'Custom response configured' };
|
|
897
|
+
* };
|
|
898
|
+
*
|
|
899
|
+
* // Conditional header removal
|
|
900
|
+
* const conditionalHandler: HandlerCallback = async (ctx) => {
|
|
901
|
+
* const { response, request } = ctx;
|
|
902
|
+
*
|
|
903
|
+
* // Remove cache headers for sensitive operations
|
|
904
|
+
* if (request.method === 'POST' || request.method === 'PUT') {
|
|
905
|
+
* response.removeHeaders(['Cache-Control', 'ETag', 'Last-Modified']);
|
|
906
|
+
*
|
|
907
|
+
* // Add no-cache headers
|
|
908
|
+
* response.addHeaders({
|
|
909
|
+
* 'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
910
|
+
* 'Pragma': 'no-cache',
|
|
911
|
+
* 'Expires': '0'
|
|
912
|
+
* });
|
|
913
|
+
* }
|
|
914
|
+
*
|
|
915
|
+
* return { message: 'Response configured based on method' };
|
|
916
|
+
* };
|
|
917
|
+
* ```
|
|
918
|
+
*
|
|
919
|
+
* @see {@link InternalHttpHeaders} for all available header names
|
|
920
|
+
*/
|
|
146
921
|
removeHeaders: (headerNames: Array<InternalHttpHeaders>) => void;
|
|
147
922
|
}
|
|
923
|
+
/**
|
|
924
|
+
* Request context that provides access to request, response, and user-defined state
|
|
925
|
+
*
|
|
926
|
+
* The context is the central object passed to all route handlers and middleware.
|
|
927
|
+
* It contains the request and response objects, plus any custom state data
|
|
928
|
+
* defined by the user through generics.
|
|
929
|
+
*
|
|
930
|
+
* @template T - Extends InternalHandlerCallbackGenerics to provide custom typing
|
|
931
|
+
*
|
|
932
|
+
* @example
|
|
933
|
+
* ```typescript
|
|
934
|
+
* // Basic usage with default context
|
|
935
|
+
* const handler: HandlerCallback = async (ctx) => {
|
|
936
|
+
* // Access request data
|
|
937
|
+
* const userId = ctx.request.params.id;
|
|
938
|
+
* const userData = ctx.request.body;
|
|
939
|
+
*
|
|
940
|
+
* // Store custom data in state
|
|
941
|
+
* ctx.state.user = { id: userId, name: 'John' };
|
|
942
|
+
* ctx.state.requestId = generateRequestId();
|
|
943
|
+
*
|
|
944
|
+
* // Control response
|
|
945
|
+
* ctx.response.setStatusCode(200);
|
|
946
|
+
* ctx.response.addHeaders({ 'X-User-ID': userId });
|
|
947
|
+
*
|
|
948
|
+
* // Return response body
|
|
949
|
+
* return { success: true, user: ctx.state.user };
|
|
950
|
+
* };
|
|
951
|
+
*
|
|
952
|
+
* // Advanced usage with custom state typing
|
|
953
|
+
* interface AuthContext extends InternalHandlerCallbackGenerics {
|
|
954
|
+
* state: {
|
|
955
|
+
* user: User;
|
|
956
|
+
* permissions: string[];
|
|
957
|
+
* session: Session;
|
|
958
|
+
* };
|
|
959
|
+
* }
|
|
960
|
+
*
|
|
961
|
+
* const authHandler: HandlerCallback<AuthContext> = async (ctx) => {
|
|
962
|
+
* // Fully type-safe access to state
|
|
963
|
+
* const { user, permissions, session } = ctx.state;
|
|
964
|
+
*
|
|
965
|
+
* // No type assertions needed!
|
|
966
|
+
* if (permissions.includes('admin')) {
|
|
967
|
+
* ctx.response.setStatusCode(200);
|
|
968
|
+
* return { message: 'Admin access granted', user };
|
|
969
|
+
* }
|
|
970
|
+
*
|
|
971
|
+
* ctx.response.setStatusCode(403);
|
|
972
|
+
* return { error: 'Insufficient permissions' };
|
|
973
|
+
* };
|
|
974
|
+
* ```
|
|
975
|
+
*/
|
|
148
976
|
export interface Context<T extends InternalHandlerCallbackGenerics = InternalHandlerCallbackGenerics> {
|
|
977
|
+
/**
|
|
978
|
+
* The incoming request object containing all request data and metadata
|
|
979
|
+
*
|
|
980
|
+
* Provides access to headers, body, query parameters, route parameters, and metadata.
|
|
981
|
+
*
|
|
982
|
+
* @example
|
|
983
|
+
* ```typescript
|
|
984
|
+
* const handler: HandlerCallback = async (ctx) => {
|
|
985
|
+
* const userId = ctx.request.params.id;
|
|
986
|
+
* const userData = ctx.request.body;
|
|
987
|
+
* const authToken = ctx.request.headers.authorization;
|
|
988
|
+
* const clientIp = ctx.request.ipAddress;
|
|
989
|
+
*
|
|
990
|
+
* return { userId, userData, hasAuth: !!authToken };
|
|
991
|
+
* };
|
|
992
|
+
* ```
|
|
993
|
+
*
|
|
994
|
+
* @see {@link Request} for complete request interface documentation
|
|
995
|
+
*/
|
|
149
996
|
request: Request$1<T>;
|
|
997
|
+
/**
|
|
998
|
+
* The outgoing response object for controlling HTTP response behavior
|
|
999
|
+
*
|
|
1000
|
+
* Provides methods to set status codes, add headers, and control response formatting.
|
|
1001
|
+
*
|
|
1002
|
+
* @example
|
|
1003
|
+
* ```typescript
|
|
1004
|
+
* const handler: HandlerCallback = async (ctx) => {
|
|
1005
|
+
* const { request, response } = ctx;
|
|
1006
|
+
*
|
|
1007
|
+
* response.setStatusCode(201);
|
|
1008
|
+
* response.addHeaders({
|
|
1009
|
+
* 'Location': `/api/users/${request.params.id}`,
|
|
1010
|
+
* 'X-User-ID': request.params.id
|
|
1011
|
+
* });
|
|
1012
|
+
*
|
|
1013
|
+
* return { message: 'User created successfully' };
|
|
1014
|
+
* };
|
|
1015
|
+
* ```
|
|
1016
|
+
*
|
|
1017
|
+
* @see {@link Response} for complete response interface documentation
|
|
1018
|
+
*/
|
|
150
1019
|
response: Response$1;
|
|
1020
|
+
/**
|
|
1021
|
+
* User-defined state data that persists throughout the request lifecycle
|
|
1022
|
+
*
|
|
1023
|
+
* State is request-scoped data that can be accessed by route handlers and middleware.
|
|
1024
|
+
* Each request gets its own isolated state object that's automatically cleaned up.
|
|
1025
|
+
*
|
|
1026
|
+
* ## State Lifecycle
|
|
1027
|
+
*
|
|
1028
|
+
* 1. **Request Start**: State object is created as empty object
|
|
1029
|
+
* 2. **Global Hooks**: `beforeAll` hooks can populate state
|
|
1030
|
+
* 3. **Route Hooks**: `beforeHooks` can access and modify state
|
|
1031
|
+
* 4. **Route Handler**: Your handler can access and modify state
|
|
1032
|
+
* 5. **Route Hooks**: `afterHooks` can access state and modify response
|
|
1033
|
+
* 6. **Global Hooks**: `afterAll` hooks can access state and modify response
|
|
1034
|
+
* 7. **Request End**: State is automatically garbage collected
|
|
1035
|
+
*
|
|
1036
|
+
* @example
|
|
1037
|
+
* ```typescript
|
|
1038
|
+
* // Store data in state
|
|
1039
|
+
* ctx.state.user = { id: 1, name: 'John' };
|
|
1040
|
+
* ctx.state.requestId = generateRequestId();
|
|
1041
|
+
*
|
|
1042
|
+
* // Access the data
|
|
1043
|
+
* const user = ctx.state.user;
|
|
1044
|
+
* const requestId = ctx.state.requestId;
|
|
1045
|
+
* ```
|
|
1046
|
+
*/
|
|
1047
|
+
state: T["state"] extends Record<string, unknown> ? T["state"] : Record<string, unknown>;
|
|
151
1048
|
}
|
|
152
1049
|
/**
|
|
153
|
-
* Represents a route handler function that
|
|
1050
|
+
* Represents a route handler function that processes requests and returns responses.
|
|
1051
|
+
*
|
|
1052
|
+
* This type defines the signature for all route handlers, middleware, and hooks
|
|
1053
|
+
* in YinzerFlow. The function receives a context object and can optionally
|
|
1054
|
+
* receive an error parameter for error handlers.
|
|
1055
|
+
*
|
|
1056
|
+
* ## Handler Types
|
|
1057
|
+
*
|
|
1058
|
+
* - **Route Handlers**: Process requests and return response data
|
|
1059
|
+
* - **Middleware**: Modify context or perform side effects
|
|
1060
|
+
* - **Hooks**: beforeHooks, afterHooks, beforeAll, afterAll
|
|
1061
|
+
* - **Error Handlers**: Handle errors with error parameter
|
|
1062
|
+
*
|
|
1063
|
+
* ## Return Values
|
|
1064
|
+
*
|
|
1065
|
+
* Handlers can return:
|
|
1066
|
+
* - **Response Data**: Objects, strings, numbers, etc. (automatically JSON serialized)
|
|
1067
|
+
* - **Promise**: Async operations that resolve to response data
|
|
1068
|
+
* - **Void**: No response body (useful for middleware)
|
|
1069
|
+
* - **Error**: Thrown errors are caught by error handlers
|
|
1070
|
+
*
|
|
1071
|
+
* @template T - Extends InternalHandlerCallbackGenerics for custom typing
|
|
1072
|
+
* @param ctx - The request context containing request, response, and state objects
|
|
1073
|
+
* @param error - Optional error object (only provided to error handlers)
|
|
1074
|
+
* @returns Response data, promise, or void
|
|
1075
|
+
*
|
|
1076
|
+
* @example
|
|
1077
|
+
* ```typescript
|
|
1078
|
+
* // Basic route handler
|
|
1079
|
+
* const userHandler: HandlerCallback = async (ctx) => {
|
|
1080
|
+
* const userId = ctx.request.params.id;
|
|
1081
|
+
* const user = await getUserById(userId);
|
|
1082
|
+
*
|
|
1083
|
+
* return { user, timestamp: new Date().toISOString() };
|
|
1084
|
+
* };
|
|
1085
|
+
*
|
|
1086
|
+
* // Typed route handler with custom state
|
|
1087
|
+
* interface UserContext extends InternalHandlerCallbackGenerics {
|
|
1088
|
+
* body: { name: string; email: string };
|
|
1089
|
+
* response: { id: string; name: string; email: string };
|
|
1090
|
+
* state: { user: User; permissions: string[] };
|
|
1091
|
+
* }
|
|
154
1092
|
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
1093
|
+
* const createUser: HandlerCallback<UserContext> = async (ctx) => {
|
|
1094
|
+
* const { name, email } = ctx.request.body; // Fully typed!
|
|
1095
|
+
* const { user, permissions } = ctx.state; // Fully typed!
|
|
158
1096
|
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
1097
|
+
* if (!permissions.includes('create')) {
|
|
1098
|
+
* throw new Error('Insufficient permissions');
|
|
1099
|
+
* }
|
|
1100
|
+
*
|
|
1101
|
+
* const newUser = await createUserInDatabase({ name, email });
|
|
1102
|
+
*
|
|
1103
|
+
* return { id: newUser.id, name: newUser.name, email: newUser.email };
|
|
1104
|
+
* };
|
|
1105
|
+
*
|
|
1106
|
+
* // Middleware that doesn't return data
|
|
1107
|
+
* const authMiddleware: HandlerCallback = async (ctx) => {
|
|
1108
|
+
* const token = ctx.request.headers.authorization;
|
|
1109
|
+
* const user = await validateToken(token);
|
|
1110
|
+
*
|
|
1111
|
+
* ctx.state.user = user;
|
|
1112
|
+
* ctx.state.isAuthenticated = true;
|
|
1113
|
+
*
|
|
1114
|
+
* // No return value needed for middleware
|
|
1115
|
+
* };
|
|
1116
|
+
*
|
|
1117
|
+
* // Error handler
|
|
1118
|
+
* const errorHandler: HandlerCallback = async (ctx, error) => {
|
|
1119
|
+
* console.error('Error occurred:', error);
|
|
1120
|
+
*
|
|
1121
|
+
* ctx.response.setStatusCode(500);
|
|
1122
|
+
* return {
|
|
1123
|
+
* error: 'Internal server error',
|
|
1124
|
+
* message: process.env.NODE_ENV === 'production' ? 'Something went wrong' : error.message
|
|
1125
|
+
* };
|
|
1126
|
+
* };
|
|
1127
|
+
* ```
|
|
1128
|
+
*
|
|
1129
|
+
* @see {@link Context} for context interface details
|
|
1130
|
+
* @see {@link InternalHandlerCallbackGenerics} for custom typing options
|
|
161
1131
|
*/
|
|
162
1132
|
export type HandlerCallback<T extends InternalHandlerCallbackGenerics = InternalHandlerCallbackGenerics> = (ctx: Context<T>, error?: unknown) => Promise<T["response"] | void> | T["response"] | void;
|
|
163
1133
|
export type InternalGlobalHookOptions = {
|
|
@@ -181,39 +1151,406 @@ export interface InternalHookRegistryImpl {
|
|
|
181
1151
|
_addOnError: (handler: HandlerCallback) => void;
|
|
182
1152
|
_addOnNotFound: (handler: HandlerCallback) => void;
|
|
183
1153
|
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Internal route registry implementation for managing route storage and lookup.
|
|
1156
|
+
*
|
|
1157
|
+
* This interface provides the core functionality for registering routes and
|
|
1158
|
+
* finding them efficiently at runtime. It maintains separate collections for
|
|
1159
|
+
* exact routes and parameterized routes for optimal performance.
|
|
1160
|
+
*
|
|
1161
|
+
* ## Route Types
|
|
1162
|
+
*
|
|
1163
|
+
* - **Exact Routes**: Direct string matches (e.g., "/api/users")
|
|
1164
|
+
* - **Parameterized Routes**: Dynamic routes with parameters (e.g., "/users/:id")
|
|
1165
|
+
*
|
|
1166
|
+
* ## Performance Characteristics
|
|
1167
|
+
*
|
|
1168
|
+
* - **Exact Routes**: O(1) lookup time using Map
|
|
1169
|
+
* - **Parameterized Routes**: O(n) lookup time with pre-compiled regex patterns
|
|
1170
|
+
*
|
|
1171
|
+
* @example
|
|
1172
|
+
* ```typescript
|
|
1173
|
+
* // This interface is used internally by YinzerFlow
|
|
1174
|
+
* // Users typically don't interact with it directly
|
|
1175
|
+
*
|
|
1176
|
+
* // However, if you're extending the framework:
|
|
1177
|
+
* class CustomRouteRegistry implements InternalRouteRegistryImpl {
|
|
1178
|
+
* readonly _exactRoutes = new Map();
|
|
1179
|
+
* readonly _parameterizedRoutes = new Map();
|
|
1180
|
+
*
|
|
1181
|
+
* _register(route: InternalRouteRegistry) {
|
|
1182
|
+
* // Custom registration logic
|
|
1183
|
+
* }
|
|
1184
|
+
*
|
|
1185
|
+
* _findRoute(method: InternalHttpMethod, path: string) {
|
|
1186
|
+
* // Custom route finding logic
|
|
1187
|
+
* return undefined;
|
|
1188
|
+
* }
|
|
1189
|
+
* }
|
|
1190
|
+
* ```
|
|
1191
|
+
*
|
|
1192
|
+
* @see {@link InternalRouteRegistry} for individual route structure
|
|
1193
|
+
* @see {@link InternalPreCompiledRoute} for parameterized route details
|
|
1194
|
+
* @see {@link InternalHttpMethod} for available HTTP methods
|
|
1195
|
+
*/
|
|
184
1196
|
export interface InternalRouteRegistryImpl {
|
|
1197
|
+
/**
|
|
1198
|
+
* Map of exact route matches organized by HTTP method and path.
|
|
1199
|
+
*
|
|
1200
|
+
* Provides O(1) lookup for routes that have no dynamic parameters.
|
|
1201
|
+
*
|
|
1202
|
+
* @example
|
|
1203
|
+
* ```typescript
|
|
1204
|
+
* // Structure: Map<Method, Map<Path, Route>>
|
|
1205
|
+
* _exactRoutes = new Map([
|
|
1206
|
+
* ['GET', new Map([
|
|
1207
|
+
* ['/api/users', userListRoute],
|
|
1208
|
+
* ['/api/posts', postListRoute]
|
|
1209
|
+
* ])],
|
|
1210
|
+
* ['POST', new Map([
|
|
1211
|
+
* ['/api/users', createUserRoute]
|
|
1212
|
+
* ])]
|
|
1213
|
+
* ]);
|
|
1214
|
+
* ```
|
|
1215
|
+
*/
|
|
185
1216
|
readonly _exactRoutes: Map<InternalHttpMethod, Map<string, InternalRouteRegistry>>;
|
|
1217
|
+
/**
|
|
1218
|
+
* Array of parameterized routes with pre-compiled regex patterns.
|
|
1219
|
+
*
|
|
1220
|
+
* These routes contain dynamic parameters and require regex matching
|
|
1221
|
+
* at runtime. They're pre-compiled for performance.
|
|
1222
|
+
*
|
|
1223
|
+
* @example
|
|
1224
|
+
* ```typescript
|
|
1225
|
+
* // Structure: Array of pre-compiled routes
|
|
1226
|
+
* _parameterizedRoutes = [
|
|
1227
|
+
* {
|
|
1228
|
+
* pattern: /^\/users\/([^\/]+)$/,
|
|
1229
|
+
* paramNames: ['id'],
|
|
1230
|
+
* path: '/users/:id',
|
|
1231
|
+
* method: 'GET'
|
|
1232
|
+
* }
|
|
1233
|
+
* ];
|
|
1234
|
+
* ```
|
|
1235
|
+
*/
|
|
186
1236
|
readonly _parameterizedRoutes: Map<InternalHttpMethod, Array<InternalPreCompiledRoute>>;
|
|
1237
|
+
/**
|
|
1238
|
+
* Registers a new route in the appropriate collection.
|
|
1239
|
+
*
|
|
1240
|
+
* Routes are automatically categorized as exact or parameterized
|
|
1241
|
+
* based on whether they contain dynamic parameters.
|
|
1242
|
+
*
|
|
1243
|
+
* @param route - The route to register
|
|
1244
|
+
*
|
|
1245
|
+
* @example
|
|
1246
|
+
* ```typescript
|
|
1247
|
+
* _register({
|
|
1248
|
+
* path: '/api/users/:id',
|
|
1249
|
+
* method: 'GET',
|
|
1250
|
+
* handler: getUserHandler,
|
|
1251
|
+
* options: { beforeHooks: [authMiddleware] },
|
|
1252
|
+
* params: {}
|
|
1253
|
+
* });
|
|
1254
|
+
* ```
|
|
1255
|
+
*/
|
|
187
1256
|
_register: (route: InternalRouteRegistry) => void;
|
|
1257
|
+
/**
|
|
1258
|
+
* Finds a matching route for the given HTTP method and path.
|
|
1259
|
+
*
|
|
1260
|
+
* Searches exact routes first (O(1)), then parameterized routes (O(n))
|
|
1261
|
+
* until a match is found.
|
|
1262
|
+
*
|
|
1263
|
+
* @param method - The HTTP method to search for
|
|
1264
|
+
* @param path - The request path to match
|
|
1265
|
+
* @returns The matching route or undefined if no match found
|
|
1266
|
+
*
|
|
1267
|
+
* @example
|
|
1268
|
+
* ```typescript
|
|
1269
|
+
* const route = _findRoute('GET', '/api/users/123');
|
|
1270
|
+
* if (route) {
|
|
1271
|
+
* // Extract parameters from path
|
|
1272
|
+
* const params = extractParams(route, '/api/users/123');
|
|
1273
|
+
* // Execute route handler
|
|
1274
|
+
* await route.handler(context);
|
|
1275
|
+
* }
|
|
1276
|
+
* ```
|
|
1277
|
+
*/
|
|
188
1278
|
_findRoute: (method: InternalHttpMethod, path: string) => InternalRouteRegistry | undefined;
|
|
189
1279
|
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Configuration options for route registration.
|
|
1282
|
+
*
|
|
1283
|
+
* Defines hooks that will be executed before and after the route handler.
|
|
1284
|
+
* Both hooks are optional and can be used independently.
|
|
1285
|
+
*
|
|
1286
|
+
* @example
|
|
1287
|
+
* ```typescript
|
|
1288
|
+
* // Route with authentication middleware
|
|
1289
|
+
* const authRoute: InternalRouteRegistryOptions = {
|
|
1290
|
+
* beforeHooks: [
|
|
1291
|
+
* async (ctx) => {
|
|
1292
|
+
* const token = ctx.request.headers.authorization;
|
|
1293
|
+
* if (!token) throw new Error('Unauthorized');
|
|
1294
|
+
* ctx.state.user = await validateToken(token);
|
|
1295
|
+
* }
|
|
1296
|
+
* ]
|
|
1297
|
+
* };
|
|
1298
|
+
*
|
|
1299
|
+
* // Route with response logging
|
|
1300
|
+
* const loggingRoute: InternalRouteRegistryOptions = {
|
|
1301
|
+
* afterHooks: [
|
|
1302
|
+
* async (ctx, result) => {
|
|
1303
|
+
* console.log(`Response: ${JSON.stringify(result)}`);
|
|
1304
|
+
* }
|
|
1305
|
+
* ]
|
|
1306
|
+
* };
|
|
1307
|
+
*
|
|
1308
|
+
* // Route with both hooks
|
|
1309
|
+
* const fullRoute: InternalRouteRegistryOptions = {
|
|
1310
|
+
* beforeHooks: [authMiddleware],
|
|
1311
|
+
* afterHooks: [loggingMiddleware, metricsMiddleware]
|
|
1312
|
+
* };
|
|
1313
|
+
* ```
|
|
1314
|
+
*
|
|
1315
|
+
* @see {@link HandlerCallback} for hook function signature
|
|
1316
|
+
*/
|
|
190
1317
|
export interface InternalRouteRegistryOptions {
|
|
191
|
-
|
|
192
|
-
|
|
1318
|
+
/**
|
|
1319
|
+
* Hooks to execute before the route handler.
|
|
1320
|
+
*
|
|
1321
|
+
* These hooks run in order and can modify the context or throw errors.
|
|
1322
|
+
* If any hook throws an error, the route handler is not executed.
|
|
1323
|
+
*
|
|
1324
|
+
* @example
|
|
1325
|
+
* ```typescript
|
|
1326
|
+
* beforeHooks: [
|
|
1327
|
+
* async (ctx) => {
|
|
1328
|
+
* // Authentication
|
|
1329
|
+
* ctx.state.user = await authenticate(ctx.request.headers.authorization);
|
|
1330
|
+
* },
|
|
1331
|
+
* async (ctx) => {
|
|
1332
|
+
* // Rate limiting
|
|
1333
|
+
* await checkRateLimit(ctx.request.ipAddress);
|
|
1334
|
+
* },
|
|
1335
|
+
* async (ctx) => {
|
|
1336
|
+
* // Request validation
|
|
1337
|
+
* validateRequest(ctx.request.body);
|
|
1338
|
+
* }
|
|
1339
|
+
* ]
|
|
1340
|
+
* ```
|
|
1341
|
+
*/
|
|
1342
|
+
beforeHooks?: Array<HandlerCallback>;
|
|
1343
|
+
/**
|
|
1344
|
+
* Hooks to execute after the route handler.
|
|
1345
|
+
*
|
|
1346
|
+
* These hooks run in reverse order and can modify the response or context.
|
|
1347
|
+
* They always execute, even if the route handler throws an error.
|
|
1348
|
+
*
|
|
1349
|
+
* @example
|
|
1350
|
+
* ```typescript
|
|
1351
|
+
* afterHooks: [
|
|
1352
|
+
* async (ctx, result) => {
|
|
1353
|
+
* // Response logging
|
|
1354
|
+
* console.log(`Response: ${JSON.stringify(result)}`);
|
|
1355
|
+
* },
|
|
1356
|
+
* async (ctx) => {
|
|
1357
|
+
* // Add response headers
|
|
1358
|
+
* ctx.response.addHeaders({
|
|
1359
|
+
* 'X-Processing-Time': `${Date.now() - ctx.state.startTime}ms`
|
|
1360
|
+
* });
|
|
1361
|
+
* }
|
|
1362
|
+
* ]
|
|
1363
|
+
* ```
|
|
1364
|
+
*/
|
|
1365
|
+
afterHooks?: Array<HandlerCallback>;
|
|
193
1366
|
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Individual route registration with all necessary metadata.
|
|
1369
|
+
*
|
|
1370
|
+
* Each route contains the path, method, handler, and configuration options.
|
|
1371
|
+
* Routes can be either exact matches or parameterized with dynamic segments.
|
|
1372
|
+
*
|
|
1373
|
+
* @example
|
|
1374
|
+
* ```typescript
|
|
1375
|
+
* const userRoute: InternalRouteRegistry = {
|
|
1376
|
+
* path: '/api/users/:id',
|
|
1377
|
+
* method: 'GET',
|
|
1378
|
+
* handler: async (ctx) => {
|
|
1379
|
+
* const { id } = ctx.request.params;
|
|
1380
|
+
* const user = await getUserById(id);
|
|
1381
|
+
* return user;
|
|
1382
|
+
* },
|
|
1383
|
+
* options: {
|
|
1384
|
+
* beforeHooks: [authMiddleware],
|
|
1385
|
+
* afterHooks: [loggingMiddleware]
|
|
1386
|
+
* },
|
|
1387
|
+
* params: { id: ':id' }
|
|
1388
|
+
* };
|
|
1389
|
+
* ```
|
|
1390
|
+
*
|
|
1391
|
+
* @see {@link InternalRouteRegistryOptions} for hook configuration
|
|
1392
|
+
* @see {@link HandlerCallback} for handler function signature
|
|
1393
|
+
* @see {@link InternalHttpMethod} for available HTTP methods
|
|
1394
|
+
*/
|
|
194
1395
|
export interface InternalRouteRegistry {
|
|
1396
|
+
/**
|
|
1397
|
+
* Optional path prefix for route groups.
|
|
1398
|
+
*
|
|
1399
|
+
* When routes are registered in groups, this contains the group prefix.
|
|
1400
|
+
* The full route path is constructed by combining prefix + path.
|
|
1401
|
+
*
|
|
1402
|
+
* @example
|
|
1403
|
+
* ```typescript
|
|
1404
|
+
* // For a route in group '/api/v1'
|
|
1405
|
+
* prefix: '/api/v1'
|
|
1406
|
+
* path: '/users'
|
|
1407
|
+
* // Full route: /api/v1/users
|
|
1408
|
+
* ```
|
|
1409
|
+
*/
|
|
195
1410
|
prefix?: string;
|
|
1411
|
+
/**
|
|
1412
|
+
* The route path pattern.
|
|
1413
|
+
*
|
|
1414
|
+
* Can contain static segments and dynamic parameters (e.g., ':id').
|
|
1415
|
+
*
|
|
1416
|
+
* @example
|
|
1417
|
+
* ```typescript
|
|
1418
|
+
* path: '/users/:id/posts/:postId'
|
|
1419
|
+
* // Matches: /users/123/posts/456
|
|
1420
|
+
* // Extracts: { id: '123', postId: '456' }
|
|
1421
|
+
* ```
|
|
1422
|
+
*/
|
|
196
1423
|
path: string;
|
|
1424
|
+
/**
|
|
1425
|
+
* HTTP method this route responds to.
|
|
1426
|
+
*
|
|
1427
|
+
* @example
|
|
1428
|
+
* ```typescript
|
|
1429
|
+
* method: 'POST' // Only responds to POST requests
|
|
1430
|
+
* ```
|
|
1431
|
+
*/
|
|
197
1432
|
method: InternalHttpMethod;
|
|
1433
|
+
/**
|
|
1434
|
+
* The function that handles requests to this route.
|
|
1435
|
+
*
|
|
1436
|
+
* Receives the request context and returns the response data.
|
|
1437
|
+
*
|
|
1438
|
+
* @example
|
|
1439
|
+
* ```typescript
|
|
1440
|
+
* handler: async (ctx) => {
|
|
1441
|
+
* const { id } = ctx.request.params;
|
|
1442
|
+
* const user = await getUserById(id);
|
|
1443
|
+
* return { user, timestamp: new Date() };
|
|
1444
|
+
* }
|
|
1445
|
+
* ```
|
|
1446
|
+
*/
|
|
198
1447
|
handler: HandlerCallback;
|
|
1448
|
+
/**
|
|
1449
|
+
* Configuration options including hooks and middleware.
|
|
1450
|
+
*
|
|
1451
|
+
* @example
|
|
1452
|
+
* ```typescript
|
|
1453
|
+
* options: {
|
|
1454
|
+
* beforeHooks: [authMiddleware, validationMiddleware],
|
|
1455
|
+
* afterHooks: [loggingMiddleware]
|
|
1456
|
+
* }
|
|
1457
|
+
* ```
|
|
1458
|
+
*/
|
|
199
1459
|
options: InternalRouteRegistryOptions;
|
|
1460
|
+
/**
|
|
1461
|
+
* Parameter placeholders extracted from the path.
|
|
1462
|
+
*
|
|
1463
|
+
* Maps parameter names to their placeholder values (e.g., ':id').
|
|
1464
|
+
*
|
|
1465
|
+
* @example
|
|
1466
|
+
* ```typescript
|
|
1467
|
+
* // For path '/users/:id/posts/:postId'
|
|
1468
|
+
* params: {
|
|
1469
|
+
* id: ':id',
|
|
1470
|
+
* postId: ':postId'
|
|
1471
|
+
* }
|
|
1472
|
+
* ```
|
|
1473
|
+
*/
|
|
200
1474
|
params: Record<string, string>;
|
|
201
1475
|
}
|
|
202
1476
|
/**
|
|
203
|
-
* Pre-compiled route with regex pattern for efficient runtime matching
|
|
1477
|
+
* Pre-compiled route with regex pattern for efficient runtime matching.
|
|
204
1478
|
*
|
|
205
1479
|
* We compile route patterns into regexes at registration time (server startup)
|
|
206
1480
|
* rather than at request time for performance reasons:
|
|
207
1481
|
* - Registration: O(1) one-time cost per route
|
|
208
1482
|
* - Runtime: O(1) for exact routes, O(n) for parameterized routes with pre-compiled regex
|
|
209
1483
|
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
1484
|
+
* @example
|
|
1485
|
+
* ```typescript
|
|
1486
|
+
* // Route: "/users/:id/posts/:postId"
|
|
1487
|
+
* const compiledRoute: InternalPreCompiledRoute = {
|
|
1488
|
+
* pattern: /^\/users\/([^\/]+)\/posts\/([^\/]+)$/,
|
|
1489
|
+
* paramNames: ["id", "postId"],
|
|
1490
|
+
* isParameterized: true,
|
|
1491
|
+
* path: "/users/:id/posts/:postId",
|
|
1492
|
+
* method: "GET",
|
|
1493
|
+
* handler: getUserPostsHandler,
|
|
1494
|
+
* options: { beforeHooks: [authMiddleware] },
|
|
1495
|
+
* params: { id: ":id", postId: ":postId" }
|
|
1496
|
+
* };
|
|
1497
|
+
*
|
|
1498
|
+
* // Runtime matching
|
|
1499
|
+
* const match = "/users/123/posts/456".match(compiledRoute.pattern);
|
|
1500
|
+
* if (match) {
|
|
1501
|
+
* const params = {
|
|
1502
|
+
* [compiledRoute.paramNames[0]]: match[1], // id: "123"
|
|
1503
|
+
* [compiledRoute.paramNames[1]]: match[2] // postId: "456"
|
|
1504
|
+
* };
|
|
1505
|
+
* }
|
|
1506
|
+
* ```
|
|
1507
|
+
*
|
|
1508
|
+
* @see {@link InternalRouteRegistry} for base route structure
|
|
213
1509
|
*/
|
|
214
1510
|
export interface InternalPreCompiledRoute extends InternalRouteRegistry {
|
|
1511
|
+
/**
|
|
1512
|
+
* Pre-compiled regex pattern for matching this route.
|
|
1513
|
+
*
|
|
1514
|
+
* The regex is created once at registration time and reused
|
|
1515
|
+
* for all subsequent requests to this route.
|
|
1516
|
+
*
|
|
1517
|
+
* @example
|
|
1518
|
+
* ```typescript
|
|
1519
|
+
* // Path: "/users/:id"
|
|
1520
|
+
* pattern: /^\/users\/([^\/]+)$/
|
|
1521
|
+
*
|
|
1522
|
+
* // Path: "/posts/:category/:id"
|
|
1523
|
+
* pattern: /^\/posts\/([^\/]+)\/([^\/]+)$/
|
|
1524
|
+
* ```
|
|
1525
|
+
*/
|
|
215
1526
|
pattern: RegExp;
|
|
1527
|
+
/**
|
|
1528
|
+
* Names of parameters in the order they appear in the path.
|
|
1529
|
+
*
|
|
1530
|
+
* Used to map regex capture groups to parameter names.
|
|
1531
|
+
*
|
|
1532
|
+
* @example
|
|
1533
|
+
* ```typescript
|
|
1534
|
+
* // Path: "/users/:id/posts/:postId"
|
|
1535
|
+
* paramNames: ["id", "postId"]
|
|
1536
|
+
*
|
|
1537
|
+
* // Regex: /^\/users\/([^\/]+)\/posts\/([^\/]+)$/
|
|
1538
|
+
* // match[1] = "123" -> params.id = "123"
|
|
1539
|
+
* // match[2] = "456" -> params.postId = "456"
|
|
1540
|
+
* ```
|
|
1541
|
+
*/
|
|
216
1542
|
paramNames: Array<string>;
|
|
1543
|
+
/**
|
|
1544
|
+
* Flag indicating this route has dynamic parameters.
|
|
1545
|
+
*
|
|
1546
|
+
* Always true for InternalPreCompiledRoute since these are
|
|
1547
|
+
* specifically for parameterized routes.
|
|
1548
|
+
*
|
|
1549
|
+
* @example
|
|
1550
|
+
* ```typescript
|
|
1551
|
+
* isParameterized: true // Always true for this interface
|
|
1552
|
+
* ```
|
|
1553
|
+
*/
|
|
217
1554
|
isParameterized: boolean;
|
|
218
1555
|
}
|
|
219
1556
|
declare const logLevels: {
|
|
@@ -585,18 +1922,279 @@ export interface InternalServerConfiguration {
|
|
|
585
1922
|
*/
|
|
586
1923
|
autoGracefulShutdown: boolean;
|
|
587
1924
|
}
|
|
588
|
-
export type
|
|
589
|
-
export
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
1925
|
+
export type HttpMethodHandlers = Record<Lowercase<keyof typeof httpMethod>, InternalSetupMethod>;
|
|
1926
|
+
export type RouteGroupMethod = (prefix: string, callback: (group: InternalGroupApp) => void, options?: InternalRouteRegistryOptions) => InternalGroupApp;
|
|
1927
|
+
export interface InternalGroupApp extends HttpMethodHandlers {
|
|
1928
|
+
readonly group: RouteGroupMethod;
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Main setup interface for configuring YinzerFlow routes, hooks, and middleware.
|
|
1932
|
+
*
|
|
1933
|
+
* The Setup interface provides methods for:
|
|
1934
|
+
* - **Route Registration**: HTTP method handlers (GET, POST, PUT, etc.)
|
|
1935
|
+
* - **Route Grouping**: Organized route prefixes with shared hooks
|
|
1936
|
+
* - **Global Hooks**: beforeAll/afterAll hooks that run for all routes
|
|
1937
|
+
* - **Error Handling**: Custom error and not-found handlers
|
|
1938
|
+
*
|
|
1939
|
+
* @example
|
|
1940
|
+
* ```typescript
|
|
1941
|
+
* import { YinzerFlow } from 'yinzerflow';
|
|
1942
|
+
*
|
|
1943
|
+
* const app = new YinzerFlow({ port: 3000 });
|
|
1944
|
+
*
|
|
1945
|
+
* // Register routes
|
|
1946
|
+
* app.get('/api/users', async (ctx) => {
|
|
1947
|
+
* return { users: ['John', 'Jane'] };
|
|
1948
|
+
* });
|
|
1949
|
+
*
|
|
1950
|
+
* app.post('/api/users', async (ctx) => {
|
|
1951
|
+
* const userData = ctx.request.body;
|
|
1952
|
+
* return { message: 'User created', data: userData };
|
|
1953
|
+
* });
|
|
1954
|
+
*
|
|
1955
|
+
* // Set up global hooks
|
|
1956
|
+
* app.beforeAll([
|
|
1957
|
+
* async (ctx) => {
|
|
1958
|
+
* ctx.state.requestId = generateRequestId();
|
|
1959
|
+
* ctx.state.timestamp = Date.now();
|
|
1960
|
+
* }
|
|
1961
|
+
* ]);
|
|
1962
|
+
*
|
|
1963
|
+
* app.afterAll([
|
|
1964
|
+
* async (ctx, result) => {
|
|
1965
|
+
* ctx.response.addHeaders({
|
|
1966
|
+
* 'X-Request-ID': ctx.state.requestId,
|
|
1967
|
+
* 'X-Processing-Time': `${Date.now() - ctx.state.timestamp}ms`
|
|
1968
|
+
* });
|
|
1969
|
+
* }
|
|
1970
|
+
* ]);
|
|
1971
|
+
*
|
|
1972
|
+
* // Create route groups
|
|
1973
|
+
* app.group('/api/v1', (api) => {
|
|
1974
|
+
* api.group('/admin', (admin) => {
|
|
1975
|
+
* admin.get('/users', async (ctx) => {
|
|
1976
|
+
* return { adminUsers: ['Admin1', 'Admin2'] };
|
|
1977
|
+
* });
|
|
1978
|
+
* });
|
|
1979
|
+
* });
|
|
1980
|
+
*
|
|
1981
|
+
* // Custom error handlers
|
|
1982
|
+
* app.onError(async (ctx, error) => {
|
|
1983
|
+
* ctx.response.setStatusCode(500);
|
|
1984
|
+
* return { error: 'Internal server error', message: error.message };
|
|
1985
|
+
* });
|
|
1986
|
+
*
|
|
1987
|
+
* app.onNotFound(async (ctx) => {
|
|
1988
|
+
* ctx.response.setStatusCode(404);
|
|
1989
|
+
* return { error: 'Not found', path: ctx.request.url };
|
|
1990
|
+
* });
|
|
1991
|
+
* ```
|
|
1992
|
+
*
|
|
1993
|
+
* @see {@link HttpMethodHandlers} for HTTP method registration methods
|
|
1994
|
+
* @see {@link RouteGroupMethod} for route group creation method
|
|
1995
|
+
* @see {@link InternalGlobalHookOptions} for global hook configuration options
|
|
1996
|
+
* @see {@link HandlerCallback} for route handler function signature
|
|
1997
|
+
*/
|
|
1998
|
+
export interface Setup extends HttpMethodHandlers {
|
|
1999
|
+
/**
|
|
2000
|
+
* Creates a route group with a shared prefix and optional shared hooks.
|
|
2001
|
+
*
|
|
2002
|
+
* Route groups allow you to organize related routes under a common path
|
|
2003
|
+
* prefix and apply shared hooks to all routes within the group.
|
|
2004
|
+
*
|
|
2005
|
+
* @param prefix - The URL prefix for all routes in this group (e.g., '/api/v1')
|
|
2006
|
+
* @param callback - Function that receives the group instance for registering routes
|
|
2007
|
+
* @param options - Optional shared hooks and configuration for the group
|
|
2008
|
+
*
|
|
2009
|
+
* @example
|
|
2010
|
+
* ```typescript
|
|
2011
|
+
* // Basic route group
|
|
2012
|
+
* app.group('/api', (api) => {
|
|
2013
|
+
* api.get('/users', () => ({ users: [] }));
|
|
2014
|
+
* api.post('/users', () => ({ created: true }));
|
|
2015
|
+
* });
|
|
2016
|
+
*
|
|
2017
|
+
* // Route group with shared hooks
|
|
2018
|
+
* app.group('/api/v1', (api) => {
|
|
2019
|
+
* api.get('/users', () => ({ users: [] }));
|
|
2020
|
+
* api.post('/users', () => ({ created: true }));
|
|
2021
|
+
* }, {
|
|
2022
|
+
* beforeHooks: [
|
|
2023
|
+
* async (ctx) => {
|
|
2024
|
+
* ctx.state.apiVersion = 'v1';
|
|
2025
|
+
* ctx.state.requiresAuth = true;
|
|
2026
|
+
* }
|
|
2027
|
+
* ]
|
|
2028
|
+
* });
|
|
2029
|
+
*
|
|
2030
|
+
* // Nested route groups
|
|
2031
|
+
* app.group('/api/v1', (api) => {
|
|
2032
|
+
* api.group('/admin', (admin) => {
|
|
2033
|
+
* admin.get('/users', () => ({ adminUsers: [] }));
|
|
2034
|
+
* admin.get('/stats', () => ({ stats: {} }));
|
|
2035
|
+
* });
|
|
2036
|
+
* });
|
|
2037
|
+
* ```
|
|
2038
|
+
*
|
|
2039
|
+
* @see {@link RouteGroupMethod} for method signature details
|
|
2040
|
+
* @see {@link InternalGroupApp} for group instance interface
|
|
2041
|
+
*/
|
|
2042
|
+
group: RouteGroupMethod;
|
|
2043
|
+
/**
|
|
2044
|
+
* Registers global hooks that run before all route handlers.
|
|
2045
|
+
*
|
|
2046
|
+
* These hooks execute for every request before any route-specific hooks
|
|
2047
|
+
* or the route handler itself. They're perfect for setting up global
|
|
2048
|
+
* state, authentication, logging, or request preprocessing.
|
|
2049
|
+
*
|
|
2050
|
+
* @param handlers - Array of hook functions to execute
|
|
2051
|
+
* @param options - Optional configuration for hook execution scope
|
|
2052
|
+
*
|
|
2053
|
+
* @example
|
|
2054
|
+
* ```typescript
|
|
2055
|
+
* // Global authentication hook
|
|
2056
|
+
* app.beforeAll([
|
|
2057
|
+
* async (ctx) => {
|
|
2058
|
+
* const token = ctx.request.headers.authorization;
|
|
2059
|
+
* if (token) {
|
|
2060
|
+
* const user = await validateToken(token);
|
|
2061
|
+
* ctx.state.user = user;
|
|
2062
|
+
* ctx.state.isAuthenticated = true;
|
|
2063
|
+
* }
|
|
2064
|
+
* }
|
|
2065
|
+
* ]);
|
|
2066
|
+
*
|
|
2067
|
+
* // Global logging hook
|
|
2068
|
+
* app.beforeAll([
|
|
2069
|
+
* async (ctx) => {
|
|
2070
|
+
* ctx.state.requestId = generateRequestId();
|
|
2071
|
+
* ctx.state.startTime = Date.now();
|
|
2072
|
+
*
|
|
2073
|
+
* console.log(`Request ${ctx.state.requestId} to ${ctx.request.url}`);
|
|
2074
|
+
* }
|
|
2075
|
+
* ], {
|
|
2076
|
+
* routesToExclude: ['/health', '/metrics'] // Skip logging for health checks
|
|
2077
|
+
* });
|
|
2078
|
+
* ```
|
|
2079
|
+
*
|
|
2080
|
+
* @see {@link InternalGlobalHookOptions} for hook configuration options
|
|
2081
|
+
* @see {@link HandlerCallback} for hook function signature
|
|
2082
|
+
*/
|
|
597
2083
|
beforeAll: (handlers: Array<HandlerCallback>, options?: InternalGlobalHookOptions) => void;
|
|
2084
|
+
/**
|
|
2085
|
+
* Registers global hooks that run after all route handlers.
|
|
2086
|
+
*
|
|
2087
|
+
* These hooks execute for every request after the route handler completes
|
|
2088
|
+
* and after any route-specific after hooks. They're perfect for response
|
|
2089
|
+
* modification, logging, cleanup, or adding response headers.
|
|
2090
|
+
*
|
|
2091
|
+
* @param handlers - Array of hook functions to execute
|
|
2092
|
+
* @param options - Optional configuration for hook execution scope
|
|
2093
|
+
*
|
|
2094
|
+
* @example
|
|
2095
|
+
* ```typescript
|
|
2096
|
+
* // Global response modification hook
|
|
2097
|
+
* app.afterAll([
|
|
2098
|
+
* async (ctx, result) => {
|
|
2099
|
+
* // Add response headers based on state
|
|
2100
|
+
* if (ctx.state.requestId) {
|
|
2101
|
+
* ctx.response.addHeaders({
|
|
2102
|
+
* 'X-Request-ID': ctx.state.requestId,
|
|
2103
|
+
* 'X-Processing-Time': `${Date.now() - ctx.state.startTime}ms`
|
|
2104
|
+
* });
|
|
2105
|
+
* }
|
|
2106
|
+
*
|
|
2107
|
+
* // Log response
|
|
2108
|
+
* console.log(`Request ${ctx.state.requestId} completed with status ${ctx.response.statusCode}`);
|
|
2109
|
+
* }
|
|
2110
|
+
* ]);
|
|
2111
|
+
*
|
|
2112
|
+
* // Global CORS hook
|
|
2113
|
+
* app.afterAll([
|
|
2114
|
+
* async (ctx) => {
|
|
2115
|
+
* ctx.response.addHeaders({
|
|
2116
|
+
* 'Access-Control-Allow-Origin': '*',
|
|
2117
|
+
* 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
2118
|
+
* 'Access-Control-Allow-Headers': 'Content-Type, Authorization'
|
|
2119
|
+
* });
|
|
2120
|
+
* }
|
|
2121
|
+
* ]);
|
|
2122
|
+
* ```
|
|
2123
|
+
*
|
|
2124
|
+
* @see {@link InternalGlobalHookOptions} for hook configuration options
|
|
2125
|
+
* @see {@link HandlerCallback} for hook function signature
|
|
2126
|
+
*/
|
|
598
2127
|
afterAll: (handlers: Array<HandlerCallback>, options?: InternalGlobalHookOptions) => void;
|
|
2128
|
+
/**
|
|
2129
|
+
* Registers a custom error handler for all routes.
|
|
2130
|
+
*
|
|
2131
|
+
* This handler is called when any route throws an error or returns
|
|
2132
|
+
* a rejected promise. It receives the context and the error object,
|
|
2133
|
+
* allowing you to customize error responses and logging.
|
|
2134
|
+
*
|
|
2135
|
+
* @param handler - Function to handle errors
|
|
2136
|
+
*
|
|
2137
|
+
* @example
|
|
2138
|
+
* ```typescript
|
|
2139
|
+
* // Custom error handler
|
|
2140
|
+
* app.onError(async (ctx, error) => {
|
|
2141
|
+
* // Log the error
|
|
2142
|
+
* console.error(`Error in ${ctx.request.method} ${ctx.request.url}:`, error);
|
|
2143
|
+
*
|
|
2144
|
+
* // Set appropriate status code
|
|
2145
|
+
* if (error.name === 'ValidationError') {
|
|
2146
|
+
* ctx.response.setStatusCode(400);
|
|
2147
|
+
* return { error: 'Validation failed', details: error.message };
|
|
2148
|
+
* }
|
|
2149
|
+
*
|
|
2150
|
+
* if (error.name === 'UnauthorizedError') {
|
|
2151
|
+
* ctx.response.setStatusCode(401);
|
|
2152
|
+
* return { error: 'Unauthorized', message: error.message };
|
|
2153
|
+
* }
|
|
2154
|
+
*
|
|
2155
|
+
* // Default error response
|
|
2156
|
+
* ctx.response.setStatusCode(500);
|
|
2157
|
+
* return {
|
|
2158
|
+
* error: 'Internal server error',
|
|
2159
|
+
* message: process.env.NODE_ENV === 'production' ? 'Something went wrong' : error.message
|
|
2160
|
+
* };
|
|
2161
|
+
* });
|
|
2162
|
+
* ```
|
|
2163
|
+
*
|
|
2164
|
+
* @see {@link HandlerCallback} for error handler function signature
|
|
2165
|
+
*/
|
|
599
2166
|
onError: (handler: HandlerCallback) => void;
|
|
2167
|
+
/**
|
|
2168
|
+
* Registers a custom not-found handler for unmatched routes.
|
|
2169
|
+
*
|
|
2170
|
+
* This handler is called when a request doesn't match any registered
|
|
2171
|
+
* route. It receives the context and allows you to customize the
|
|
2172
|
+
* 404 response.
|
|
2173
|
+
*
|
|
2174
|
+
* @param handler - Function to handle not-found requests
|
|
2175
|
+
*
|
|
2176
|
+
* @example
|
|
2177
|
+
* ```typescript
|
|
2178
|
+
* // Custom not-found handler
|
|
2179
|
+
* app.onNotFound(async (ctx) => {
|
|
2180
|
+
* ctx.response.setStatusCode(404);
|
|
2181
|
+
*
|
|
2182
|
+
* // Return helpful error message
|
|
2183
|
+
* return {
|
|
2184
|
+
* error: 'Not found',
|
|
2185
|
+
* message: `The requested resource '${ctx.request.url}' was not found`,
|
|
2186
|
+
* availableEndpoints: [
|
|
2187
|
+
* '/api/users',
|
|
2188
|
+
* '/api/posts',
|
|
2189
|
+
* '/health'
|
|
2190
|
+
* ],
|
|
2191
|
+
* documentation: 'https://api.example.com/docs'
|
|
2192
|
+
* };
|
|
2193
|
+
* });
|
|
2194
|
+
* ```
|
|
2195
|
+
*
|
|
2196
|
+
* @see {@link HandlerCallback} for not-found handler function signature
|
|
2197
|
+
*/
|
|
600
2198
|
onNotFound: (handler: HandlerCallback) => void;
|
|
601
2199
|
}
|
|
602
2200
|
export type InternalSetupMethod = (path: string, handler: HandlerCallback<any>, options?: InternalRouteRegistryOptions) => void;
|
|
@@ -623,10 +2221,60 @@ declare class HookRegistryImpl implements InternalHookRegistryImpl {
|
|
|
623
2221
|
_addOnNotFound(handler: HandlerCallback): void;
|
|
624
2222
|
}
|
|
625
2223
|
/**
|
|
626
|
-
* User-facing configuration interface where all properties are optional
|
|
627
|
-
*
|
|
2224
|
+
* User-facing configuration interface where all properties are optional.
|
|
2225
|
+
*
|
|
2226
|
+
* Users only need to specify what they want to override from defaults.
|
|
2227
|
+
* This is created by making the complete internal ServerConfigurationShape
|
|
2228
|
+
* partially optional using DeepPartial.
|
|
2229
|
+
*
|
|
2230
|
+
* ## Configuration Options
|
|
2231
|
+
*
|
|
2232
|
+
* - **Server Settings**: Port, host, and network configuration
|
|
2233
|
+
* - **Logging**: Log levels and custom logger configuration
|
|
2234
|
+
* - **CORS**: Cross-origin resource sharing settings
|
|
2235
|
+
* - **Performance**: Request handling and timeout settings
|
|
2236
|
+
* - **Security**: Headers and security-related options
|
|
2237
|
+
*
|
|
2238
|
+
* @example
|
|
2239
|
+
* ```typescript
|
|
2240
|
+
* import { YinzerFlow } from 'yinzerflow';
|
|
2241
|
+
*
|
|
2242
|
+
* // Minimal configuration - only specify what you need
|
|
2243
|
+
* const app = new YinzerFlow({ port: 3000 });
|
|
2244
|
+
*
|
|
2245
|
+
* // Basic configuration with logging
|
|
2246
|
+
* const app = new YinzerFlow({
|
|
2247
|
+
* port: 8080,
|
|
2248
|
+
* logLevel: 'info',
|
|
2249
|
+
* networkLogs: true
|
|
2250
|
+
* });
|
|
2251
|
+
*
|
|
2252
|
+
* // Full configuration example
|
|
2253
|
+
* const app = new YinzerFlow({
|
|
2254
|
+
* port: 9000,
|
|
2255
|
+
* host: '0.0.0.0',
|
|
2256
|
+
* logLevel: 'debug',
|
|
2257
|
+
* networkLogs: true,
|
|
2258
|
+
* autoGracefulShutdown: true,
|
|
2259
|
+
* cors: {
|
|
2260
|
+
* enabled: true,
|
|
2261
|
+
* origin: ['https://example.com', 'https://app.example.com'],
|
|
2262
|
+
* methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
2263
|
+
* headers: ['Content-Type', 'Authorization'],
|
|
2264
|
+
* credentials: true
|
|
2265
|
+
* },
|
|
2266
|
+
* logger: {
|
|
2267
|
+
* info: (message, ...args) => console.log(`[APP] ${message}`, ...args),
|
|
2268
|
+
* warn: (message, ...args) => console.warn(`[APP] ${message}`, ...args),
|
|
2269
|
+
* error: (message, ...args) => console.error(`[APP] ${message}`, ...args),
|
|
2270
|
+
* debug: (message, ...args) => console.debug(`[APP] ${message}`, ...args)
|
|
2271
|
+
* }
|
|
2272
|
+
* });
|
|
2273
|
+
* ```
|
|
628
2274
|
*
|
|
629
|
-
*
|
|
2275
|
+
* @see {@link InternalServerConfiguration} for complete internal configuration
|
|
2276
|
+
* @see {@link DeepPartial} for how optional properties are created
|
|
2277
|
+
* @see {@link CorsConfiguration} for CORS configuration options
|
|
630
2278
|
*/
|
|
631
2279
|
export type ServerConfiguration = DeepPartial<InternalServerConfiguration>;
|
|
632
2280
|
declare class RouteRegistryImpl implements InternalRouteRegistryImpl {
|
|
@@ -651,7 +2299,7 @@ declare class SetupImpl implements InternalSetupImpl {
|
|
|
651
2299
|
patch(path: string, handler: HandlerCallback<any>, options?: InternalRouteRegistryOptions): void;
|
|
652
2300
|
delete(path: string, handler: HandlerCallback<any>, options?: InternalRouteRegistryOptions): void;
|
|
653
2301
|
options(path: string, handler: HandlerCallback<any>, options?: InternalRouteRegistryOptions): void;
|
|
654
|
-
group(prefix: string, callback: (group:
|
|
2302
|
+
group(prefix: string, callback: (group: InternalGroupApp) => void, options?: InternalRouteRegistryOptions): InternalGroupApp;
|
|
655
2303
|
beforeAll(handlers: Array<HandlerCallback<any>>, options?: InternalGlobalHookOptions): void;
|
|
656
2304
|
afterAll(handlers: Array<HandlerCallback<any>>, options?: InternalGlobalHookOptions): void;
|
|
657
2305
|
onError(handler: HandlerCallback<any>): void;
|