astro-routify 1.2.2 → 1.5.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.
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @Get decorator - auto-registers a GET route.
3
+ */
4
+ export declare const Get: (path: string) => (target: any, propertyKey: string, descriptor?: PropertyDescriptor) => void;
5
+ /**
6
+ * @Post decorator - auto-registers a POST route.
7
+ */
8
+ export declare const Post: (path: string) => (target: any, propertyKey: string, descriptor?: PropertyDescriptor) => void;
9
+ /**
10
+ * @Put decorator - auto-registers a PUT route.
11
+ */
12
+ export declare const Put: (path: string) => (target: any, propertyKey: string, descriptor?: PropertyDescriptor) => void;
13
+ /**
14
+ * @Delete decorator - auto-registers a DELETE route.
15
+ */
16
+ export declare const Delete: (path: string) => (target: any, propertyKey: string, descriptor?: PropertyDescriptor) => void;
17
+ /**
18
+ * @Patch decorator - auto-registers a PATCH route.
19
+ */
20
+ export declare const Patch: (path: string) => (target: any, propertyKey: string, descriptor?: PropertyDescriptor) => void;
21
+ /**
22
+ * @RouteGroup decorator - can be used on classes to auto-register a group.
23
+ * Note: This requires the class to have a static method or property that returns the routes,
24
+ * or it can be used in conjunction with method decorators.
25
+ *
26
+ * For now, we'll implement a simple version that can be used on a class
27
+ * if the class is intended to be a group.
28
+ */
29
+ export declare function Group(basePath: string): (constructor: Function) => void;
@@ -0,0 +1,49 @@
1
+ import { HttpMethod } from './HttpMethod';
2
+ import { defineRoute } from './defineRoute';
3
+ /**
4
+ * Decorator factory for HTTP methods.
5
+ */
6
+ function createMethodDecorator(method) {
7
+ return function (path) {
8
+ return function (target, propertyKey, descriptor) {
9
+ const handler = descriptor ? descriptor.value : target[propertyKey];
10
+ if (typeof handler === 'function') {
11
+ defineRoute(method, path, handler, true);
12
+ }
13
+ };
14
+ };
15
+ }
16
+ /**
17
+ * @Get decorator - auto-registers a GET route.
18
+ */
19
+ export const Get = createMethodDecorator(HttpMethod.GET);
20
+ /**
21
+ * @Post decorator - auto-registers a POST route.
22
+ */
23
+ export const Post = createMethodDecorator(HttpMethod.POST);
24
+ /**
25
+ * @Put decorator - auto-registers a PUT route.
26
+ */
27
+ export const Put = createMethodDecorator(HttpMethod.PUT);
28
+ /**
29
+ * @Delete decorator - auto-registers a DELETE route.
30
+ */
31
+ export const Delete = createMethodDecorator(HttpMethod.DELETE);
32
+ /**
33
+ * @Patch decorator - auto-registers a PATCH route.
34
+ */
35
+ export const Patch = createMethodDecorator(HttpMethod.PATCH);
36
+ /**
37
+ * @RouteGroup decorator - can be used on classes to auto-register a group.
38
+ * Note: This requires the class to have a static method or property that returns the routes,
39
+ * or it can be used in conjunction with method decorators.
40
+ *
41
+ * For now, we'll implement a simple version that can be used on a class
42
+ * if the class is intended to be a group.
43
+ */
44
+ export function Group(basePath) {
45
+ return function (constructor) {
46
+ // In a more advanced implementation, we could collect all methods
47
+ // and register them as a group. For now, we'll keep it simple.
48
+ };
49
+ }
@@ -1,5 +1,6 @@
1
+ import { HttpMethod } from './HttpMethod';
1
2
  import { type Route } from './defineRoute';
2
- import { Handler } from "./defineHandler";
3
+ import { Middleware } from "./defineHandler";
3
4
  /**
4
5
  * Represents a group of routes under a shared base path.
5
6
  *
@@ -12,8 +13,10 @@ import { Handler } from "./defineHandler";
12
13
  * .addPost('/', createUser);
13
14
  */
