express-version-api 1.0.1 → 1.0.6

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/LICENSE CHANGED
@@ -1,21 +1,23 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 Braian Quintian
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ # MIT License
2
+
3
+ Copyright (c) 2025 Braian Quintian
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ For more information, see <https://github.com/Braian-Quintian/express-version-api>
package/README.md CHANGED
@@ -1,146 +1,162 @@
1
- # Express version api
2
-
3
- [![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/Braian-Quintian/express-version-routes?tab=MIT-1-ov-file#readme)
4
-
5
- Express version api is a simple library that allows you to create versioned routes in your Express.js application. It provides a middleware function that allows you to define different route handlers based on the requested API version. This can be useful when you need to maintain multiple versions of an API and serve different responses based on the version number specified by the client.
6
-
7
- **🛑Note:** This library is intended for personal use and experimentation, and is **_not recommended_** for production environments or widespread adoption.
8
-
9
- # 🚧 Under Development 🚧
10
-
11
- **This library is actively being developed. Improvements and new features are added regularly. As such, there's no stable version and changes are frequent. Please consider this when using the library.**
12
-
13
- ## Installation
14
-
15
- ```bash
16
- npm install express-version-api
17
- ```
18
-
19
- ## Disclaimer
20
-
21
- **⚠️ Disclaimer: This library is intended for personal use and experimentation. While it is released under the MIT license, allowing anyone to use and modify it, please note the following considerations:**
22
-
23
- - **Not Recommended for Production:** This library is designed for educational and personal use. It is not optimized for production environments or widespread adoption. Use in production settings is discouraged.
24
-
25
- - **Limited Support and Maintenance:** As this is a personal project, there may be limited ongoing support or maintenance. Contributors are welcome, but please manage expectations accordingly.
26
-
27
- - **Not a Replacement for Established Libraries:** This library is not intended to replace established or widely used libraries for API versioning in production applications. Consider using dedicated solutions with robust community support for production environments.
28
-
29
- ---
30
-
31
- ## Usage
32
-
33
- > You can use the `versionApi` middleware to create versioned routes in your Express.js application. The `versionApi` middleware accepts an object with the following properties:
34
-
35
- - `version` (string): The version of the API.
36
- - `function` (function): The function that will be called when the route is accessed.
37
-
38
- > Follows semver versioning format. Supports '^, ~' symbols for matching version numbers.
39
-
40
- ### Example basic:
41
-
42
- ```javascript
43
- const express = require("express");
44
- const versionApi = require("express-version-api");
45
- const app = express();
46
-
47
- // Define your route with versioned handlers
48
- app.get(
49
- "/api",
50
- versionApi({
51
- "1.0.0": functionV1, // Route handler for API version 1.0.0
52
- "2.0.0": functionV2, // Route handler for API version 2.0.0
53
- })
54
- );
55
-
56
- // Handler for version 1.0.0
57
- function functionV1(req, res) {
58
- res.send("This is version 1.0.0"); // Sends a response for API version 1.0.0
59
- }
60
-
61
- // Handler for version 2.0.0
62
- function functionV2(req, res) {
63
- res.send("This is version 2.0.0"); // Sends a response for API version 2.0.0
64
- }
65
-
66
- app.listen(3000, () => {
67
- console.log("Server is running on port 3000");
68
- });
69
- ```
70
-
71
- ### Example using '^, ~' semver symbols:
72
-
73
- > The `^` symbol is used to match the specified version and any minor or patch updates. The `~` symbol is used to match the specified version and any patch updates.
74
-
75
- **Caret Operator** **_(^)_**: Matches the specified version and any minor or patch updates. For example, `^1.0.0` will match `1.0.0`, `1.1.0`, `1.1.1`, etc., but not `2.0.0`.
76
-
77
- **Tilde Operator** **_(~)_**: Matches the specified version and any patch updates. For example, ~1.0.0 will match `1.0.0`, `1.0.1`, `1.0.2`, etc., but not `1.1.0`.
78
-
79
- ```javascript
80
- const express = require("express");
81
- const versionApi = require("express-version-api");
82
- const app = express();
83
-
84
- // Define your route with versioned handlers
85
- app.get(
86
- "/api",
87
- versionApi({
88
- "^1.0.0": functionV1, // Route handler for API version 1.X.X Caret Operator
89
- "~2.0.0": functionV2, // Route handler for API version 2.0.X Tilde Operator
90
- })
91
- );
92
-
93
- // Handler for version 1.0.0
94
- function functionV1(req, res) {
95
- res.send("This is version 1.0.0"); // Sends a response for API version 1.X.X
96
- }
97
-
98
- // Handler for version 2.0.0
99
- function functionV2(req, res) {
100
- res.send("This is version 2.0.0"); // Sends a response for API version 2.0.X
101
- }
102
-
103
- app.listen(3000, () => {
104
- console.log("Server is running on port 3000");
105
- });
106
- ```
107
-
108
- ### Versioning via Headers
109
-
110
- To specify the desired API version, clients should include the `Accept-Version` header in their requests with the version number they want to access. Here's an example of how to set the Accept-Version header using curl:
111
-
112
- ```bash
113
- curl -H "Accept-Version: 1.0.0" http://localhost:3000/api
114
- ```
115
-
116
- In this example, the server will invoke functionV1 when the request specifies Accept-Version: 1.0.0 and functionV2 when Accept-Version: 2.0.0 is specified.
117
-
118
- Make sure to handle the Accept-Version header appropriately in your middleware or route handlers to serve the correct API version based on client preferences.
119
-
120
- ## Running Tests
121
-
122
- This library uses Jest for testing. To run the tests, you can use the following command:
123
-
124
- ```bash
125
- npm run test
126
- ```
127
-
128
- ---
129
-
130
- ## Roadmap
131
-
132
- - Added support for semver features🌟
133
- - Enhanced support for APIs starting with v1🌟
134
- - Add more integrations and examples🌟
135
-
136
- ### Development Status
137
-
138
- This library is currently under active development. We are continuously working on improving and adding new features to enhance its functionality.
139
-
140
- ## Contributing
141
-
142
- We welcome contributions! If you're interested in improving this library or adding new features, we'd love to receive your contributions.
143
-
144
- ## Feedback
145
-
146
- If you have any feedback, please reach out to us at bquintian.developer@gmail.com
1
+ # 📦 express-version-api
2
+
3
+ [![npm version](https://img.shields.io/npm/v/express-version-api.svg)](https://www.npmjs.com/package/express-version-api)
4
+ [![Build Status](https://img.shields.io/github/actions/workflow/status/Braian-Quintian/express-version-api/test.yml)](https://github.com/Braian-Quintian/express-version-api/actions)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE)
6
+ [![Coverage Status](https://img.shields.io/codecov/c/github/Braian-Quintian/express-version-api)](https://codecov.io/gh/Braian-Quintian/express-version-api)
7
+ [![Node.js](https://img.shields.io/badge/node-%3E=14.0.0-brightgreen)](https://nodejs.org)
8
+
9
+ > Versioned routing middleware for Express.js based on semantic versioning (`semver`).
10
+
11
+ ---
12
+
13
+ **⚠️ NOTE:** This package is intended for personal use and experimentation. It is **not recommended** for production use.
14
+
15
+ ---
16
+
17
+ ## 🚧 Project Status
18
+
19
+ This library is under **active development**. Features and improvements are being added frequently. Use at your own discretion in unstable environments.
20
+
21
+ ---
22
+
23
+ ## ✨ Features
24
+
25
+ - ✅ Version-based routing (`^`, `~`, exact versions)
26
+ - ✅ Automatic fallback to default or latest version
27
+ - ✅ Fully compatible with Express middleware
28
+ - ✅ Well-tested with Jest and Supertest
29
+
30
+ ---
31
+
32
+ ## 📦 Installation
33
+
34
+ ```bash
35
+ npm install express-version-api
36
+ ```
37
+
38
+ ---
39
+
40
+ ## 🚀 Quick Usage
41
+
42
+ ```js
43
+ const express = require("express");
44
+ const versionApi = require("express-version-api");
45
+ const app = express();
46
+
47
+ const handlerV1 = (req, res) => res.send("This is version 1.0.0");
48
+ const handlerV2 = (req, res) => res.send("This is version 2.0.0");
49
+
50
+ app.get(
51
+ "/api",
52
+ versionApi({
53
+ "1.0.0": handlerV1,
54
+ "2.0.0": handlerV2,
55
+ })
56
+ );
57
+
58
+ app.listen(3000, () => {
59
+ console.log("Server running on port 3000");
60
+ });
61
+ ```
62
+
63
+ ---
64
+
65
+ ## 🎯 Semver Support (`^`, `~`)
66
+
67
+ | Symbol | Matches | Example Matches |
68
+ | ------ | -------------------------- | ------------------ |
69
+ | `^` | Major-compatible (`1.x.x`) | `^1.0.0` → `1.2.3` |
70
+ | `~` | Minor-compatible (`2.1.x`) | `~2.1.0` → `2.1.4` |
71
+ | Exact | Exact version only | `3.0.0` → `3.0.0` |
72
+
73
+ ```js
74
+ app.get(
75
+ "/api",
76
+ versionApi({
77
+ "^1.0.0": handlerV1,
78
+ "~2.1.0": handlerV2,
79
+ "3.0.0": handlerV3,
80
+ })
81
+ );
82
+ ```
83
+
84
+ ---
85
+
86
+ ## 📥 Version Header
87
+
88
+ Clients should include the `Accept-Version` HTTP header in requests:
89
+
90
+ ```bash
91
+ curl -H "Accept-Version: 1.0.0" http://localhost:3000/api
92
+ ```
93
+
94
+ ---
95
+
96
+ ## 🧪 Running Tests
97
+
98
+ ```bash
99
+ npm run test
100
+ ```
101
+
102
+ Tests are powered by [Jest](https://jestjs.io/) and [Supertest](https://github.com/visionmedia/supertest). Full coverage is included.
103
+
104
+ ---
105
+
106
+ ## 🧩 API
107
+
108
+ ### `versionApi(handlers, defaultHandler?)`
109
+
110
+ - `handlers`: Object with semver-style version strings as keys and Express handlers as values.
111
+ - `defaultHandler`: Optional fallback if no version matches.
112
+
113
+ ```js
114
+ app.get(
115
+ "/api",
116
+ versionApi({
117
+ "^1.0.0": v1Handler,
118
+ "~2.0.0": v2Handler,
119
+ "3.0.0": v3Handler,
120
+ }, fallbackHandler)
121
+ );
122
+ ```
123
+
124
+ ---
125
+
126
+ ## 🔄 Fallback Strategy
127
+
128
+ If the version is not matched:
129
+
130
+ 1. Use `defaultHandler` if defined
131
+ 2. Otherwise, fallback to the latest available handler
132
+ 3. If no handler matches, return `422 Unprocessable Entity`
133
+
134
+ ---
135
+
136
+ ## 📚 Examples
137
+
138
+ Check the `test/` directory for integration examples and how `^`, `~` and fallbacks work in practice.
139
+
140
+ ---
141
+
142
+ ## 🛣️ Roadmap
143
+
144
+ - [x] Basic versioning with `^`, `~`, exact
145
+ - [x] Default and latest fallback
146
+ - [ ] Advanced semver range support (planned)
147
+ - [ ] Improved validation and DX
148
+ - [ ] Full ESM and CJS dual package
149
+ - [ ] Typed handler inference with TypeScript
150
+
151
+ ---
152
+
153
+ ## 🤝 Contributing
154
+
155
+ Contributions, issues, and feature requests are welcome!
156
+ Contact: [bquintian.developer@gmail.com](mailto:bquintian.developer@gmail.com)
157
+
158
+ ---
159
+
160
+ ## 🛡 License
161
+
162
+ This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = void 0;
7
+ // Main entry point of the library.
8
+ // Re-exports the default export from lib.ts to support both ESM and CommonJS.
9
+ var lib_1 = require("./lib");
10
+ Object.defineProperty(exports, "default", { enumerable: true, get: function () { return __importDefault(lib_1).default; } });
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;AAAA,mCAAmC;AACnC,8EAA8E;AAC9E,6BAAgC;AAAvB,+GAAA,OAAO,OAAA"}
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findLatestVersion = exports.versioningMiddleware = exports.executeDefaultHandler = exports.executeVersionHandler = exports.extractVersionFromRequest = void 0;
4
+ /**
5
+ * Attempts to extract a semantic version string from an Express request.
6
+ *
7
+ * This function checks the `Request` object for a version identifier in the following order:
8
+ * 1. `req.version` — typically set by middleware.
9
+ * 2. `req.headers['accept-version']` — often provided by clients via HTTP headers.
10
+ *
11
+ * If no version is found, it returns `false`.
12
+ *
13
+ * @param req - The Express request object.
14
+ * @returns The version string (e.g., "1.0.0"), or `false` if none is provided.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const version = extractVersionFromRequest(req);
19
+ * if (!version) {
20
+ * res.status(422).send("No version provided");
21
+ * }
22
+ * ```
23
+ */
24
+ const extractVersionFromRequest = (req) => {
25
+ const sources = [req.version, req.headers["accept-version"]];
26
+ for (const source of sources) {
27
+ if (typeof source === "string")
28
+ return source;
29
+ }
30
+ return false;
31
+ };
32
+ exports.extractVersionFromRequest = extractVersionFromRequest;
33
+ /**
34
+ * Executes a version handler if the client version matches the handler's semantic key.
35
+ *
36
+ * Determines compatibility between the given `handlerKey` (e.g., "^1", "~2.1", "3.0.0")
37
+ * and the `clientVersion` (e.g., "2.1.3") using simplified semantic version matching:
38
+ *
39
+ * - `^1` matches any version starting with "1" (e.g., "1.0.0", "1.9.5")
40
+ * - `~2.1` matches versions like "2.1.x" (e.g., "2.1.0", "2.1.9")
41
+ * - `"3.0.0"` matches exactly "3.0.0"
42
+ *
43
+ * If a match is found, the associated handler from `versionHandlers` is invoked.
44
+ *
45
+ * @param handlerKey - The version key used for comparison (e.g., "^1", "~2.1", "3.0.0").
46
+ * @param clientVersion - The version string from the client request (e.g., "2.1.3").
47
+ * @param versionHandlers - A mapping of version keys to Express request handlers.
48
+ * @param req - The Express request object.
49
+ * @param res - The Express response object.
50
+ * @param next - The Express next middleware function.
51
+ * @returns `true` if a compatible handler was executed, `false` otherwise.
52
+ */
53
+ const executeVersionHandler = (handlerKey, clientVersion, versionHandlers, req, res, next) => {
54
+ const [clientMajor = "0", clientMinor = "0", clientPatch = "0"] = clientVersion.split(".");
55
+ let targetVersion;
56
+ let requestedVersion;
57
+ if (handlerKey.startsWith("~")) {
58
+ // "~2.1" → match major.minor
59
+ targetVersion = handlerKey.slice(1).split(".").slice(0, 2).join(".");
60
+ requestedVersion = `${clientMajor}.${clientMinor}`;
61
+ }
62
+ else if (handlerKey.startsWith("^")) {
63
+ // "^1" → match major only
64
+ targetVersion = handlerKey.slice(1).split(".")[0];
65
+ requestedVersion = clientMajor;
66
+ }
67
+ else {
68
+ // Exact match: "2.1.3"
69
+ targetVersion = handlerKey;
70
+ requestedVersion = `${clientMajor}.${clientMinor}.${clientPatch}`;
71
+ }
72
+ const handler = versionHandlers[handlerKey];
73
+ if (requestedVersion === targetVersion && typeof handler === "function") {
74
+ void handler(req, res, next);
75
+ return true;
76
+ }
77
+ return false;
78
+ };
79
+ exports.executeVersionHandler = executeVersionHandler;
80
+ /**
81
+ * Executes the default handler when no version match is found.
82
+ *
83
+ * Priority order:
84
+ * 1. If a `defaultHandler` is provided, it is invoked immediately.
85
+ * 2. Otherwise, it tries to find and execute the handler for the latest version available.
86
+ * 3. If none is found, it responds with HTTP 422 and an optional custom error message.
87
+ *
88
+ * This ensures that version-based routing gracefully degrades to a default behavior
89
+ * or returns an informative error if no handler can be determined.
90
+ *
91
+ * @param defaultHandler - Optional fallback Express handler.
92
+ * @param versionHandlers - A map of version keys to handlers.
93
+ * @param keys - An array of version keys from the versionHandlers object.
94
+ * @param req - Express request object.
95
+ * @param res - Express response object.
96
+ * @param next - Express next middleware function.
97
+ * @param notFoundMessage - Optional custom error message for 422 responses.
98
+ */
99
+ const executeDefaultHandler = (defaultHandler, versionHandlers, keys, req, res, next, notFoundMessage = "Unprocessable Entity: No valid version handler available.") => {
100
+ if (defaultHandler) {
101
+ void defaultHandler(req, res, next);
102
+ return;
103
+ }
104
+ if (keys.length === 0) {
105
+ res.status(422).send(notFoundMessage);
106
+ return;
107
+ }
108
+ const latestVersionKey = (0, exports.findLatestVersion)(keys);
109
+ const fallbackHandler = latestVersionKey ? versionHandlers[latestVersionKey] : undefined;
110
+ if (fallbackHandler) {
111
+ void fallbackHandler(req, res, next);
112
+ }
113
+ else {
114
+ res.status(422).send(notFoundMessage);
115
+ }
116
+ };
117
+ exports.executeDefaultHandler = executeDefaultHandler;
118
+ /**
119
+ * Middleware for handling version-based routing in Express.
120
+ *
121
+ * This middleware inspects the incoming request for a semantic version (e.g., "1.0.0", "~2.1", "^3")
122
+ * and delegates the request to the appropriate handler defined in `versionHandlers`.
123
+ *
124
+ * Matching priority:
125
+ * 1. If a version is found in the request, attempt to match a handler.
126
+ * 2. If no match is found, invoke the `defaultHandler` (if provided).
127
+ * 3. If no default handler exists, fall back to the latest version handler.
128
+ * 4. If none is available, respond with HTTP 422.
129
+ *
130
+ * @param versionHandlers - A map of semantic version keys to Express handlers.
131
+ * @param defaultHandler - Optional fallback handler if no version matches.
132
+ * @returns An Express middleware function.
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * app.use(
137
+ * versioningMiddleware({
138
+ * "^1": handlerForV1,
139
+ * "~2.1": handlerForV2_1,
140
+ * "3.0.0": handlerForV3
141
+ * })
142
+ * );
143
+ * ```
144
+ */
145
+ const versioningMiddleware = (versionHandlers, defaultHandler) => {
146
+ if (!versionHandlers ||
147
+ typeof versionHandlers !== "object" ||
148
+ Array.isArray(versionHandlers)) {
149
+ throw new Error("Invalid argument: 'versionHandlers' must be a non-array object.");
150
+ }
151
+ return (req, res, next) => {
152
+ const clientVersion = (0, exports.extractVersionFromRequest)(req);
153
+ const handlerKeys = Object.keys(versionHandlers);
154
+ // No version provided by the client
155
+ if (!clientVersion) {
156
+ res.status(422).send("Unprocessable Entity: No version provided.");
157
+ return;
158
+ }
159
+ // Try to match a handler by version
160
+ for (const handlerKey of handlerKeys) {
161
+ const matched = (0, exports.executeVersionHandler)(handlerKey, clientVersion, versionHandlers, req, res, next);
162
+ if (matched)
163
+ return;
164
+ }
165
+ // Fallback to default or latest handler
166
+ (0, exports.executeDefaultHandler)(defaultHandler, versionHandlers, handlerKeys, req, res, next);
167
+ };
168
+ };
169
+ exports.versioningMiddleware = versioningMiddleware;
170
+ /**
171
+ * Determines the latest semantic version from a list of version strings.
172
+ *
173
+ * This function removes any leading semantic operators (`^`, `~`) and
174
+ * compares versions based on major, minor, and patch numbers.
175
+ *
176
+ * @param versions - An array of version keys, possibly including "^" or "~" prefixes.
177
+ * @returns The latest normalized version string (e.g. "3.2.0"), or `null` if the array is empty.
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * const latest = findLatestVersion(["^1", "~2.3", "3.0.0"]);
182
+ * latest === "3.0.0"
183
+ * ```
184
+ */
185
+ const findLatestVersion = (versions) => {
186
+ if (!Array.isArray(versions) || versions.length === 0) {
187
+ return null;
188
+ }
189
+ const normalize = (version) => {
190
+ const cleaned = version.replace(/^[\^~]/, "");
191
+ const parts = cleaned.split(".").map((n) => parseInt(n, 10));
192
+ while (parts.length < 3)
193
+ parts.push(0); // pad missing parts with 0
194
+ return parts.slice(0, 3);
195
+ };
196
+ const sorted = [...versions].sort((a, b) => {
197
+ const [aMajor, aMinor, aPatch] = normalize(a);
198
+ const [bMajor, bMinor, bPatch] = normalize(b);
199
+ if (aMajor !== bMajor)
200
+ return bMajor - aMajor;
201
+ if (aMinor !== bMinor)
202
+ return bMinor - aMinor;
203
+ return bPatch - aPatch;
204
+ });
205
+ return sorted.length > 0 ? sorted[0].replace(/^[\^~]/, "") : null;
206
+ };
207
+ exports.findLatestVersion = findLatestVersion;
208
+ exports.default = exports.versioningMiddleware;
209
+ //# sourceMappingURL=lib.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lib.js","sourceRoot":"","sources":["../../src/lib.ts"],"names":[],"mappings":";;;AAiCA;;;;;;;;;;;;;;;;;;;GAmBG;AACI,MAAM,yBAAyB,GAAG,CAAC,GAAqB,EAAkB,EAAE;IACjF,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC;IAE7D,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO,MAAM,CAAC;IAChD,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AARW,QAAA,yBAAyB,6BAQpC;AACF;;;;;;;;;;;;;;;;;;;GAmBG;AACI,MAAM,qBAAqB,GAAG,CACnC,UAAkB,EAClB,aAAqB,EACrB,eAAgC,EAChC,GAAY,EACZ,GAAa,EACb,IAAkB,EACT,EAAE;IACX,MAAM,CAAC,WAAW,GAAG,GAAG,EAAE,WAAW,GAAG,GAAG,EAAE,WAAW,GAAG,GAAG,CAAC,GAAG,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3F,IAAI,aAAqB,CAAC;IAC1B,IAAI,gBAAwB,CAAC;IAE7B,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/B,6BAA6B;QAC7B,aAAa,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACrE,gBAAgB,GAAG,GAAG,WAAW,IAAI,WAAW,EAAE,CAAC;IACrD,CAAC;SAAM,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACtC,0BAA0B;QAC1B,aAAa,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAClD,gBAAgB,GAAG,WAAW,CAAC;IACjC,CAAC;SAAM,CAAC;QACN,uBAAuB;QACvB,aAAa,GAAG,UAAU,CAAC;QAC3B,gBAAgB,GAAG,GAAG,WAAW,IAAI,WAAW,IAAI,WAAW,EAAE,CAAC;IACpE,CAAC;IAED,MAAM,OAAO,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IAE5C,IAAI,gBAAgB,KAAK,aAAa,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE,CAAC;QACxE,KAAK,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;QAC7B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAlCW,QAAA,qBAAqB,yBAkChC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACI,MAAM,qBAAqB,GAAG,CACnC,cAA0C,EAC1C,eAAgC,EAChC,IAAc,EACd,GAAY,EACZ,GAAa,EACb,IAAkB,EAClB,eAAe,GAAG,2DAA2D,EACvE,EAAE;IACR,IAAI,cAAc,EAAE,CAAC;QACnB,KAAK,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;QACpC,OAAO;IACT,CAAC;IAED,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QACtC,OAAO;IACT,CAAC;IAED,MAAM,gBAAgB,GAAG,IAAA,yBAAiB,EAAC,IAAI,CAAC,CAAC;IACjD,MAAM,eAAe,GAAG,gBAAgB,CAAC,CAAC,CAAC,eAAe,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAEzF,IAAI,eAAe,EAAE,CAAC;QACpB,KAAK,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IACvC,CAAC;SAAM,CAAC;QACN,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IACxC,CAAC;AACH,CAAC,CAAC;AA3BW,QAAA,qBAAqB,yBA2BhC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACI,MAAM,oBAAoB,GAAG,CAClC,eAAgC,EAChC,cAA+B,EACf,EAAE;IAClB,IACE,CAAC,eAAe;QAChB,OAAO,eAAe,KAAK,QAAQ;QACnC,KAAK,CAAC,OAAO,CAAC,eAAe,CAAC,EAC9B,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,iEAAiE,CAAC,CAAC;IACrF,CAAC;IAED,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACxB,MAAM,aAAa,GAAG,IAAA,iCAAyB,EAAC,GAAG,CAAC,CAAC;QACrD,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QAEjD,oCAAoC;QACpC,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,4CAA4C,CAAC,CAAC;YACnE,OAAO;QACT,CAAC;QAED,oCAAoC;QACpC,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;YACrC,MAAM,OAAO,GAAG,IAAA,6BAAqB,EACnC,UAAU,EACV,aAAa,EACb,eAAe,EACf,GAAG,EACH,GAAG,EACH,IAAI,CACL,CAAC;YACF,IAAI,OAAO;gBAAE,OAAO;QACtB,CAAC;QAED,wCAAwC;QACxC,IAAA,6BAAqB,EACnB,cAAc,EACd,eAAe,EACf,WAAW,EACX,GAAG,EACH,GAAG,EACH,IAAI,CACL,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC,CAAC;AA7CW,QAAA,oBAAoB,wBA6C/B;AAEF;;;;;;;;;;;;;;GAcG;AACI,MAAM,iBAAiB,GAAG,CAAC,QAAkB,EAAiB,EAAE;IACrE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,SAAS,GAAG,CAAC,OAAe,EAAY,EAAE;QAC9C,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAC9C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;QAC7D,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,2BAA2B;QACnE,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3B,CAAC,CAAC;IAEF,MAAM,MAAM,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACzC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;QAC9C,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;QAE9C,IAAI,MAAM,KAAK,MAAM;YAAE,OAAO,MAAM,GAAG,MAAM,CAAC;QAC9C,IAAI,MAAM,KAAK,MAAM;YAAE,OAAO,MAAM,GAAG,MAAM,CAAC;QAC9C,OAAO,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACpE,CAAC,CAAC;AAtBW,QAAA,iBAAiB,qBAsB5B;AAEF,kBAAe,4BAAoB,CAAC"}
@@ -0,0 +1,4 @@
1
+ // Main entry point of the library.
2
+ // Re-exports the default export from lib.ts to support both ESM and CommonJS.
3
+ export { default } from "./lib";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,8EAA8E;AAC9E,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC"}
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Attempts to extract a semantic version string from an Express request.
3
+ *
4
+ * This function checks the `Request` object for a version identifier in the following order:
5
+ * 1. `req.version` — typically set by middleware.
6
+ * 2. `req.headers['accept-version']` — often provided by clients via HTTP headers.
7
+ *
8
+ * If no version is found, it returns `false`.
9
+ *
10
+ * @param req - The Express request object.
11
+ * @returns The version string (e.g., "1.0.0"), or `false` if none is provided.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const version = extractVersionFromRequest(req);
16
+ * if (!version) {
17
+ * res.status(422).send("No version provided");
18
+ * }
19
+ * ```
20
+ */
21
+ export const extractVersionFromRequest = (req) => {
22
+ const sources = [req.version, req.headers["accept-version"]];
23
+ for (const source of sources) {
24
+ if (typeof source === "string")
25
+ return source;
26
+ }
27
+ return false;
28
+ };
29
+ /**
30
+ * Executes a version handler if the client version matches the handler's semantic key.
31
+ *
32
+ * Determines compatibility between the given `handlerKey` (e.g., "^1", "~2.1", "3.0.0")
33
+ * and the `clientVersion` (e.g., "2.1.3") using simplified semantic version matching:
34
+ *
35
+ * - `^1` matches any version starting with "1" (e.g., "1.0.0", "1.9.5")
36
+ * - `~2.1` matches versions like "2.1.x" (e.g., "2.1.0", "2.1.9")
37
+ * - `"3.0.0"` matches exactly "3.0.0"
38
+ *
39
+ * If a match is found, the associated handler from `versionHandlers` is invoked.
40
+ *
41
+ * @param handlerKey - The version key used for comparison (e.g., "^1", "~2.1", "3.0.0").
42
+ * @param clientVersion - The version string from the client request (e.g., "2.1.3").
43
+ * @param versionHandlers - A mapping of version keys to Express request handlers.
44
+ * @param req - The Express request object.
45
+ * @param res - The Express response object.
46
+ * @param next - The Express next middleware function.
47
+ * @returns `true` if a compatible handler was executed, `false` otherwise.
48
+ */
49
+ export const executeVersionHandler = (handlerKey, clientVersion, versionHandlers, req, res, next) => {
50
+ const [clientMajor = "0", clientMinor = "0", clientPatch = "0"] = clientVersion.split(".");
51
+ let targetVersion;
52
+ let requestedVersion;
53
+ if (handlerKey.startsWith("~")) {
54
+ // "~2.1" → match major.minor
55
+ targetVersion = handlerKey.slice(1).split(".").slice(0, 2).join(".");
56
+ requestedVersion = `${clientMajor}.${clientMinor}`;
57
+ }
58
+ else if (handlerKey.startsWith("^")) {
59
+ // "^1" → match major only
60
+ targetVersion = handlerKey.slice(1).split(".")[0];
61
+ requestedVersion = clientMajor;
62
+ }
63
+ else {
64
+ // Exact match: "2.1.3"
65
+ targetVersion = handlerKey;
66
+ requestedVersion = `${clientMajor}.${clientMinor}.${clientPatch}`;
67
+ }
68
+ const handler = versionHandlers[handlerKey];
69
+ if (requestedVersion === targetVersion && typeof handler === "function") {
70
+ void handler(req, res, next);
71
+ return true;
72
+ }
73
+ return false;
74
+ };
75
+ /**
76
+ * Executes the default handler when no version match is found.
77
+ *
78
+ * Priority order:
79
+ * 1. If a `defaultHandler` is provided, it is invoked immediately.
80
+ * 2. Otherwise, it tries to find and execute the handler for the latest version available.
81
+ * 3. If none is found, it responds with HTTP 422 and an optional custom error message.
82
+ *
83
+ * This ensures that version-based routing gracefully degrades to a default behavior
84
+ * or returns an informative error if no handler can be determined.
85
+ *
86
+ * @param defaultHandler - Optional fallback Express handler.
87
+ * @param versionHandlers - A map of version keys to handlers.
88
+ * @param keys - An array of version keys from the versionHandlers object.
89
+ * @param req - Express request object.
90
+ * @param res - Express response object.
91
+ * @param next - Express next middleware function.
92
+ * @param notFoundMessage - Optional custom error message for 422 responses.
93
+ */
94
+ export const executeDefaultHandler = (defaultHandler, versionHandlers, keys, req, res, next, notFoundMessage = "Unprocessable Entity: No valid version handler available.") => {
95
+ if (defaultHandler) {
96
+ void defaultHandler(req, res, next);
97
+ return;
98
+ }
99
+ if (keys.length === 0) {
100
+ res.status(422).send(notFoundMessage);
101
+ return;
102
+ }
103
+ const latestVersionKey = findLatestVersion(keys);
104
+ const fallbackHandler = latestVersionKey ? versionHandlers[latestVersionKey] : undefined;
105
+ if (fallbackHandler) {
106
+ void fallbackHandler(req, res, next);
107
+ }
108
+ else {
109
+ res.status(422).send(notFoundMessage);
110
+ }
111
+ };
112
+ /**
113
+ * Middleware for handling version-based routing in Express.
114
+ *
115
+ * This middleware inspects the incoming request for a semantic version (e.g., "1.0.0", "~2.1", "^3")
116
+ * and delegates the request to the appropriate handler defined in `versionHandlers`.
117
+ *
118
+ * Matching priority:
119
+ * 1. If a version is found in the request, attempt to match a handler.
120
+ * 2. If no match is found, invoke the `defaultHandler` (if provided).
121
+ * 3. If no default handler exists, fall back to the latest version handler.
122
+ * 4. If none is available, respond with HTTP 422.
123
+ *
124
+ * @param versionHandlers - A map of semantic version keys to Express handlers.
125
+ * @param defaultHandler - Optional fallback handler if no version matches.
126
+ * @returns An Express middleware function.
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * app.use(
131
+ * versioningMiddleware({
132
+ * "^1": handlerForV1,
133
+ * "~2.1": handlerForV2_1,
134
+ * "3.0.0": handlerForV3
135
+ * })
136
+ * );
137
+ * ```
138
+ */
139
+ export const versioningMiddleware = (versionHandlers, defaultHandler) => {
140
+ if (!versionHandlers ||
141
+ typeof versionHandlers !== "object" ||
142
+ Array.isArray(versionHandlers)) {
143
+ throw new Error("Invalid argument: 'versionHandlers' must be a non-array object.");
144
+ }
145
+ return (req, res, next) => {
146
+ const clientVersion = extractVersionFromRequest(req);
147
+ const handlerKeys = Object.keys(versionHandlers);
148
+ // No version provided by the client
149
+ if (!clientVersion) {
150
+ res.status(422).send("Unprocessable Entity: No version provided.");
151
+ return;
152
+ }
153
+ // Try to match a handler by version
154
+ for (const handlerKey of handlerKeys) {
155
+ const matched = executeVersionHandler(handlerKey, clientVersion, versionHandlers, req, res, next);
156
+ if (matched)
157
+ return;
158
+ }
159
+ // Fallback to default or latest handler
160
+ executeDefaultHandler(defaultHandler, versionHandlers, handlerKeys, req, res, next);
161
+ };
162
+ };
163
+ /**
164
+ * Determines the latest semantic version from a list of version strings.
165
+ *
166
+ * This function removes any leading semantic operators (`^`, `~`) and
167
+ * compares versions based on major, minor, and patch numbers.
168
+ *
169
+ * @param versions - An array of version keys, possibly including "^" or "~" prefixes.
170
+ * @returns The latest normalized version string (e.g. "3.2.0"), or `null` if the array is empty.
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * const latest = findLatestVersion(["^1", "~2.3", "3.0.0"]);
175
+ * latest === "3.0.0"
176
+ * ```
177
+ */
178
+ export const findLatestVersion = (versions) => {
179
+ if (!Array.isArray(versions) || versions.length === 0) {
180
+ return null;
181
+ }
182
+ const normalize = (version) => {
183
+ const cleaned = version.replace(/^[\^~]/, "");
184
+ const parts = cleaned.split(".").map((n) => parseInt(n, 10));
185
+ while (parts.length < 3)
186
+ parts.push(0); // pad missing parts with 0
187
+ return parts.slice(0, 3);
188
+ };
189
+ const sorted = [...versions].sort((a, b) => {
190
+ const [aMajor, aMinor, aPatch] = normalize(a);
191
+ const [bMajor, bMinor, bPatch] = normalize(b);
192
+ if (aMajor !== bMajor)
193
+ return bMajor - aMajor;
194
+ if (aMinor !== bMinor)
195
+ return bMinor - aMinor;
196
+ return bPatch - aPatch;
197
+ });
198
+ return sorted.length > 0 ? sorted[0].replace(/^[\^~]/, "") : null;
199
+ };
200
+ export default versioningMiddleware;
201
+ //# sourceMappingURL=lib.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lib.js","sourceRoot":"","sources":["../../src/lib.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export { default } from "./lib";
@@ -0,0 +1,137 @@
1
+ import { type Request, type Response, type NextFunction, type RequestHandler } from "express";
2
+ /**
3
+ * An object that maps semantic version strings to specific request handlers.
4
+ *
5
+ * This is the core input to the `versioningMiddleware` function. Each key must be a valid
6
+ * version string, such as `"1.0.0"`, `"^1"`, or `"~2.1"`. These prefixes (`^` and `~`) allow
7
+ * matching of major/minor version ranges, similar to how they work in `package.json`.
8
+ *
9
+ * The value for each key is an Express-compatible handler that should respond to requests
10
+ * targeting that version.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const handlers: VersionHandlers = {
15
+ * "^1": (req, res) => res.send("v1.x handler"),
16
+ * "~2.1": (req, res) => res.send("v2.1.x handler"),
17
+ * "3.0.0": (req, res) => res.send("exact v3.0.0 handler"),
18
+ * };
19
+ * ```
20
+ */
21
+ export interface VersionHandlers {
22
+ [version: string]: RequestHandler;
23
+ }
24
+ /**
25
+ * An extended Express `Request` object that optionally includes a `version` property.
26
+ * This allows support for custom middleware.
27
+ */
28
+ interface VersionedRequest extends Request {
29
+ version?: string;
30
+ }
31
+ /**
32
+ * Attempts to extract a semantic version string from an Express request.
33
+ *
34
+ * This function checks the `Request` object for a version identifier in the following order:
35
+ * 1. `req.version` — typically set by middleware.
36
+ * 2. `req.headers['accept-version']` — often provided by clients via HTTP headers.
37
+ *
38
+ * If no version is found, it returns `false`.
39
+ *
40
+ * @param req - The Express request object.
41
+ * @returns The version string (e.g., "1.0.0"), or `false` if none is provided.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * const version = extractVersionFromRequest(req);
46
+ * if (!version) {
47
+ * res.status(422).send("No version provided");
48
+ * }
49
+ * ```
50
+ */
51
+ export declare const extractVersionFromRequest: (req: VersionedRequest) => string | false;
52
+ /**
53
+ * Executes a version handler if the client version matches the handler's semantic key.
54
+ *
55
+ * Determines compatibility between the given `handlerKey` (e.g., "^1", "~2.1", "3.0.0")
56
+ * and the `clientVersion` (e.g., "2.1.3") using simplified semantic version matching:
57
+ *
58
+ * - `^1` matches any version starting with "1" (e.g., "1.0.0", "1.9.5")
59
+ * - `~2.1` matches versions like "2.1.x" (e.g., "2.1.0", "2.1.9")
60
+ * - `"3.0.0"` matches exactly "3.0.0"
61
+ *
62
+ * If a match is found, the associated handler from `versionHandlers` is invoked.
63
+ *
64
+ * @param handlerKey - The version key used for comparison (e.g., "^1", "~2.1", "3.0.0").
65
+ * @param clientVersion - The version string from the client request (e.g., "2.1.3").
66
+ * @param versionHandlers - A mapping of version keys to Express request handlers.
67
+ * @param req - The Express request object.
68
+ * @param res - The Express response object.
69
+ * @param next - The Express next middleware function.
70
+ * @returns `true` if a compatible handler was executed, `false` otherwise.
71
+ */
72
+ export declare const executeVersionHandler: (handlerKey: string, clientVersion: string, versionHandlers: VersionHandlers, req: Request, res: Response, next: NextFunction) => boolean;
73
+ /**
74
+ * Executes the default handler when no version match is found.
75
+ *
76
+ * Priority order:
77
+ * 1. If a `defaultHandler` is provided, it is invoked immediately.
78
+ * 2. Otherwise, it tries to find and execute the handler for the latest version available.
79
+ * 3. If none is found, it responds with HTTP 422 and an optional custom error message.
80
+ *
81
+ * This ensures that version-based routing gracefully degrades to a default behavior
82
+ * or returns an informative error if no handler can be determined.
83
+ *
84
+ * @param defaultHandler - Optional fallback Express handler.
85
+ * @param versionHandlers - A map of version keys to handlers.
86
+ * @param keys - An array of version keys from the versionHandlers object.
87
+ * @param req - Express request object.
88
+ * @param res - Express response object.
89
+ * @param next - Express next middleware function.
90
+ * @param notFoundMessage - Optional custom error message for 422 responses.
91
+ */
92
+ export declare const executeDefaultHandler: (defaultHandler: RequestHandler | undefined, versionHandlers: VersionHandlers, keys: string[], req: Request, res: Response, next: NextFunction, notFoundMessage?: string) => void;
93
+ /**
94
+ * Middleware for handling version-based routing in Express.
95
+ *
96
+ * This middleware inspects the incoming request for a semantic version (e.g., "1.0.0", "~2.1", "^3")
97
+ * and delegates the request to the appropriate handler defined in `versionHandlers`.
98
+ *
99
+ * Matching priority:
100
+ * 1. If a version is found in the request, attempt to match a handler.
101
+ * 2. If no match is found, invoke the `defaultHandler` (if provided).
102
+ * 3. If no default handler exists, fall back to the latest version handler.
103
+ * 4. If none is available, respond with HTTP 422.
104
+ *
105
+ * @param versionHandlers - A map of semantic version keys to Express handlers.
106
+ * @param defaultHandler - Optional fallback handler if no version matches.
107
+ * @returns An Express middleware function.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * app.use(
112
+ * versioningMiddleware({
113
+ * "^1": handlerForV1,
114
+ * "~2.1": handlerForV2_1,
115
+ * "3.0.0": handlerForV3
116
+ * })
117
+ * );
118
+ * ```
119
+ */
120
+ export declare const versioningMiddleware: (versionHandlers: VersionHandlers, defaultHandler?: RequestHandler) => RequestHandler;
121
+ /**
122
+ * Determines the latest semantic version from a list of version strings.
123
+ *
124
+ * This function removes any leading semantic operators (`^`, `~`) and
125
+ * compares versions based on major, minor, and patch numbers.
126
+ *
127
+ * @param versions - An array of version keys, possibly including "^" or "~" prefixes.
128
+ * @returns The latest normalized version string (e.g. "3.2.0"), or `null` if the array is empty.
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * const latest = findLatestVersion(["^1", "~2.3", "3.0.0"]);
133
+ * latest === "3.0.0"
134
+ * ```
135
+ */
136
+ export declare const findLatestVersion: (versions: string[]) => string | null;
137
+ export default versioningMiddleware;
package/package.json CHANGED
@@ -1,41 +1,62 @@
1
1
  {
2
2
  "name": "express-version-api",
3
- "description": "Middleware for versioning routes/APIs in Express.js using ECMAScript 6. Note: This library is intended for personal use and experimentation, and is not recommended for production environments or widespread adoption.",
4
- "version": "1.0.1",
3
+ "version": "1.0.6",
4
+ "description": "Middleware for versioning routes/APIs in Express.js using semantic versioning like ^1.0 or ~1.2.",
5
5
  "author": "Braian Quintian",
6
6
  "license": "MIT",
7
- "homepage": "https://github.com/Braian-Quintian/express-version-routes?tab=readme-ov-file",
7
+ "homepage": "https://github.com/Braian-Quintian/express-version-api#readme",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/Braian-Quintian/express-version-api"
10
+ "url": "git+https://github.com/Braian-Quintian/express-version-api.git"
11
11
  },
12
12
  "bugs": {
13
- "url": "https://github.com/Braian-Quintian/express-version-routes/issues"
13
+ "url": "https://github.com/Braian-Quintian/express-version-api/issues"
14
+ },
15
+ "main": "dist/cjs/index.js",
16
+ "module": "dist/esm/index.js",
17
+ "types": "dist/types/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "require": "./dist/cjs/index.js",
21
+ "import": "./dist/esm/index.js",
22
+ "types": "./dist/types/index.d.ts"
23
+ }
14
24
  },
15
- "main": "dist/index.js",
16
25
  "engines": {
17
26
  "node": ">=20.0.0"
18
27
  },
28
+ "files": [
29
+ "dist"
30
+ ],
19
31
  "scripts": {
20
- "build": "npx tsc",
21
- "test": "jest --verbose"
32
+ "build:esm": "tsc --project tsconfig.esm.json",
33
+ "build:cjs": "tsc --project tsconfig.cjs.json",
34
+ "build": "npm run build:esm && npm run build:cjs",
35
+ "clean": "rm -rf dist",
36
+ "prepare": "npm run clean && npm run build",
37
+ "lint": "eslint src --ext .ts",
38
+ "test": "jest --config jest.config.ts --verbose"
22
39
  },
23
40
  "keywords": [
24
- "routes",
41
+ "express",
42
+ "versioning",
25
43
  "api",
26
- "version",
27
- "versioning"
28
- ],
29
- "files": [
30
- "dist"
44
+ "routes",
45
+ "middleware",
46
+ "semver"
31
47
  ],
32
48
  "devDependencies": {
33
- "@types/express": "^4.17.21",
34
- "@types/jest": "^29.5.12",
35
- "@types/supertest": "^6.0.2",
36
- "express": "^4.19.2",
37
- "jest": "^29.7.0",
38
- "supertest": "^7.0.0",
39
- "ts-jest": "^29.1.2"
49
+ "@eslint/js": "^9.30.1",
50
+ "@types/express": "^5.0.3",
51
+ "@types/jest": "^30.0.0",
52
+ "@types/supertest": "^6.0.3",
53
+ "eslint": "^9.30.1",
54
+ "express": "5.1.0",
55
+ "jest": "^30.0.4",
56
+ "supertest": "^7.1.1",
57
+ "ts-jest": "^29.4.0",
58
+ "ts-node": "^10.9.2",
59
+ "typescript": "^5.8.3",
60
+ "typescript-eslint": "^8.35.1"
40
61
  }
41
62
  }
package/dist/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- import versionApi from "./lib";
2
- export default versionApi;
package/dist/index.js DELETED
@@ -1,8 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const lib_1 = __importDefault(require("./lib"));
7
- module.exports = lib_1.default;
8
- exports.default = lib_1.default;
package/dist/lib.d.ts DELETED
@@ -1,15 +0,0 @@
1
- /**
2
- * Interface for version handlers.
3
- * Each key is a version string and the value is a function to handle requests for that version.
4
- */
5
- interface VersionHandlers {
6
- [key: string]: (req: any, res: any, next: any) => void;
7
- }
8
- /**
9
- * Main middleware that handles version-based routing logic.
10
- * @param {VersionHandlers} versionHandlers - The object containing all version handlers.
11
- * @param {((req: any, res: any, next: any) => void) | undefined} defaultHandler - The default handler function.
12
- * @returns {((req: any, res: any, next: any) => void)} - The middleware function.
13
- */
14
- declare const versioningMiddleware: (versionHandlers: VersionHandlers, defaultHandler?: (req: any, res: any, next: any) => void) => ((req: any, res: any, next: any) => void);
15
- export default versioningMiddleware;
package/dist/lib.js DELETED
@@ -1,147 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- /**
4
- * Extracts the version from the request object.
5
- * @param {any} req - The request object.
6
- * @returns {string | boolean} - The version string if it exists, otherwise false.
7
- */
8
- const extractVersionFromRequest = (req) => {
9
- return req
10
- ? req.version
11
- ? String(req.version)
12
- : req.headers["accept-version"]
13
- : false;
14
- };
15
- /**
16
- * Executes the corresponding version handler if there is a version match.
17
- * @param {string} handlerKey - The key of the handler to execute.
18
- * @param {string} clientVersion - The version from the client's request.
19
- * @param {VersionHandlers} versionHandlers - The object containing all version handlers.
20
- * @param {any} req - The request object.
21
- * @param {any} res - The response object.
22
- * @param {any} next - The next middleware function.
23
- * @returns {boolean} - True if a matching version handler was executed, otherwise false.
24
- */
25
- const executeVersionHandler = (handlerKey, clientVersion, versionHandlers, req, res, next) => {
26
- let tempKey;
27
- let versionArray = clientVersion.split(".");
28
- let tempVersion;
29
- if (handlerKey[0] === "~") {
30
- // Handling versions using '~'
31
- tempKey = handlerKey.substring(1).split(".").slice(0, 2).join(".");
32
- versionArray[1] = versionArray[1] || "0";
33
- tempVersion = versionArray.slice(0, 2).join(".");
34
- }
35
- else if (handlerKey[0] === "^") {
36
- // Handling versions using '^'
37
- tempKey = handlerKey.substring(1).split(".").slice(0, 1).join(".");
38
- tempVersion = versionArray.slice(0, 1).join(".");
39
- }
40
- else {
41
- // Standard version handling
42
- tempKey = handlerKey;
43
- versionArray[1] = versionArray[1] || "0";
44
- versionArray[2] = versionArray[2] || "0";
45
- tempVersion = versionArray.join(".");
46
- }
47
- if (tempKey === tempVersion && versionHandlers[handlerKey]) {
48
- // Call the corresponding version handler if there is a match
49
- versionHandlers[handlerKey].call(this, req, res, next);
50
- return true;
51
- }
52
- return false;
53
- };
54
- /**
55
- * Executes the default handler if no matching version is found.
56
- * @param {((req: any, res: any, next: any) => void) | undefined} defaultHandler - The default handler function.
57
- * @param {VersionHandlers} versionHandlers - The object containing all version handlers.
58
- * @param {string[]} keys - The keys of all version handlers.
59
- * @param {any} req - The request object.
60
- * @param {any} res - The response object.
61
- * @param {any} next - The next middleware function.
62
- */
63
- const executeDefaultHandler = (defaultHandler, versionHandlers, keys, req, res, next) => {
64
- // Check if there is a defaultHandler defined and execute it if it exists
65
- if (defaultHandler) {
66
- defaultHandler(req, res, next);
67
- }
68
- else {
69
- // If there is no defaultHandler, determine the most recent version
70
- const latestVersionKey = findLatestVersion(keys);
71
- // If the latest version is found and there is a handler for that version, execute it
72
- if (latestVersionKey && versionHandlers[latestVersionKey]) {
73
- versionHandlers[latestVersionKey](req, res, next);
74
- }
75
- else {
76
- // If no valid version is found to handle the request, respond with a 422 error
77
- res.status(422).send("Unprocessable Entity: No valid handler found for this request.");
78
- }
79
- }
80
- };
81
- /**
82
- * Main middleware that handles version-based routing logic.
83
- * @param {VersionHandlers} versionHandlers - The object containing all version handlers.
84
- * @param {((req: any, res: any, next: any) => void) | undefined} defaultHandler - The default handler function.
85
- * @returns {((req: any, res: any, next: any) => void)} - The middleware function.
86
- */
87
- const versioningMiddleware = (versionHandlers, defaultHandler) => {
88
- if (!versionHandlers ||
89
- typeof versionHandlers !== "object" ||
90
- Array.isArray(versionHandlers)) {
91
- throw new Error("Invalid argument: 'versionHandlers' must be a non-array object.");
92
- }
93
- else {
94
- return (req, res, next) => {
95
- const clientVersion = extractVersionFromRequest(req);
96
- const keys = Object.keys(versionHandlers);
97
- if (!clientVersion) {
98
- // Execute the default handler if no version is provided
99
- return res.status(422).send("Unprocessable Entity: No version provided.");
100
- }
101
- for (let i = 0; i < keys.length; i++) {
102
- const handlerKey = keys[i];
103
- if (executeVersionHandler(handlerKey, clientVersion, versionHandlers, req, res, next)) {
104
- return;
105
- }
106
- }
107
- // If no matching version is found, execute the default handler
108
- executeDefaultHandler(defaultHandler, versionHandlers, keys, req, res, next);
109
- };
110
- }
111
- };
112
- /**
113
- * Finds the latest version among an array of versions.
114
- * @param {string[]} versions - The array of version strings.
115
- * @returns {string | null} - The latest version string, or null if the array is empty.
116
- */
117
- const findLatestVersion = (versions) => {
118
- // Find the latest version among an array of versions
119
- if (!versions || versions.length === 0) {
120
- return null;
121
- }
122
- const processedVersions = versions.map((version) => {
123
- // Process versions by removing special characters and padding with zeros
124
- const versionArr = version.replace(/[\^~]/g, "").split(".");
125
- for (let i = 0; i < 2; i++) {
126
- versionArr[i] = versionArr[i] || "0";
127
- }
128
- return versionArr;
129
- });
130
- // Sort processed versions to find the latest one
131
- processedVersions.sort((v1Arr, v2Arr) => {
132
- for (let i = 0; i < 2; i++) {
133
- if (Number(v1Arr[i]) > Number(v2Arr[i])) {
134
- return 1;
135
- }
136
- else if (Number(v1Arr[i]) < Number(v2Arr[i])) {
137
- return -1;
138
- }
139
- }
140
- return 0;
141
- });
142
- return processedVersions[processedVersions.length - 1].join(".");
143
- };
144
- // Exporta la función para CommonJS
145
- module.exports = versioningMiddleware;
146
- // Exporta la función como default para ES6 modules
147
- exports.default = versioningMiddleware;