clear-router 2.1.10 → 2.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,6 +42,7 @@ yarn add clear-router express
42
42
 
43
43
  - Simple and clean route declarations (get, post, put, delete, patch, options, head)
44
44
  - Grouped routes with prefix
45
+ - Method override support via body or header keys (configurable)
45
46
  - Middleware stack: per-route and group-level
46
47
  - Controller-method pair as route handler
47
48
  - Supports HttpContext style handlers: { req, res, next }
@@ -5,10 +5,15 @@ import { H3, H3Event, Middleware as Middleware$1, TypedServerRequest } from "h3"
5
5
  type H3App = Omit<H3['fetch'], 'fetch'> & {
6
6
  fetch: (request: TypedServerRequest) => Promise<Response>;
7
7
  };
8
+ type HttpRequest = H3Event['req'] & {
9
+ getBody: () => Record<string, any>;
10
+ };
8
11
  /**
9
12
  * HTTP context passed to route handlers
10
13
  */
11
- type HttpContext$1 = H3Event & {};
14
+ type HttpContext$1 = Omit<H3Event, 'req'> & {
15
+ req: HttpRequest;
16
+ };
12
17
  /**
13
18
  * Route handler function type
14
19
  */
@@ -66,11 +71,14 @@ declare class ClearRequest<X = any, M = Middleware$1 | Middleware> {
66
71
  }
67
72
  //#endregion
68
73
  //#region types/express.d.ts
74
+ interface RequestWithGetBody extends Request {
75
+ getBody: () => Record<string, any>;
76
+ }
69
77
  /**
70
78
  * HTTP context passed to route handlers
71
79
  */
72
80
  interface HttpContext {
73
- req: Request;
81
+ req: RequestWithGetBody;
74
82
  res: Response$1;
75
83
  next: NextFunction;
76
84
  }
@@ -115,5 +123,16 @@ type ControllerAction = 'index' | 'show' | 'create' | 'update' | 'destroy';
115
123
  */
116
124
  type RequestData = Record<string, any>;
117
125
  type ApiResourceMiddleware<M extends Middleware | Middleware$1> = M | M[] | { [K in ControllerAction]?: M | M[] };
126
+ interface RouterConfig {
127
+ /**
128
+ * Configuration for method override functionality, allowing clients to use a
129
+ * specific header or body parameter to override the HTTP method.
130
+ */
131
+ methodOverride?: {
132
+ /** Whether method override is enabled */enabled?: boolean; /** Keys in the request body to check for method override */
133
+ bodyKeys?: string[] | string; /** Keys in the request headers to check for method override */
134
+ headerKeys?: string[] | string;
135
+ };
136
+ }
118
137
  //#endregion
119
- export { HttpContext as a, H3App as c, Middleware$1 as d, Handler as i, Handler$1 as l, ControllerAction as n, Middleware as o, HttpMethod as r, Route as s, ApiResourceMiddleware as t, HttpContext$1 as u };
138
+ export { Handler as a, Route as c, HttpContext$1 as d, Middleware$1 as f, RouterConfig as i, H3App as l, ControllerAction as n, HttpContext as o, HttpMethod as r, Middleware as s, ApiResourceMiddleware as t, Handler$1 as u };
@@ -5,10 +5,15 @@ import { NextFunction, Request, Response as Response$1 } from "express";
5
5
  type H3App = Omit<H3['fetch'], 'fetch'> & {
6
6
  fetch: (request: TypedServerRequest) => Promise<Response>;
7
7
  };
8
+ type HttpRequest = H3Event['req'] & {
9
+ getBody: () => Record<string, any>;
10
+ };
8
11
  /**
9
12
  * HTTP context passed to route handlers
10
13
  */
11
- type HttpContext$1 = H3Event & {};
14
+ type HttpContext$1 = Omit<H3Event, 'req'> & {
15
+ req: HttpRequest;
16
+ };
12
17
  /**
13
18
  * Route handler function type
14
19
  */
@@ -66,11 +71,14 @@ declare class ClearRequest<X = any, M = Middleware$1 | Middleware> {
66
71
  }
67
72
  //#endregion
68
73
  //#region types/express.d.ts
74
+ interface RequestWithGetBody extends Request {
75
+ getBody: () => Record<string, any>;
76
+ }
69
77
  /**
70
78
  * HTTP context passed to route handlers
71
79
  */
72
80
  interface HttpContext {
73
- req: Request;
81
+ req: RequestWithGetBody;
74
82
  res: Response$1;
75
83
  next: NextFunction;
76
84
  }
@@ -115,5 +123,16 @@ type ControllerAction = 'index' | 'show' | 'create' | 'update' | 'destroy';
115
123
  */
116
124
  type RequestData = Record<string, any>;
117
125
  type ApiResourceMiddleware<M extends Middleware | Middleware$1> = M | M[] | { [K in ControllerAction]?: M | M[] };
126
+ interface RouterConfig {
127
+ /**
128
+ * Configuration for method override functionality, allowing clients to use a
129
+ * specific header or body parameter to override the HTTP method.
130
+ */
131
+ methodOverride?: {
132
+ /** Whether method override is enabled */enabled?: boolean; /** Keys in the request body to check for method override */
133
+ bodyKeys?: string[] | string; /** Keys in the request headers to check for method override */
134
+ headerKeys?: string[] | string;
135
+ };
136
+ }
118
137
  //#endregion