14
15
  export declare class RouteGroup {
16
+ readonly _routifyType = "group";
15
17
  private basePath;
16
18
  private routes;
19
+ private middlewares;
17
20
  /**
18
21
  * Creates a new route group with the specified base path.
19
22
  * Trailing slashes are automatically removed.
@@ -25,49 +28,61 @@ export declare class RouteGroup {
25
28
  * Returns the normalized base path used by the group.
26
29
  */
27
30
  getBasePath(): string;
31
+ /**
32
+ * Adds a middleware to all routes in this group.
33
+ *
34
+ * @param middleware - The middleware function to add
35
+ * @returns The current group instance
36
+ */
37
+ use(middleware: Middleware): this;
28
38
  /**
29
39
  * Registers a GET route under the group's base path.
30
40
  *
31
41
  * @param path - Path relative to the base path (e.g. "/:id")
32
- * @param handler - The handler function for this route
42
+ * @param handlers - Middleware(s) followed by a handler function
33
43
  */
34
- addGet(path: string, handler: Handler): this;
44
+ addGet(path: string, ...handlers: any[]): this;
35
45
  /**
36
46
  * Registers a POST route under the group's base path.
37
47
  *
38
48
  * @param path - Path relative to the base path
39
- * @param handler - The handler function for this route
49
+ * @param handlers - Middleware(s) followed by a handler function
40
50
  */
41
- addPost(path: string, handler: Handler): this;
51
+ addPost(path: string, ...handlers: any[]): this;
42
52
  /**
43
53
  * Registers a PUT route under the group's base path.
44
54
  *
45
55
  * @param path - Path relative to the base path
46
- * @param handler - The handler function for this route
56
+ * @param handlers - Middleware(s) followed by a handler function
47
57
  */
48
- addPut(path: string, handler: Handler): this;
58
+ addPut(path: string, ...handlers: any[]): this;
49
59
  /**
50
60
  * Registers a DELETE route under the group's base path.
51
61
  *
52
62
  * @param path - Path relative to the base path
53
- * @param handler - The handler function for this route
63
+ * @param handlers - Middleware(s) followed by a handler function
54
64
  */
55
- addDelete(path: string, handler: Handler): this;
65
+ addDelete(path: string, ...handlers: any[]): this;
56
66
  /**
57
67
  * Registers a PATCH route under the group's base path.
58
68
  *
59
69
  * @param path - Path relative to the base path
60
- * @param handler - The handler function for this route
70
+ * @param handlers - Middleware(s) followed by a handler function
61
71
  */
62
- addPatch(path: string, handler: Handler): this;
72
+ addPatch(path: string, ...handlers: any[]): this;
63
73
  /**
64
- * Internal method to register a route under the group with any HTTP method.
74
+ * Registers a route under the group's base path.
65
75
  *
66
76
  * @param method - HTTP verb
67
- * @param subPath - Route path relative to the base
68
- * @param handler - The handler function for this route
77
+ * @param path - Path relative to the base path
78
+ * @param args - Middleware(s) and handler
79
+ * @returns The current group instance
80
+ */
81
+ add(method: HttpMethod, path: string, ...args: any[]): this;
82
+ /**
83
+ * Registers the group and all its current routes to the global registry.
69
84
  */
70
- private add;
85
+ register(): this;
71
86
  /**
72
87
  * Returns all the registered routes in the group.
73
88
  */
@@ -78,10 +93,11 @@ export declare class RouteGroup {
78
93
  *
79
94
  * @param basePath - The base path prefix for all routes
80
95
  * @param configure - Optional callback to configure the group inline
96
+ * @param autoRegister - If true, registers the group to the global registry
81
97
  *
82
98
  * @example
83
99
  * const users = defineGroup('/users', (group) => {
84
100
  * group.addGet('/:id', handler);
85
- * });
101
+ * }, true);
86
102
  */
87
- export declare function defineGroup(basePath: string, configure?: (group: RouteGroup) => void): RouteGroup;
103
+ export declare function defineGroup(basePath: string, configure?: (group: RouteGroup) => void, autoRegister?: boolean): RouteGroup;
@@ -1,5 +1,6 @@
1
1
  import { HttpMethod } from './HttpMethod';
