@koa/router 15.1.2 → 15.2.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/README.md CHANGED
@@ -490,6 +490,36 @@ router.get('/files/{/*path}', (ctx) => {
490
490
 
491
491
  **Note:** Custom regex patterns in parameters (`:param(regex)`) are **no longer supported** in v14+ due to path-to-regexp v8. Use validation in handlers or middleware instead.
492
492
 
493
+ **Helper for parameter validation (v14+)** <small>(Added on v15.2)</small>
494
+
495
+ If you want to keep regex-style validation, register a param middleware instead:
496
+
497
+ ```javascript
498
+ import Router, { createParameterValidationMiddleware } from '@koa/router';
499
+
500
+ const uuid =
501
+ /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
502
+
503
+ const validateId = createParameterValidationMiddleware('id', uuid);
504
+
505
+ router.param('id', validateId).get('/role/:id', middleware);
506
+ ```
507
+
508
+ Or validate inline on a specific route:
509
+
510
+ ```javascript
511
+ import Router, { createParameterValidationMiddleware } from '@koa/router';
512
+
513
+ const uuid =
514
+ /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
515
+
516
+ router.get(
517
+ '/role/:id',
518
+ createParameterValidationMiddleware('id', uuid),
519
+ middleware
520
+ );
521
+ ```
522
+
493
523
  ### router.routes()
494
524
 
495
525
  Returns router middleware which dispatches matched routes.
package/dist/index.d.mts CHANGED
@@ -842,4 +842,47 @@ type RouterMethodFunction<StateT = DefaultState, ContextT = DefaultContext> = {
842
842
  */
843
843
  type RouterWithMethods<M extends string, StateT = DefaultState, ContextT = DefaultContext> = RouterInstance<StateT, ContextT> & Record<Lowercase<M>, RouterMethodFunction<StateT, ContextT>>;
844
844
 
845
- export { type AllowedMethodsOptions, type HttpMethod, Layer, type LayerOptions, type MatchResult, Router, type RouterContext, type RouterInstance, type RouterMethodFunction, type RouterMiddleware, type RouterOptions, type RouterOptionsWithMethods, type RouterParameterMiddleware, type RouterWithMethods, type UrlOptions, Router as default };
845
+ /**
846
+ * Parameter validation helpers.
847
+ *
848
+ * These utilities exist to help migrate legacy `:param(regex)` usage from older
849
+ * router/path-to-regexp versions to v14+ (path-to-regexp v8), where inline
850
+ * parameter regexes are no longer supported in route strings.
851
+ */
852
+
853
+ /**
854
+ * Options for createParameterValidationMiddleware helper
855
+ */
856
+ type ParameterValidationOptions = {
857
+ /**
858
+ * HTTP status to use when the value does not match
859
+ * @default 400
860
+ */
861
+ status?: number;
862
+ /**
863
+ * Error message to use when the value does not match
864
+ * @default `Invalid value for parameter "<parameterName>"`
865
+ */
866
+ message?: string;
867
+ /**
868
+ * Whether the error message should be exposed to the client.
869
+ * Passed through to HttpError#expose.
870
+ */
871
+ expose?: boolean;
872
+ /**
873
+ * Optional custom error factory. If provided, it is used
874
+ * instead of the default HttpError.
875
+ */
876
+ createError?: (parameterName: string, value: string) => Error;
877
+ };
878
+ /**
879
+ * Convenience helper to recreate legacy `:param(regex)` validation.
880
+ *
881
+ * @example
882
+ * const validateUuid = createParameterValidationMiddleware('id', uuidRegex);
883
+ * router.param('id', validateUuid).get('/role/:id', handler);
884
+ * router.get('/role/:id', createParameterValidationMiddleware('id', uuidRegex)
885
+ */
886
+ declare function createParameterValidationMiddleware(parameterName: string, pattern: RegExp, options?: ParameterValidationOptions): RouterMiddleware<any, any, any> & RouterParameterMiddleware<any, any, any>;
887
+
888
+ export { type AllowedMethodsOptions, type HttpMethod, Layer, type LayerOptions, type MatchResult, type ParameterValidationOptions, Router, type RouterContext, type RouterInstance, type RouterMethodFunction, type RouterMiddleware, type RouterOptions, type RouterOptionsWithMethods, type RouterParameterMiddleware, type RouterWithMethods, type UrlOptions, createParameterValidationMiddleware, Router as default };
package/dist/index.d.ts CHANGED
@@ -842,4 +842,47 @@ type RouterMethodFunction<StateT = DefaultState, ContextT = DefaultContext> = {
842
842
  */
843
843
  type RouterWithMethods<M extends string, StateT = DefaultState, ContextT = DefaultContext> = RouterInstance<StateT, ContextT> & Record<Lowercase<M>, RouterMethodFunction<StateT, ContextT>>;
844
844
 
845
- export { type AllowedMethodsOptions, type HttpMethod, Layer, type LayerOptions, type MatchResult, Router, type RouterContext, type RouterInstance, type RouterMethodFunction, type RouterMiddleware, type RouterOptions, type RouterOptionsWithMethods, type RouterParameterMiddleware, type RouterWithMethods, type UrlOptions, Router as default };
845
+ /**
846
+ * Parameter validation helpers.
847
+ *
848
+ * These utilities exist to help migrate legacy `:param(regex)` usage from older
849
+ * router/path-to-regexp versions to v14+ (path-to-regexp v8), where inline
850
+ * parameter regexes are no longer supported in route strings.
851
+ */
852
+
853
+ /**
854
+ * Options for createParameterValidationMiddleware helper
855
+ */
856
+ type ParameterValidationOptions = {
857
+ /**
858
+ * HTTP status to use when the value does not match
859
+ * @default 400
860
+ */
861
+ status?: number;
862
+ /**
863
+ * Error message to use when the value does not match
864
+ * @default `Invalid value for parameter "<parameterName>"`
865
+ */
866
+ message?: string;
867
+ /**
868
+ * Whether the error message should be exposed to the client.
869
+ * Passed through to HttpError#expose.
870
+ */
871
+ expose?: boolean;
872
+ /**
873
+ * Optional custom error factory. If provided, it is used
874
+ * instead of the default HttpError.
875
+ */
876
+ createError?: (parameterName: string, value: string) => Error;
877
+ };
878
+ /**
879
+ * Convenience helper to recreate legacy `:param(regex)` validation.
880
+ *
881
+ * @example
882
+ * const validateUuid = createParameterValidationMiddleware('id', uuidRegex);
883
+ * router.param('id', validateUuid).get('/role/:id', handler);
884
+ * router.get('/role/:id', createParameterValidationMiddleware('id', uuidRegex)
885
+ */
886
+ declare function createParameterValidationMiddleware(parameterName: string, pattern: RegExp, options?: ParameterValidationOptions): RouterMiddleware<any, any, any> & RouterParameterMiddleware<any, any, any>;
887
+
888
+ export { type AllowedMethodsOptions, type HttpMethod, Layer, type LayerOptions, type MatchResult, type ParameterValidationOptions, Router, type RouterContext, type RouterInstance, type RouterMethodFunction, type RouterMiddleware, type RouterOptions, type RouterOptionsWithMethods, type RouterParameterMiddleware, type RouterWithMethods, type UrlOptions, createParameterValidationMiddleware, Router as default };
package/dist/index.js CHANGED
@@ -31,13 +31,63 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  Router: () => Router,
34
+ createParameterValidationMiddleware: () => createParameterValidationMiddleware,
34
35
  default: () => router_default
35
36
  });
36
37
  module.exports = __toCommonJS(index_exports);
37
38
 
39
+ // src/utils/parameter-match.ts
40
+ var import_http_errors = __toESM(require("http-errors"));
41
+ function createParameterValidationMiddleware(parameterName, pattern, options = {}) {
42
+ if (!(pattern instanceof RegExp)) {
43
+ throw new TypeError("pattern must be a RegExp instance");
44
+ }
45
+ const matcher = new RegExp(pattern.source, pattern.flags);
46
+ const createDefaultHttpError = (message) => {
47
+ const httpError = (0, import_http_errors.default)(options.status ?? 400, message);
48
+ if (options.expose !== void 0) {
49
+ httpError.expose = options.expose;
50
+ }
51
+ return httpError;
52
+ };
53
+ const validateValue = (value) => {
54
+ if (matcher.global || matcher.sticky) {
55
+ matcher.lastIndex = 0;
56
+ }
57
+ if (matcher.test(value)) {
58
+ return;
59
+ }
60
+ if (options.createError) {
61
+ throw options.createError(parameterName, value);
62
+ }
63
+ throw createDefaultHttpError(
64
+ options.message ?? `Invalid value for parameter "${parameterName}": "${value}"`
65
+ );
66
+ };
67
+ const middleware = async (argument1, argument2, argument3) => {
68
+ if (typeof argument1 !== "string") {
69
+ const context = argument1;
70
+ const next2 = argument2;
71
+ const parameterValue = context.params && parameterName in context.params ? context.params[parameterName] : void 0;
72
+ if (typeof parameterValue !== "string") {
73
+ throw createDefaultHttpError(
74
+ options.message ?? `Missing required parameter "${parameterName}" in route params`
75
+ );
76
+ }
77
+ validateValue(parameterValue);
78
+ return next2();
79
+ }
80
+ const value = argument1;
81
+ const next = argument3;
82
+ validateValue(value);
83
+ return next();
84
+ };
85
+ return middleware;
86
+ }
87
+
38
88
  // src/router.ts
39
89
  var import_koa_compose = __toESM(require("koa-compose"));
40
- var import_http_errors = __toESM(require("http-errors"));
90
+ var import_http_errors2 = __toESM(require("http-errors"));
41
91
 
42
92
  // src/layer.ts
43
93
  var import_node_url = require("url");
@@ -1058,7 +1108,7 @@ var RouterImplementation = class {
1058
1108
  */
1059
1109
  _handleNotImplemented(context, allowedMethodsList, options) {
1060
1110
  if (options.throw) {
1061
- const error = typeof options.notImplemented === "function" ? options.notImplemented() : new import_http_errors.default.NotImplemented();
1111
+ const error = typeof options.notImplemented === "function" ? options.notImplemented() : new import_http_errors2.default.NotImplemented();
1062
1112
  throw error;
1063
1113
  }
1064
1114
  context.status = 501;
@@ -1079,7 +1129,7 @@ var RouterImplementation = class {
1079
1129
  */
1080
1130
  _handleMethodNotAllowed(context, allowedMethodsList, options) {
1081
1131
  if (options.throw) {
1082
- const error = typeof options.methodNotAllowed === "function" ? options.methodNotAllowed() : new import_http_errors.default.MethodNotAllowed();
1132
+ const error = typeof options.methodNotAllowed === "function" ? options.methodNotAllowed() : new import_http_errors2.default.MethodNotAllowed();
1083
1133
  throw error;
1084
1134
  }
1085
1135
  context.status = 405;
@@ -1436,7 +1486,8 @@ for (const httpMethod of httpMethods) {
1436
1486
  }
1437
1487
  // Annotate the CommonJS export names for ESM import in node:
1438
1488
  0 && (module.exports = {
1439
- Router
1489
+ Router,
1490
+ createParameterValidationMiddleware
1440
1491
  });
1441
1492
  if (module.exports.default) {
1442
1493
  Object.assign(module.exports.default, module.exports);
package/dist/index.mjs CHANGED
@@ -1,3 +1,52 @@
1
+ // src/utils/parameter-match.ts
2
+ import createHttpError from "http-errors";
3
+ function createParameterValidationMiddleware(parameterName, pattern, options = {}) {
4
+ if (!(pattern instanceof RegExp)) {
5
+ throw new TypeError("pattern must be a RegExp instance");
6
+ }
7
+ const matcher = new RegExp(pattern.source, pattern.flags);
8
+ const createDefaultHttpError = (message) => {
9
+ const httpError = createHttpError(options.status ?? 400, message);
10
+ if (options.expose !== void 0) {
11
+ httpError.expose = options.expose;
12
+ }
13
+ return httpError;
14
+ };
15
+ const validateValue = (value) => {
16
+ if (matcher.global || matcher.sticky) {
17
+ matcher.lastIndex = 0;
18
+ }
19
+ if (matcher.test(value)) {
20
+ return;
21
+ }
22
+ if (options.createError) {
23
+ throw options.createError(parameterName, value);
24
+ }
25
+ throw createDefaultHttpError(
26
+ options.message ?? `Invalid value for parameter "${parameterName}": "${value}"`
27
+ );
28
+ };
29
+ const middleware = async (argument1, argument2, argument3) => {
30
+ if (typeof argument1 !== "string") {
31
+ const context = argument1;
32
+ const next2 = argument2;
33
+ const parameterValue = context.params && parameterName in context.params ? context.params[parameterName] : void 0;
34
+ if (typeof parameterValue !== "string") {
35
+ throw createDefaultHttpError(
36
+ options.message ?? `Missing required parameter "${parameterName}" in route params`
37
+ );
38
+ }
39
+ validateValue(parameterValue);
40
+ return next2();
41
+ }
42
+ const value = argument1;
43
+ const next = argument3;
44
+ validateValue(value);
45
+ return next();
46
+ };
47
+ return middleware;
48
+ }
49
+
1
50
  // src/router.ts
2
51
  import compose from "koa-compose";
3
52
  import HttpError from "http-errors";
@@ -1399,5 +1448,6 @@ for (const httpMethod of httpMethods) {
1399
1448
  }
1400
1449
  export {
1401
1450
  Router,
1451
+ createParameterValidationMiddleware,
1402
1452
  router_default as default
1403
1453
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@koa/router",
3
3
  "description": "Router middleware for koa. Maintained by Forward Email and Lad.",
4
- "version": "15.1.2",
4
+ "version": "15.2.0",
5
5
  "author": "Alex Mingoia <talk@alexmingoia.com>",
6
6
  "bugs": {
7
7
  "url": "https://github.com/koajs/router/issues",
@@ -110,7 +110,7 @@
110
110
  "test:recipes": "TS_NODE_PROJECT=tsconfig.recipes.json node --require ts-node/register --test recipes/**/*.test.ts",
111
111
  "pretest:all": "npm run lint && npm run build",
112
112
  "test:all": "TS_NODE_PROJECT=tsconfig.ts-node.json node --require ts-node/register --test test/*.test.ts test/**/*.test.ts recipes/**/*.test.ts",
113
- "test:coverage": "c8 npm run test:all",
113
+ "test:coverage": "c8 --all --exclude eslint.config.js --exclude tsup.config.ts --exclude src/types.ts --exclude \"bench/**\" --exclude \"dist/**\" --exclude \"recipes/**\" --exclude \"test/**\" --exclude-after-remap npm run test:all",
114
114
  "ts:check": "tsc --noEmit --project tsconfig.typecheck.json",
115
115
  "ts:check:test": "tsc --noEmit --project tsconfig.test.json",
116
116
  "prebuild": "rimraf dist",