119
- export { HttpContext as a, H3App as c, Middleware$1 as d, Handler as i, Handler$1 as l, ControllerAction as n, Middleware as o, HttpMethod as r, Route as s, ApiResourceMiddleware as t, HttpContext$1 as u };
138
+ export { Handler as a, Route as c, HttpContext$1 as d, Middleware$1 as f, RouterConfig as i, H3App as l, ControllerAction as n, HttpContext as o, HttpMethod as r, Middleware as s, ApiResourceMiddleware as t, Handler$1 as u };
@@ -11,6 +11,11 @@ let node_async_hooks = require("node:async_hooks");
11
11
  * @repository https://github.com/toneflix/clear-router
12
12
  */
13
13
  var Router = class Router {
14
+ static config = { methodOverride: {
15
+ enabled: true,
16
+ bodyKeys: ["_method"],
17
+ headerKeys: ["x-http-method"]
18
+ } };
14
19
  static groupContext = new node_async_hooks.AsyncLocalStorage();
15
20
  /**
16
21
  * All registered routes
@@ -45,6 +50,56 @@ var Router = class Router {
45
50
  return "/" + path.split("/").filter(Boolean).join("/");
46
51
  }
47
52
  /**
53
+ * Configure router settings to modify behavior.
54
+ *
55
+ * @param options - Configuration options for the router
56
+ * @returns
57
+ */
58
+ static configure(options) {
59
+ if (!this.config.methodOverride) this.config.methodOverride = {
60
+ enabled: true,
61
+ bodyKeys: ["_method"],
62
+ headerKeys: ["x-http-method"]
63
+ };
64
+ const override = options?.methodOverride;
65
+ if (!override) return;
66
+ if (typeof override.enabled === "boolean") this.config.methodOverride.enabled = override.enabled;
67
+ const bodyKeys = override.bodyKeys;
68
+ if (typeof bodyKeys !== "undefined") this.config.methodOverride.bodyKeys = (Array.isArray(bodyKeys) ? bodyKeys : [bodyKeys]).map((e) => String(e).trim()).filter(Boolean);
69
+ const headerKeys = override.headerKeys;
70
+ if (typeof headerKeys !== "undefined") this.config.methodOverride.headerKeys = (Array.isArray(headerKeys) ? headerKeys : [headerKeys]).map((e) => String(e).trim().toLowerCase()).filter(Boolean);
71
+ }
72
+ static ensureRequestBodyAccessor(req) {
73
+ if (typeof req.getBody !== "function") req.getBody = () => req.body ?? {};
74
+ }
75
+ static resolveMethodOverride(method, headers, body) {
76
+ if (!this.config.methodOverride?.enabled || method.toLowerCase() !== "post") return null;
77
+ let override;
78
+ for (const key of this.config.methodOverride?.headerKeys || []) {
79
+ const value = headers?.[key];
80
+ if (Array.isArray(value) ? value[0] : value) {
81
+ override = Array.isArray(value) ? value[0] : value;
82
+ break;
83
+ }
84
+ }
85
+ if (!override && body && typeof body === "object") for (const key of this.config.methodOverride?.bodyKeys || []) {
86
+ const value = body[key];
87
+ if (typeof value !== "undefined" && value !== null && value !== "") {
88
+ override = value;
89
+ break;
90
+ }
91
+ }
92
+ const normalized = String(override || "").trim().toLowerCase();
93
+ if (!normalized) return null;
94
+ if ([
95
+ "put",
96
+ "patch",
97
+ "delete",
98
+ "post"
99
+ ].includes(normalized)) return normalized;
100
+ return null;
101
+ }
102
+ /**
48
103
  * Add a route with specified HTTP methods, path, handler, and middlewares
49
104
  * @param methods - HTTP method(s) for the route
50
105
  * @param path - Route path
@@ -256,8 +311,39 @@ var Router = class Router {
256
311
  console.error("[ROUTES]", error.message);
257
312
  throw error;
258
313
  }
259
- router[method](route.path, ...route.middlewares || [], async (req, res, next) => {
314
+ router[method](route.path, (req, res, next) => {
315
+ Router.ensureRequestBodyAccessor(req);
316
+ const override = Router.resolveMethodOverride(req.method, req.headers, req.body);
317
+ if (method === "post" && override && override !== "post") return next("route");
318
+ return next();
319
+ }, ...route.middlewares || [], async (req, res, next) => {
320
+ try {
321
+ Router.ensureRequestBodyAccessor(req);
322
+ const ctx = {
323
+ req,
324
+ res,
325
+ next
326
+ };
327
+ const inst = instance ?? route;
328
+ await Router.bindRequestToInstance(ctx, inst, route);
329
+ const result = handlerFunction(ctx, inst.clearRequest);
330
+ await Promise.resolve(result);
331
+ } catch (error) {
332
+ next(error);
333
+ }
334
+ });
335
+ if ([
336
+ "put",
337
+ "patch",
338
+ "delete"
339
+ ].includes(method)) router.post(route.path, (req, res, next) => {
340
+ Router.ensureRequestBodyAccessor(req);
341
+ if (Router.resolveMethodOverride(req.method, req.headers, req.body) !== method) return next("route");
342
+ req.method = method.toUpperCase();
343
+ return next();
344
+ }, ...route.middlewares || [], async (req, res, next) => {
260
345
  try {
346
+ Router.ensureRequestBodyAccessor(req);
261
347
  const ctx = {
262
348
  req,
263
349
  res,
@@ -276,8 +362,9 @@ var Router = class Router {
276
362
  }
277
363
  static async bindRequestToInstance(ctx, instance, route) {
278
364
  if (!instance) return;
365
+ Router.ensureRequestBodyAccessor(ctx.req);
279
366
  instance.ctx = ctx;
280
- instance.body = ctx.req.body;
367
+ instance.body = ctx.req.getBody();
281
368
  instance.query = ctx.req.query;
282
369
  instance.params = ctx.req.params;
283
370
  instance.clearRequest = new require_Route.ClearRequest({
@@ -1,4 +1,4 @@
1
- import { a as HttpContext, i as Handler, n as ControllerAction, o as Middleware, r as HttpMethod, s as Route, t as ApiResourceMiddleware } from "../basic-Chn8OGPD.cjs";
1
+ import { a as Handler, c as Route, i as RouterConfig, n as ControllerAction, o as HttpContext, r as HttpMethod, s as Middleware, t as ApiResourceMiddleware } from "../basic-C_1O6RVq.cjs";
2
2
  import { Router as Router$1 } from "express";
3
3
 
4
4
  //#region src/express/router.d.ts
@@ -10,6 +10,7 @@ import { Router as Router$1 } from "express";
10
10
  * @repository https://github.com/toneflix/clear-router
11
11
  */
12
12
  declare class Router {
13
+ static config: RouterConfig;
13
14
  private static readonly groupContext;
14
15
  /**
15
16
  * All registered routes
@@ -41,6 +42,15 @@ declare class Router {
41
42
  * @returns Normalized path
42
43
  */
43
44
  static normalizePath(path: string): string;
45
+ /**
46
+ * Configure router settings to modify behavior.
47
+ *
48
+ * @param options - Configuration options for the router
49
+ * @returns
50
+ */
51
+ static configure(options?: RouterConfig): void;
52
+ private static ensureRequestBodyAccessor;
53
+ private static resolveMethodOverride;
44
54
  /**
45
55
  * Add a route with specified HTTP methods, path, handler, and middlewares
46
56
  * @param methods - HTTP method(s) for the route
@@ -1,4 +1,4 @@
1
- import { a as HttpContext, i as Handler, n as ControllerAction, o as Middleware, r as HttpMethod, s as Route, t as ApiResourceMiddleware } from "../basic-DJmmZq1h.mjs";
1
+ import { a as Handler, c as Route, i as RouterConfig, n as ControllerAction, o as HttpContext, r as HttpMethod, s as Middleware, t as ApiResourceMiddleware } from "../basic-cLeny2Zk.mjs";
2
2
  import { Router as Router$1 } from "express";
3
3
 
4
4
  //#region src/express/router.d.ts
@@ -10,6 +10,7 @@ import { Router as Router$1 } from "express";
10
10
  * @repository https://github.com/toneflix/clear-router
11
11
  */
12
12
  declare class Router {
13
+ static config: RouterConfig;
13
14
  private static readonly groupContext;
14
15
  /**
15
16
  * All registered routes
@@ -41,6 +42,15 @@ declare class Router {
41
42
  * @returns Normalized path
42
43
  */
43
44
  static normalizePath(path: string): string;
45
+ /**
46
+ * Configure router settings to modify behavior.
47
+ *
48
+ * @param options - Configuration options for the router
49
+ * @returns
50
+ */
51
+ static configure(options?: RouterConfig): void;
52
+ private static ensureRequestBodyAccessor;
53
+ private static resolveMethodOverride;
44
54
  /**
45
55
  * Add a route with specified HTTP methods, path, handler, and middlewares
46
56
  * @param methods - HTTP method(s) for the route
@@ -10,6 +10,11 @@ import { AsyncLocalStorage } from "node:async_hooks";
10
10
  * @repository https://github.com/toneflix/clear-router
11
11
  */
12
12
  var Router = class Router {
13
+ static config = { methodOverride: {
14
+ enabled: true,
15
+ bodyKeys: ["_method"],
16
+ headerKeys: ["x-http-method"]
17
+ } };
13
18
  static groupContext = new AsyncLocalStorage();
14
19
  /**
15
20
  * All registered routes
@@ -44,6 +49,56 @@ var Router = class Router {
44
49
  return "/" + path.split("/").filter(Boolean).join("/");
45
50
  }
46
51
  /**
52
+ * Configure router settings to modify behavior.
53
+ *
54
+ * @param options - Configuration options for the router
55
+ * @returns
56
+ */
57
+ static configure(options) {
58
+ if (!this.config.methodOverride) this.config.methodOverride = {
59
+ enabled: true,
60
+ bodyKeys: ["_method"],
61
+ headerKeys: ["x-http-method"]
62
+ };
63
+ const override = options?.methodOverride;
64
+ if (!override) return;
65
+ if (typeof override.enabled === "boolean") this.config.methodOverride.enabled = override.enabled;
66
+ const bodyKeys = override.bodyKeys;
67
+ if (typeof bodyKeys !== "undefined") this.config.methodOverride.bodyKeys = (Array.isArray(bodyKeys) ? bodyKeys : [bodyKeys]).map((e) => String(e).trim()).filter(Boolean);
68
+ const headerKeys = override.headerKeys;
69
+ if (typeof headerKeys !== "undefined") this.config.methodOverride.headerKeys = (Array.isArray(headerKeys) ? headerKeys : [headerKeys]).map((e) => String(e).trim().toLowerCase()).filter(Boolean);
70
+ }
71
+ static ensureRequestBodyAccessor(req) {
72
+ if (typeof req.getBody !== "function") req.getBody = () => req.body ?? {};
73
+ }
74
+ static resolveMethodOverride(method, headers, body) {
75
+ if (!this.config.methodOverride?.enabled || method.toLowerCase() !== "post") return null;
76
+ let override;
77
+ for (const key of this.config.methodOverride?.headerKeys || []) {
78
+ const value = headers?.[key];
79
+ if (Array.isArray(value) ? value[0] : value) {
80
+ override = Array.isArray(value) ? value[0] : value;
81
+ break;
82
+ }
83
+ }
84
+ if (!override && body && typeof body === "object") for (const key of this.config.methodOverride?.bodyKeys || []) {
85
+ const value = body[key];
86
+ if (typeof value !== "undefined" && value !== null && value !== "") {
87
+ override = value;
88
+ break;
89
+ }
90
+ }
91
+ const normalized = String(override || "").trim().toLowerCase();
92
+ if (!normalized) return null;
93
+ if ([
94
+ "put",
95
+ "patch",
96
+ "delete",
97
+ "post"
98
+ ].includes(normalized)) return normalized;
99
+ return null;
100
+ }
101
+ /**
47
102
  * Add a route with specified HTTP methods, path, handler, and middlewares
48
103
  * @param methods - HTTP method(s) for the route
49
104
  * @param path - Route path
@@ -255,8 +310,39 @@ var Router = class Router {
255
310
  console.error("[ROUTES]", error.message);
256
311
  throw error;
257
312
  }
258
- router[method](route.path, ...route.middlewares || [], async (req, res, next) => {
313
+ router[method](route.path, (req, res, next) => {
314
+ Router.ensureRequestBodyAccessor(req);
315
+ const override = Router.resolveMethodOverride(req.method, req.headers, req.body);
316
+ if (method === "post" && override && override !== "post") return next("route");
317
+ return next();
318
+ }, ...route.middlewares || [], async (req, res, next) => {
319
+ try {
320
+ Router.ensureRequestBodyAccessor(req);
321
+ const ctx = {
322
+ req,
323
+ res,
324
+ next
325
+ };
326
+ const inst = instance ?? route;
327
+ await Router.bindRequestToInstance(ctx, inst, route);
328
+ const result = handlerFunction(ctx, inst.clearRequest);
329
+ await Promise.resolve(result);
330
+ } catch (error) {
331
+ next(error);
332
+ }
333
+ });
334
+ if ([
335
+ "put",
336
+ "patch",
337
+ "delete"
338
+ ].includes(method)) router.post(route.path, (req, res, next) => {
339
+ Router.ensureRequestBodyAccessor(req);
340
+ if (Router.resolveMethodOverride(req.method, req.headers, req.body) !== method) return next("route");
341
+ req.method = method.toUpperCase();
342
+ return next();
343
+ }, ...route.middlewares || [], async (req, res, next) => {
259
344
  try {
345
+ Router.ensureRequestBodyAccessor(req);
260
346
  const ctx = {
261
347
  req,
262
348
  res,
@@ -275,8 +361,9 @@ var Router = class Router {
275
361
  }
276
362
  static async bindRequestToInstance(ctx, instance, route) {
277
363
  if (!instance) return;
364
+ Router.ensureRequestBodyAccessor(ctx.req);
278
365
  instance.ctx = ctx;
279
- instance.body = ctx.req.body;
366
+ instance.body = ctx.req.getBody();
280
367
  instance.query = ctx.req.query;
281
368
  instance.params = ctx.req.params;
282
369
  instance.clearRequest = new ClearRequest({
package/dist/h3/index.cjs CHANGED
@@ -11,6 +11,12 @@ let h3 = require("h3");
11
11
  * @repository https://github.com/toneflix/clear-router
12
12
  */
13
13
  var Router = class Router {
14
+ static config = { methodOverride: {
15
+ enabled: true,
16
+ bodyKeys: ["_method"],
17
+ headerKeys: ["x-http-method"]
18
+ } };
19
+ static bodyCache = /* @__PURE__ */ new WeakMap();
14
20
  static groupContext = new node_async_hooks.AsyncLocalStorage();
15
21
  /**
16
22
  * All registered routes
@@ -44,6 +50,62 @@ var Router = class Router {
44
50
  static normalizePath(path) {
45
51
  return "/" + path.split("/").filter(Boolean).join("/");
46
52
  }
53
+ static configure(options) {
54
+ if (!this.config.methodOverride) this.config.methodOverride = {
55
+ enabled: true,
56
+ bodyKeys: ["_method"],
57
+ headerKeys: ["x-http-method"]
58
+ };
59
+ const override = options?.methodOverride;
60
+ if (!override) return;
61
+ if (typeof override.enabled === "boolean") this.config.methodOverride.enabled = override.enabled;
62
+ const bodyKeys = override.bodyKeys;
63
+ if (typeof bodyKeys !== "undefined") this.config.methodOverride.bodyKeys = (Array.isArray(bodyKeys) ? bodyKeys : [bodyKeys]).map((e) => String(e).trim()).filter(Boolean);
64
+ const headerKeys = override.headerKeys;
65
+ if (typeof headerKeys !== "undefined") this.config.methodOverride.headerKeys = (Array.isArray(headerKeys) ? headerKeys : [headerKeys]).map((e) => String(e).trim().toLowerCase()).filter(Boolean);
66
+ }
67
+ static async readBodyCached(ctx) {
68
+ if (this.bodyCache.has(ctx)) {
69
+ const cached = this.bodyCache.get(ctx);
70
+ ctx.req.getBody = () => cached;
71
+ return cached;
72
+ }
73
+ let body = {};
74
+ if (ctx.req.headers.get("content-type")?.includes("multipart/form-data")) (await ctx.req.formData()).forEach((value, key) => {
75
+ body[key] = value;
76
+ });
77
+ else body = await (0, h3.readBody)(ctx) ?? {};
78
+ ctx.req.getBody = () => body;
79
+ this.bodyCache.set(ctx, body);
80
+ return body;
81
+ }
82
+ static resolveMethodOverride(method, headers, body) {
83
+ if (!this.config.methodOverride?.enabled || method.toLowerCase() !== "post") return null;
84
+ let override;
85
+ for (const key of this.config.methodOverride?.headerKeys || []) {
86
+ const value = headers.get(key);
87
+ if (value) {
88
+ override = value;
89
+ break;
90
+ }
91
+ }
92
+ if (!override && body && typeof body === "object") for (const key of this.config.methodOverride?.bodyKeys || []) {
93
+ const value = body[key];
94
+ if (typeof value !== "undefined" && value !== null && value !== "") {
95
+ override = value;
96
+ break;
97
+ }
98
+ }
99
+ const normalized = String(override || "").trim().toLowerCase();
100
+ if (!normalized) return null;
101
+ if ([
102
+ "put",
103
+ "patch",
104
+ "delete",
105
+ "post"
106
+ ].includes(normalized)) return normalized;
107
+ return null;
108
+ }
47
109
  /**
48
110
  * Add a route with specified HTTP methods, path, handler, and middlewares
49
111
  * @param methods - HTTP method(s) for the route
@@ -266,8 +328,28 @@ var Router = class Router {
266
328
  app[method](route.path, async (event) => {
267
329
  try {
268
330
  const ctx = event;
331
+ const reqBody = await Router.readBodyCached(ctx);
332
+ const override = Router.resolveMethodOverride(ctx.req.method, ctx.req.headers, reqBody);
333
+ if (method === "post" && override && override !== "post") return;
334
+ const inst = instance ?? route;
335
+ await Router.bindRequestToInstance(ctx, inst, route, reqBody);
336
+ const result = handlerFunction(ctx, inst.clearRequest);
337
+ return await Promise.resolve(result);
338
+ } catch (error) {
339
+ return error;
340
+ }
341
+ }, { middleware: route.middlewares });
342
+ if ([
343
+ "put",
344
+ "patch",
345
+ "delete"
346
+ ].includes(method)) app.post(route.path, async (event) => {
347
+ try {
348
+ const ctx = event;
349
+ const reqBody = await Router.readBodyCached(ctx);
350
+ if (Router.resolveMethodOverride(ctx.req.method, ctx.req.headers, reqBody) !== method) return;
269
351
  const inst = instance ?? route;
270
- await Router.bindRequestToInstance(ctx, inst, route);
352
+ await Router.bindRequestToInstance(ctx, inst, route, reqBody);
271
353
  const result = handlerFunction(ctx, inst.clearRequest);
272
354
  return await Promise.resolve(result);
273
355
  } catch (error) {
@@ -278,10 +360,10 @@ var Router = class Router {
278
360
  }
279
361
  return app;
280
362
  }
281
- static async bindRequestToInstance(ctx, instance, route) {
363
+ static async bindRequestToInstance(ctx, instance, route, body) {
282
364
  if (!instance) return;
283
365
  instance.ctx = ctx;
284
- instance.body = await (0, h3.readBody)(ctx) ?? {};
366
+ instance.body = body ?? await Router.readBodyCached(ctx);
285
367
  instance.query = (0, h3.getQuery)(ctx);
286
368
  instance.params = (0, h3.getRouterParams)(ctx, { decode: true });
287
369
  instance.clearRequest = new require_Route.ClearRequest({
@@ -1,4 +1,4 @@
1
- import { c as H3App, d as Middleware, l as Handler, n as ControllerAction, r as HttpMethod, s as Route, t as ApiResourceMiddleware, u as HttpContext } from "../basic-Chn8OGPD.cjs";
1
+ import { c as Route, d as HttpContext, f as Middleware, i as RouterConfig, l as H3App, n as ControllerAction, r as HttpMethod, t as ApiResourceMiddleware, u as Handler } from "../basic-C_1O6RVq.cjs";
2
2
  import { H3 } from "h3";
3
3
 
4
4
  //#region src/h3/router.d.ts
@@ -9,6 +9,8 @@ import { H3 } from "h3";
9
9
  * @repository https://github.com/toneflix/clear-router
10
10
  */
11
11
  declare class Router {
12
+ static config: RouterConfig;
13
+ private static readonly bodyCache;
12
14
  private static readonly groupContext;
13
15
  /**
14
16
  * All registered routes
@@ -40,6 +42,9 @@ declare class Router {
40
42
  * @returns Normalized path
41
43
  */
42
44
  static normalizePath(path: string): string;
45
+ static configure(options?: RouterConfig): void;
46
+ private static readBodyCached;
47
+ private static resolveMethodOverride;
43
48
  /**
44
49
  * Add a route with specified HTTP methods, path, handler, and middlewares
45
50
  * @param methods - HTTP method(s) for the route
@@ -1,4 +1,4 @@
1
- import { c as H3App, d as Middleware, l as Handler, n as ControllerAction, r as HttpMethod, s as Route, t as ApiResourceMiddleware, u as HttpContext } from "../basic-DJmmZq1h.mjs";
1
+ import { c as Route, d as HttpContext, f as Middleware, i as RouterConfig, l as H3App, n as ControllerAction, r as HttpMethod, t as ApiResourceMiddleware, u as Handler } from "../basic-cLeny2Zk.mjs";
2
2
  import { H3 } from "h3";
3
3
 
4
4
  //#region src/h3/router.d.ts
@@ -9,6 +9,8 @@ import { H3 } from "h3";
9
9
  * @repository https://github.com/toneflix/clear-router
10
10
  */
11
11
  declare class Router {
12
+ static config: RouterConfig;
13
+ private static readonly bodyCache;
12
14
  private static readonly groupContext;
13
15
  /**
14
16
  * All registered routes
@@ -40,6 +42,9 @@ declare class Router {
40
42
  * @returns Normalized path
41
43
  */
42
44
  static normalizePath(path: string): string;
45
+ static configure(options?: RouterConfig): void;
46
+ private static readBodyCached;
47
+ private static resolveMethodOverride;
43
48
  /**
44
49
  * Add a route with specified HTTP methods, path, handler, and middlewares
45
50
  * @param methods - HTTP method(s) for the route
package/dist/h3/index.mjs CHANGED
@@ -10,6 +10,12 @@ import { getQuery, getRouterParams, readBody } from "h3";
10
10
  * @repository https://github.com/toneflix/clear-router
11
11
  */
12
12
  var Router = class Router {
13
+ static config = { methodOverride: {
14
+ enabled: true,
15
+ bodyKeys: ["_method"],
16
+ headerKeys: ["x-http-method"]
17
+ } };
18
+ static bodyCache = /* @__PURE__ */ new WeakMap();
13
19
  static groupContext = new AsyncLocalStorage();
14
20
  /**
15
21
  * All registered routes
@@ -43,6 +49,62 @@ var Router = class Router {
43
49
  static normalizePath(path) {
44
50
  return "/" + path.split("/").filter(Boolean).join("/");
45
51
  }
52
+ static configure(options) {
53
+ if (!this.config.methodOverride) this.config.methodOverride = {
54
+ enabled: true,
55
+ bodyKeys: ["_method"],
56
+ headerKeys: ["x-http-method"]
57
+ };
58
+ const override = options?.methodOverride;
59
+ if (!override) return;
60
+ if (typeof override.enabled === "boolean") this.config.methodOverride.enabled = override.enabled;
61
+ const bodyKeys = override.bodyKeys;
62
+ if (typeof bodyKeys !== "undefined") this.config.methodOverride.bodyKeys = (Array.isArray(bodyKeys) ? bodyKeys : [bodyKeys]).map((e) => String(e).trim()).filter(Boolean);
63
+ const headerKeys = override.headerKeys;
64
+ if (typeof headerKeys !== "undefined") this.config.methodOverride.headerKeys = (Array.isArray(headerKeys) ? headerKeys : [headerKeys]).map((e) => String(e).trim().toLowerCase()).filter(Boolean);
65
+ }
66
+ static async readBodyCached(ctx) {
67
+ if (this.bodyCache.has(ctx)) {
68
+ const cached = this.bodyCache.get(ctx);
69
+ ctx.req.getBody = () => cached;
70
+ return cached;
71
+ }
72
+ let body = {};
73
+ if (ctx.req.headers.get("content-type")?.includes("multipart/form-data")) (await ctx.req.formData()).forEach((value, key) => {
74
+ body[key] = value;
75
+ });
76
+ else body = await readBody(ctx) ?? {};
77
+ ctx.req.getBody = () => body;
78
+ this.bodyCache.set(ctx, body);
79
+ return body;
80
+ }
81
+ static resolveMethodOverride(method, headers, body) {
82
+ if (!this.config.methodOverride?.enabled || method.toLowerCase() !== "post") return null;
83
+ let override;
84
+ for (const key of this.config.methodOverride?.headerKeys || []) {
85
+ const value = headers.get(key);
86
+ if (value) {
87
+ override = value;
88
+ break;
89
+ }
90
+ }
91
+ if (!override && body && typeof body === "object") for (const key of this.config.methodOverride?.bodyKeys || []) {
92
+ const value = body[key];
93
+ if (typeof value !== "undefined" && value !== null && value !== "") {
94
+ override = value;
95
+ break;
96
+ }
97
+ }
98
+ const normalized = String(override || "").trim().toLowerCase();
99
+ if (!normalized) return null;
100
+ if ([
101
+ "put",
102
+ "patch",
103
+ "delete",
104
+ "post"
105
+ ].includes(normalized)) return normalized;
106
+ return null;
107
+ }
46
108
  /**
47
109
  * Add a route with specified HTTP methods, path, handler, and middlewares
48
110
  * @param methods - HTTP method(s) for the route
@@ -265,8 +327,28 @@ var Router = class Router {
265
327
  app[method](route.path, async (event) => {
266
328
  try {
267
329
  const ctx = event;
330
+ const reqBody = await Router.readBodyCached(ctx);
331
+ const override = Router.resolveMethodOverride(ctx.req.method, ctx.req.headers, reqBody);
332
+ if (method === "post" && override && override !== "post") return;
333
+ const inst = instance ?? route;
334
+ await Router.bindRequestToInstance(ctx, inst, route, reqBody);
335
+ const result = handlerFunction(ctx, inst.clearRequest);
336
+ return await Promise.resolve(result);
337
+ } catch (error) {
338
+ return error;
339
+ }
340
+ }, { middleware: route.middlewares });
341
+ if ([
342
+ "put",
343
+ "patch",
344
+ "delete"
345
+ ].includes(method)) app.post(route.path, async (event) => {
346
+ try {
347
+ const ctx = event;
348
+ const reqBody = await Router.readBodyCached(ctx);
349
+ if (Router.resolveMethodOverride(ctx.req.method, ctx.req.headers, reqBody) !== method) return;
268
350
  const inst = instance ?? route;
269
- await Router.bindRequestToInstance(ctx, inst, route);
351
+ await Router.bindRequestToInstance(ctx, inst, route, reqBody);
270
352
  const result = handlerFunction(ctx, inst.clearRequest);
271
353
  return await Promise.resolve(result);
272
354
  } catch (error) {
@@ -277,10 +359,10 @@ var Router = class Router {
277
359
  }
278
360
  return app;
279
361
  }
280
- static async bindRequestToInstance(ctx, instance, route) {
362
+ static async bindRequestToInstance(ctx, instance, route, body) {
281
363
  if (!instance) return;
282
364
  instance.ctx = ctx;
283
- instance.body = await readBody(ctx) ?? {};
365
+ instance.body = body ?? await Router.readBodyCached(ctx);
284
366
  instance.query = getQuery(ctx);
285
367
  instance.params = getRouterParams(ctx, { decode: true });
286
368
  instance.clearRequest = new ClearRequest({
package/dist/index.d.cts CHANGED
@@ -2,10 +2,15 @@ import { NextFunction, Request, Response as Response$1 } from "express";
2
2
  import { H3Event, Middleware as Middleware$1 } from "h3";
3
3
 
4
4
  //#region types/h3.d.ts
5
+ type HttpRequest = H3Event['req'] & {
6
+ getBody: () => Record<string, any>;
7
+ };
5
8
  /**
6
9
  * HTTP context passed to route handlers
7
10
  */
8
- type HttpContext = H3Event & {};
11
+ type HttpContext = Omit<H3Event, 'req'> & {
12
+ req: HttpRequest;
13
+ };
9
14
  /**
10
15
  * Route handler function type
11
16
  */
@@ -13,10 +18,12 @@ type RouteHandler = (
13
18
  /**
14
19
  * H3 event context
15
20
  */
21
+
16
22
  ctx: HttpContext,
17
23
  /**
18
24
  * ClearRequest instance
19
25
  */
26
+
20
27
  req: ClearRequest) => any | Promise<any>;
21
28
  /**
22
29
  * Handler can be either a function or controller reference
package/dist/index.d.mts CHANGED
@@ -2,10 +2,15 @@ import { NextFunction, Request, Response as Response$1 } from "express";
2
2
  import { H3Event, Middleware as Middleware$1 } from "h3";
3
3
 
4
4
  //#region types/h3.d.ts
5
+ type HttpRequest = H3Event['req'] & {
6
+ getBody: () => Record<string, any>;
7
+ };
5
8
  /**
6
9
  * HTTP context passed to route handlers
7
10
  */
8
- type HttpContext = H3Event & {};
11
+ type HttpContext = Omit<H3Event, 'req'> & {
12
+ req: HttpRequest;
13
+ };
9
14
  /**
10
15
  * Route handler function type
11
16
  */
@@ -13,10 +18,12 @@ type RouteHandler = (
13
18
  /**
14
19
  * H3 event context
15
20
  */
21
+
16
22
  ctx: HttpContext,
17
23
  /**
18
24
  * ClearRequest instance
19
25
  */
26
+
20
27
  req: ClearRequest) => any | Promise<any>;
21
28
  /**
22
29
  * Handler can be either a function or controller reference
@@ -19,5 +19,16 @@ type ControllerAction = 'index' | 'show' | 'create' | 'update' | 'destroy';
19
19
  */
20
20
  type RequestData = Record<string, any>;
21
21
  type ApiResourceMiddleware<M extends Middleware$1 | Middleware> = M | M[] | { [K in ControllerAction]?: M | M[] };
22
+ interface RouterConfig {
23
+ /**
24
+ * Configuration for method override functionality, allowing clients to use a
25
+ * specific header or body parameter to override the HTTP method.
26
+ */
27
+ methodOverride?: {
28
+ /** Whether method override is enabled */enabled?: boolean; /** Keys in the request body to check for method override */
29
+ bodyKeys?: string[] | string; /** Keys in the request headers to check for method override */
30
+ headerKeys?: string[] | string;
31
+ };
32
+ }
22
33
  //#endregion
23
- export { ApiResourceMiddleware, ControllerAction, ControllerHandler, HttpMethod, RequestData };
34
+ export { ApiResourceMiddleware, ControllerAction, ControllerHandler, HttpMethod, RequestData, RouterConfig };
@@ -3,11 +3,14 @@ import { ControllerHandler } from "./basic.mjs";
3
3
  import { NextFunction, Request, Response } from "express";
4
4
 
5
5
  //#region types/express.d.ts
6
+ interface RequestWithGetBody extends Request {
7
+ getBody: () => Record<string, any>;
8
+ }
6
9
  /**
7
10
  * HTTP context passed to route handlers
8
11
  */
9
12
  interface HttpContext {
10
- req: Request;
13
+ req: RequestWithGetBody;
11
14
  res: Response;
12
15
  next: NextFunction;
13
16
  }
@@ -34,4 +37,4 @@ type Handler = RouteHandler | ControllerHandler;
34
37
  */
35
38
  type Middleware = (req: Request, res: Response, next: NextFunction) => any | Promise<any>;
36
39
  //#endregion
37
- export { Handler, HttpContext, Middleware, RouteHandler };
40
+ export { Handler, HttpContext, Middleware, RequestWithGetBody, RouteHandler };
@@ -7,10 +7,15 @@ type H3App = Omit<H3['fetch'], 'fetch'> & {
7
7
  fetch: (request: TypedServerRequest) => Promise<Response>;
8
8
  };
9
9
  type MaybePromise<T = unknown> = T | Promise<T>;
10
+ type HttpRequest = H3Event['req'] & {
11
+ getBody: () => Record<string, any>;
12
+ };
10
13
  /**
11
14
  * HTTP context passed to route handlers
12
15
  */
13
- type HttpContext = H3Event & {};
16
+ type HttpContext = Omit<H3Event, 'req'> & {
17
+ req: HttpRequest;
18
+ };
14
19
  /**
15
20
  * Route handler function type
16
21
  */
@@ -31,4 +36,4 @@ req: ClearRequest) => any | Promise<any>;
31
36
  type Handler = RouteHandler | ControllerHandler;
32
37
  type NextFunction = () => MaybePromise<unknown | undefined>;
33
38
  //#endregion
34
- export { H3App, Handler, HttpContext, MaybePromise, type Middleware, NextFunction, RouteHandler };
39
+ export { H3App, Handler, HttpContext, HttpRequest, MaybePromise, type Middleware, NextFunction, RouteHandler };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-router",
3
- "version": "2.1.10",
3
+ "version": "2.1.12",
4
4
  "description": "Laravel-style routing system for Express.js and H3, with CommonJS, ESM, and TypeScript support",
5
5
  "keywords": [
6
6
  "h3",
@@ -80,6 +80,7 @@
80
80
  "typescript": "^5.3.3",
81
81
  "typescript-eslint": "^8.56.1",
82
82
  "vite-tsconfig-paths": "^6.1.1",
83
+ "vitepress": "^1.6.4",
83
84
  "vitest": "^4.0.18"
84
85
  },
85
86
  "engines": {
@@ -88,6 +89,9 @@
88
89
  "scripts": {
89
90
  "test": "vitest",
90
91
  "lint": "eslint",
92
+ "docs:dev": "vitepress dev docs",
93
+ "docs:build": "vitepress build docs",
94
+ "docs:preview": "vitepress preview docs",
91
95
  "test:esm": "vitest tests/esm.test.ts",
92
96
  "test:ts": "vitest tests/typescript.test.ts",
93
97
  "test:coverage": "vitest run --coverage",