2
2
  import { defineRoute } from './defineRoute';
3
+ import { globalRegistry } from './registry';
3
4
  /**
4
5
  * Represents a group of routes under a shared base path.
5
6
  *
@@ -19,7 +20,9 @@ export class RouteGroup {
19
20
  * @param basePath - The common prefix for all routes in the group (e.g. "/users")
20
21
  */
21
22
  constructor(basePath) {
23
+ this._routifyType = 'group';
22
24
  this.routes = [];
25
+ this.middlewares = [];
23
26
  this.basePath = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
24
27
  }
25
28
  /**
@@ -28,61 +31,97 @@ export class RouteGroup {
28
31
  getBasePath() {
29
32
  return this.basePath;
30
33
  }
34
+ /**
35
+ * Adds a middleware to all routes in this group.
36
+ *
37
+ * @param middleware - The middleware function to add
38
+ * @returns The current group instance
39
+ */
40
+ use(middleware) {
41
+ this.middlewares.push(middleware);
42
+ // Apply to already registered routes in this group
43
+ for (const route of this.routes) {
44
+ if (!route.middlewares)
45
+ route.middlewares = [];
46
+ route.middlewares.push(middleware);
47
+ }
48
+ return this;
49
+ }
31
50
  /**
32
51
  * Registers a GET route under the group's base path.
33
52
  *
34
53
  * @param path - Path relative to the base path (e.g. "/:id")
35
- * @param handler - The handler function for this route
54
+ * @param handlers - Middleware(s) followed by a handler function
36
55
  */
37
- addGet(path, handler) {
38
- return this.add(HttpMethod.GET, path, handler);
56
+ addGet(path, ...handlers) {
57
+ return this.add(HttpMethod.GET, path, ...handlers);
39
58
  }
40
59
  /**
41
60
  * Registers a POST route under the group's base path.
42
61
  *
43
62
  * @param path - Path relative to the base path
44
- * @param handler - The handler function for this route
63
+ * @param handlers - Middleware(s) followed by a handler function
45
64
  */
46
- addPost(path, handler) {
47
- return this.add(HttpMethod.POST, path, handler);
65
+ addPost(path, ...handlers) {
66
+ return this.add(HttpMethod.POST, path, ...handlers);
48
67
  }
49
68
  /**
50
69
  * Registers a PUT route under the group's base path.
51
70
  *
52
71
  * @param path - Path relative to the base path
53
- * @param handler - The handler function for this route
72
+ * @param handlers - Middleware(s) followed by a handler function
54
73
  */
55
- addPut(path, handler) {
56
- return this.add(HttpMethod.PUT, path, handler);
74
+ addPut(path, ...handlers) {
75
+ return this.add(HttpMethod.PUT, path, ...handlers);
57
76
  }
58
77
  /**
59
78
  * Registers a DELETE route under the group's base path.
60
79
  *
61
80
  * @param path - Path relative to the base path
62
- * @param handler - The handler function for this route
81
+ * @param handlers - Middleware(s) followed by a handler function
63
82
  */
64
- addDelete(path, handler) {
65
- return this.add(HttpMethod.DELETE, path, handler);
83
+ addDelete(path, ...handlers) {
84
+ return this.add(HttpMethod.DELETE, path, ...handlers);
66
85
  }
67
86
  /**
68
87
  * Registers a PATCH route under the group's base path.
69
88
  *
70
89
  * @param path - Path relative to the base path
71
- * @param handler - The handler function for this route
90
+ * @param handlers - Middleware(s) followed by a handler function
72
91
  */
73
- addPatch(path, handler) {
74
- return this.add(HttpMethod.PATCH, path, handler);
92
+ addPatch(path, ...handlers) {
93
+ return this.add(HttpMethod.PATCH, path, ...handlers);
75
94
  }
76
95
  /**
77
- * Internal method to register a route under the group with any HTTP method.
96
+ * Registers a route under the group's base path.
78
97
  *
79
98
  * @param method - HTTP verb
80
- * @param subPath - Route path relative to the base
81
- * @param handler - The handler function for this route
99
+ * @param path - Path relative to the base path
100
+ * @param args - Middleware(s) and handler
101
+ * @returns The current group instance
82
102
  */
83
- add(method, subPath, handler) {
84
- const normalizedPath = subPath.startsWith('/') ? subPath : `/${subPath}`;
85
- this.routes.push(defineRoute(method, `${this.basePath}${normalizedPath}`, handler));
103
+ add(method, path, ...args) {
104
+ let metadata;
105
+ if (args.length > 1 && typeof args[args.length - 1] === 'object' && typeof args[args.length - 2] === 'function') {
106
+ metadata = args.pop();
107
+ }
108
+ const handler = args.pop();
109
+ const middlewares = args;
110
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
111
+ this.routes.push(defineRoute({
112
+ method,
113
+ path: `${this.basePath}${normalizedPath}`,
114
+ handler,
115
+ middlewares: [...this.middlewares, ...middlewares],
116
+ metadata
117
+ }));
118
+ return this;
119
+ }
120
+ /**
121
+ * Registers the group and all its current routes to the global registry.
122
+ */
123
+ register() {
124
+ globalRegistry.register(this);
86
125
  return this;
87
126
  }
88
127
  /**
@@ -97,15 +136,19 @@ export class RouteGroup {
97
136
  *
98
137
  * @param basePath - The base path prefix for all routes
99
138
  * @param configure - Optional callback to configure the group inline
139
+ * @param autoRegister - If true, registers the group to the global registry
100
140
  *
101
141
  * @example
102
142
  * const users = defineGroup('/users', (group) => {
103
143
  * group.addGet('/:id', handler);
104
- * });
144
+ * }, true);
105
145
  */
106
- export function defineGroup(basePath, configure) {
146
+ export function defineGroup(basePath, configure, autoRegister) {
107
147
  const group = new RouteGroup(basePath);
108
148
  if (configure)
109
149
  configure(group);
150
+ if (autoRegister) {
151
+ globalRegistry.register(group);
152
+ }
110
153
  return group;
111
154
  }
@@ -1,12 +1,37 @@
1
1
  import type { APIContext, APIRoute } from 'astro';
2
- import { type ResultResponse } from './responseHelpers';
2
+ import { type HandlerResult } from './responseHelpers';
3
3
  /**
4
- * A flexible route handler that can return:
5
- * - a native `Response` object,
6
- * - a structured `ResultResponse` object,
7
- * - or a file stream (Blob, ArrayBuffer, or ReadableStream).
4
+ * Enhanced Astro context for Routify.
8
5
  */
9
- export type Handler = (ctx: APIContext) => Promise<ResultResponse | Response> | ResultResponse | Response;
6
+ export interface RoutifyContext<State = Record<string, any>> extends APIContext {
7
+ /**
8
+ * Parsed query parameters from the URL.
9
+ * Note: If multiple parameters have the same key, only the last one is included.
10
+ * Use `searchParams` for full multi-value support.
11
+ */
12
+ query: Record<string, string | string[]>;
13
+ /**
14
+ * Full URLSearchParams object for the incoming request.
15
+ */
16
+ searchParams: URLSearchParams;
17
+ /**
18
+ * Shared state container for middlewares and handlers (e.g., validation results).
19
+ * This is where middlewares can store information for subsequent handlers.
20
+ */
21
+ state: State;
22
+ }
23
+ /**
24
+ * Convenience type alias for RoutifyContext.
25
+ */
26
+ export type Context<State = Record<string, any>> = RoutifyContext<State>;
27
+ /**
28
+ * A middleware function that can modify the context or short-circuit the response.
29
+ */
30
+ export type Middleware<State = any> = (ctx: RoutifyContext<State>, next: () => Promise<Response>) => Promise<Response> | Response;
31
+ /**
32
+ * A flexible route handler that can return various types.
33
+ */
34
+ export type Handler<State = any> = (ctx: RoutifyContext<State>) => Promise<HandlerResult> | HandlerResult;
10
35
  /**
11
36
  * Wraps a `Handler` function into an `APIRoute` that:
12
37
  * - logs requests and responses,
@@ -18,10 +43,3 @@ export type Handler = (ctx: APIContext) => Promise<ResultResponse | Response> |
18
43
  * @returns An Astro-compatible `APIRoute` function
19
44
  */
20
45
  export declare function defineHandler(handler: Handler): APIRoute;
21
- /**
22
- * Type guard to detect ReadableStreams, used for streamed/binary responses.
23
- *
24
- * @param value - Any value to test
25
- * @returns True if it looks like a ReadableStream
26
- */
27
- export declare function isReadableStream(value: unknown): value is ReadableStream<Uint8Array>;
@@ -33,18 +33,7 @@ export function defineHandler(handler) {
33
33
  logResponse(result.status, start);
34
34
  return result;
35
35
  }
36
- // Direct binary or stream body (file download, etc.)
37
- if (result?.body instanceof Blob ||
38
- result?.body instanceof ArrayBuffer ||
39
- isReadableStream(result?.body)) {
40
- const res = new Response(result.body, {
41
- status: result.status,
42
- headers: result.headers,
43
- });
44
- logResponse(res.status, start);
45
- return res;
46
- }
47
- // Structured ResultResponse → native Astro Response
36
+ // Structured ResultResponse or other HandlerResult native Astro Response
48
37
  const finalResponse = toAstroResponse(result);
49
38
  logResponse(finalResponse.status, start);
50
39
  return finalResponse;
@@ -57,14 +46,3 @@ export function defineHandler(handler) {
57
46
  }
58
47
  };
59
48
  }
60
- /**
61
- * Type guard to detect ReadableStreams, used for streamed/binary responses.
62
- *
63
- * @param value - Any value to test
64
- * @returns True if it looks like a ReadableStream
65
- */
66
- export function isReadableStream(value) {
67
- return (typeof value === 'object' &&
68
- value !== null &&
69
- typeof value.getReader === 'function');
70
- }
@@ -1,5 +1,5 @@
1
1
  import { HttpMethod } from './HttpMethod';
2
- import type { Handler } from './defineHandler';
2
+ import type { Handler, Middleware } from './defineHandler';
3
3
  /**
4
4
  * Represents a single route definition.
5
5
  */
@@ -16,6 +16,19 @@ export interface Route {
16
16
  * The function that handles the request when matched.
17
17
  */
18
18
  handler: Handler;
19
+ /**
20
+ * Optional array of middlewares to run before the handler.
21
+ */
22
+ middlewares?: Middleware[];
23
+ /**
24
+ * Optional metadata for the route (e.g. for OpenAPI generation).
25
+ */
26
+ metadata?: Record<string, any>;
27
+ /**
28
+ * Internal marker to identify the object type during module discovery.
29
+ * @internal
30
+ */
31
+ _routifyType?: 'route';
19
32
  }
20
33
  /**
21
34
  * Defines a route using a `Route` object.
@@ -24,9 +37,10 @@ export interface Route {
24
37
  * defineRoute({ method: 'GET', path: '/users', handler });
25
38
  *
26
39
  * @param route - A fully constructed route object
40
+ * @param autoRegister - If true, registers the route to the global registry
27
41
  * @returns The validated Route object
28
42
  */
29
- export declare function defineRoute(route: Route): Route;
43
+ export declare function defineRoute(route: Route, autoRegister?: boolean): Route;
30
44
  /**
31
45
  * Defines a route by specifying its method, path, and handler explicitly.
32
46
  *
@@ -36,6 +50,21 @@ export declare function defineRoute(route: Route): Route;
36
50
  * @param method - HTTP method to match
37
51
  * @param path - Route path (must start with `/`)
38
52
  * @param handler - Function to handle the matched request
53
+ * @param autoRegister - If true, registers the route to the global registry
39
54
  * @returns The validated Route object
40
55
  */
41
- export declare function defineRoute(method: HttpMethod, path: string, handler: Handler): Route;
56
+ export declare function defineRoute(method: HttpMethod, path: string, handler: Handler, autoRegister?: boolean): Route;
57
+ /**
58
+ * Ensures the route is properly formed and uses a valid method + path format.
59
+ *
60
+ * @param route - Route to validate
61
+ * @throws If method is unsupported or path doesn't start with `/`
62
+ */
63
+ export declare function validateRoute({ method, path }: Route): void;
64
+ /**
65
+ * Checks if an object implements the `Route` interface.
66
+ *
67
+ * @param obj - The object to check
68
+ * @returns True if the object is a valid Route
69
+ */
70
+ export declare function isRoute(obj: any): obj is Route;
@@ -1,18 +1,28 @@
1
1
  import { ALLOWED_HTTP_METHODS } from './HttpMethod';
2
+ import { globalRegistry } from './registry';
2
3
  /**
3
4
  * Internal route definition logic that supports both overloads.
4
5
  */
5
- export function defineRoute(methodOrRoute, maybePath, maybeHandler) {
6
+ export function defineRoute(methodOrRoute, maybePathOrAutoRegister, maybeHandler, maybeAutoRegister) {
7
+ let autoRegister = false;
8
+ let route;
6
9
  if (typeof methodOrRoute === 'object') {
7
- validateRoute(methodOrRoute);
8
- return methodOrRoute;
10
+ route = methodOrRoute;
11
+ autoRegister = !!maybePathOrAutoRegister;
9
12
  }
10
- const route = {
11
- method: methodOrRoute,
12
- path: maybePath,
13
- handler: maybeHandler,
14
- };
13
+ else {
14
+ route = {
15
+ method: methodOrRoute,
16
+ path: maybePathOrAutoRegister,
17
+ handler: maybeHandler,
18
+ };
19
+ autoRegister = !!maybeAutoRegister;
20
+ }
21
+ route._routifyType = 'route';
15
22
  validateRoute(route);
23
+ if (autoRegister) {
24
+ globalRegistry.register(route);
25
+ }
16
26
  return route;
17
27
  }
18
28
  /**
@@ -21,11 +31,28 @@ export function defineRoute(methodOrRoute, maybePath, maybeHandler) {
21
31
  * @param route - Route to validate
22
32
  * @throws If method is unsupported or path doesn't start with `/`
23
33
  */
24
- function validateRoute({ method, path }) {
34
+ export function validateRoute({ method, path }) {
25
35
  if (!path.startsWith('/')) {
26
36
  throw new Error(`Route path must start with '/': ${path}`);
27
37
  }
28
38
  if (!ALLOWED_HTTP_METHODS.has(method)) {
29
39
  throw new Error(`Unsupported HTTP method in route: ${method}`);
30
40
  }
41
+ if (path.includes('**') && !path.endsWith('**')) {
42
+ throw new Error(`Catch-all '**' is only allowed at the end of a path: ${path}`);
43
+ }
44
+ }
45
+ /**
46
+ * Checks if an object implements the `Route` interface.
47
+ *
48
+ * @param obj - The object to check
49
+ * @returns True if the object is a valid Route
50
+ */
51
+ export function isRoute(obj) {
52
+ return (obj &&
53
+ typeof obj === 'object' &&
54
+ (obj._routifyType === 'route' ||
55
+ (typeof obj.method === 'string' &&
56
+ typeof obj.path === 'string' &&
57
+ typeof obj.handler === 'function')));
31
58
  }
@@ -1,5 +1,6 @@
1
1
  import type { APIRoute } from 'astro';
2
- import { notFound } from './responseHelpers';
2
+ import { type RoutifyContext } from './defineHandler';
3
+ import { notFound, type HandlerResult } from './responseHelpers';
3
4
  import type { Route } from './defineRoute';
4
5
  /**
5
6
  * Optional configuration for the router instance.
@@ -14,6 +15,15 @@ export interface RouterOptions {
14
15
  * Custom handler to return when no route is matched (404).
15
16
  */
16
17
  onNotFound?: () => ReturnType<typeof notFound>;
18
+ /**
19
+ * If true, enables debug logging for route matching.
20
+ * Useful during development to trace which route is being matched.
21
+ */
22
+ debug?: boolean;
23
+ /**
24
+ * Custom error handler for the router.
25
+ */
26
+ onError?: (error: unknown, ctx: RoutifyContext) => HandlerResult | Response;
17
27
  }
18
28
  /**
19
29
  * Defines a router that dynamically matches registered routes based on method and path.