From fcbe44c9bb16ab959fb2d20aadde5399f0055f04 Mon Sep 17 00:00:00 2001 From: Seth Wheeler <23089578+Megapixel99@users.noreply.github.com> Date: Wed, 27 Aug 2025 15:15:07 -0500 Subject: [PATCH 1/7] fix: tweak tests --- test/_moreRoutes.js | 2 +- test/_routes.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/_moreRoutes.js b/test/_moreRoutes.js index 5a8b4fc..3411e3e 100644 --- a/test/_moreRoutes.js +++ b/test/_moreRoutes.js @@ -5,7 +5,7 @@ const oapi = openapi() router.use(oapi) router.get( - '/', + '/:id', oapi.validPath({ summary: 'Get a user.', parameters: [ diff --git a/test/_routes.js b/test/_routes.js index 30ed39f..86e56f7 100644 --- a/test/_routes.js +++ b/test/_routes.js @@ -84,13 +84,13 @@ module.exports = function () { const oapi = openapi() app.use(oapi) - app.use('/:id', _moreRoutes) + app.use('/path', _moreRoutes) supertest(app) .get(`${openapi.defaultRoutePrefix}.json`) .expect(200, (err, res) => { assert(!err, err) - assert.strictEqual(Object.keys((res.body.paths))[0], '/{id}/') + assert.strictEqual(Object.keys((res.body.paths))[0], '/path/{id}') done() }) }) From 59ef9a217b63bb292a372e778599c3694c81d057 Mon Sep 17 00:00:00 2001 From: Seth Wheeler <23089578+Megapixel99@users.noreply.github.com> Date: Fri, 29 Aug 2025 12:22:36 -0500 Subject: [PATCH 2/7] fix: update packages --- package.json | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index cbb4009..bf35b8d 100644 --- a/package.json +++ b/package.json @@ -17,13 +17,11 @@ }, "scripts": { "test": "standard && mocha", - "prepublishOnly": "EXPRESS_MAJOR=4 mocha && EXPRESS_MAJOR=5 mocha", + "prepublishOnly": "EXPRESS_MAJOR=5 mocha", "postpublish": "git push origin && git push origin --tags" }, "devDependencies": { - "express": "^4.18.2", - "express4": "github:expressjs/express#4.19.2", - "express5": "npm:express@^5.0.0-beta.3", + "express": "^5.1.0", "mocha": "^10.3.0", "standard": "^17.1.0", "supertest": "^6.3.4" @@ -33,8 +31,7 @@ "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0", "http-errors": "^2.0.0", - "path-to-regexp": "^6.2.1", - "router": "^1.3.8", + "router": "github:bjohansebas/router#maproutes", "serve-static": "^1.15.0", "swagger-parser": "^10.0.3", "swagger-ui-dist": "^5.11.8", From 1d0277740acb0a13716240dc79901026a11d9be2 Mon Sep 17 00:00:00 2001 From: Seth Wheeler <23089578+Megapixel99@users.noreply.github.com> Date: Fri, 29 Aug 2025 12:22:59 -0500 Subject: [PATCH 3/7] fix: update regex tests to match express 5 regex syntax --- test/_regexRoutes.js | 138 +++++++------------------------------------ 1 file changed, 21 insertions(+), 117 deletions(-) diff --git a/test/_regexRoutes.js b/test/_regexRoutes.js index 9140873..412af07 100644 --- a/test/_regexRoutes.js +++ b/test/_regexRoutes.js @@ -12,39 +12,7 @@ module.exports = function () { const oapi = openapi() app.use(oapi) - app.get('/route/:param*', oapi.path({ - summary: 'Test route.', - responses: { - 200: { - content: { - 'application/json': { - schema: { - type: 'string' - } - } - } - } - } - })) - - supertest(app) - .get(`${openapi.defaultRoutePrefix}.json`) - .expect(200, (err, res) => { - assert(!err, err) - assert.strictEqual(Object.keys((res.body.paths))[0], '/route/{param}') - assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters.length, 2) - assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[0].in, 'path') - assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[0].name, 'param') - done() - }) - }) - - test('serve routes with a * wildcard in parentheses', function (done) { - const app = express() - - const oapi = openapi() - app.use(oapi) - app.get('/route/:param(*)', oapi.path({ + app.get('/route/*param', oapi.path({ summary: 'Test route.', responses: { 200: { @@ -76,7 +44,7 @@ module.exports = function () { const oapi = openapi() app.use(oapi) - app.get(['/route/:param*', '/route/b', '/routeC'], oapi.path({ + app.get(['/route/*param', '/route/b', '/routeC'], oapi.path({ summary: 'Test route.', responses: { 200: { @@ -110,7 +78,7 @@ module.exports = function () { const oapi = openapi() app.use(oapi) - app.get('/route/:param/desc/*', oapi.path({ + app.get('/route/:param/desc/*reg', oapi.path({ summary: 'Test route.', responses: { 200: { @@ -129,10 +97,10 @@ module.exports = function () { .get(`${openapi.defaultRoutePrefix}.json`) .expect(200, (err, res) => { assert(!err, err) - assert.strictEqual(Object.keys((res.body.paths))[0], '/route/{param}/desc/{0}') + assert.strictEqual(Object.keys((res.body.paths))[0], '/route/{param}/desc/{reg}') assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters.length, 2) assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[0].name, 'param') - assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[1].name, 0) + assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[1].name, 'reg') done() }) }) @@ -142,7 +110,7 @@ module.exports = function () { const oapi = openapi() app.use(oapi) - app.get('/*', oapi.path({ + app.get('/*reg', oapi.path({ summary: 'Test route.', responses: { 200: { @@ -161,9 +129,9 @@ module.exports = function () { .get(`${openapi.defaultRoutePrefix}.json`) .expect(200, (err, res) => { assert(!err, err) - assert.strictEqual(Object.keys((res.body.paths))[0], '/{0}') + assert.strictEqual(Object.keys((res.body.paths))[0], '/{reg}') assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters.length, 1) - assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[0].name, 0) + assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[0].name, 'reg') done() }) }) @@ -173,7 +141,7 @@ module.exports = function () { const oapi = openapi() app.use(oapi) - app.get('/route/*/:param', oapi.path({ + app.get('/route/*reg/:param', oapi.path({ summary: 'Test route.', responses: { 200: { @@ -192,9 +160,9 @@ module.exports = function () { .get(`${openapi.defaultRoutePrefix}.json`) .expect(200, (err, res) => { assert(!err, err) - assert.strictEqual(Object.keys((res.body.paths))[0], '/route/{0}/{param}') + assert.strictEqual(Object.keys((res.body.paths))[0], '/route/{reg}/{param}') assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters.length, 2) - assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[0].name, 0) + assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[0].name, 'reg') assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[1].name, 'param') done() }) @@ -205,7 +173,7 @@ module.exports = function () { const oapi = openapi() app.use(oapi) - app.get('/route/:paramA/:paramB/desc/*', oapi.path({ + app.get('/route/:paramA/:paramB/desc/*reg', oapi.path({ summary: 'Test route.', responses: { 200: { @@ -224,11 +192,11 @@ module.exports = function () { .get(`${openapi.defaultRoutePrefix}.json`) .expect(200, (err, res) => { assert(!err, err) - assert.strictEqual(Object.keys((res.body.paths))[0], '/route/{paramA}/{paramB}/desc/{0}') + assert.strictEqual(Object.keys((res.body.paths))[0], '/route/{paramA}/{paramB}/desc/{reg}') assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters.length, 3) assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[0].name, 'paramA') assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[1].name, 'paramB') - assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[2].name, 0) + assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[2].name, 'reg') done() }) }) @@ -238,7 +206,7 @@ module.exports = function () { const oapi = openapi() app.use(oapi) - app.get('/route/:paramA/:paramB/*', oapi.path({ + app.get('/route/:paramA/:paramB/*reg', oapi.path({ summary: 'Test route.', responses: { 200: { @@ -257,21 +225,21 @@ module.exports = function () { .get(`${openapi.defaultRoutePrefix}.json`) .expect(200, (err, res) => { assert(!err, err) - assert.strictEqual(Object.keys((res.body.paths))[0], '/route/{paramA}/{paramB}/{0}') + assert.strictEqual(Object.keys((res.body.paths))[0], '/route/{paramA}/{paramB}/{reg}') assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters.length, 3) assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[0].name, 'paramA') assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[1].name, 'paramB') - assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[2].name, 0) + assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[2].name, 'reg') done() }) }) - test('serve routes with a parameter and another named, grouped, wildcard parameter', function (done) { + test('serve multiple routes which all have a parameter and a wildcard parameter', function (done) { const app = express() const oapi = openapi() app.use(oapi) - app.get('/route/:paramA/:paramB(*)', oapi.path({ + app.get(['/route/:paramA/*paramB', '/cars/:paramA/*paramB'], oapi.path({ summary: 'Test route.', responses: { 200: { @@ -298,44 +266,12 @@ module.exports = function () { }) }) - test('serve multiple routes with a parameter and another named, grouped, wildcard parameter', function (done) { - const app = express() - - const oapi = openapi() - app.use(oapi) - app.get(['/route/:paramA/:paramB(*)', '/cars/:paramA/:paramB(*)'], oapi.path({ - summary: 'Test route.', - responses: { - 200: { - content: { - 'application/json': { - schema: { - type: 'string' - } - } - } - } - } - })) - - supertest(app) - .get(`${openapi.defaultRoutePrefix}.json`) - .expect(200, (err, res) => { - assert(!err, err) - assert.strictEqual(Object.keys((res.body.paths))[0], '/route/{paramA}/{paramB}') - assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters.length, 4) - assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[0].name, 'paramA') - assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[1].name, 'paramB') - done() - }) - }) - test('serve routes with a parameter and another named wildcard parameter', function (done) { const app = express() const oapi = openapi() app.use(oapi) - app.get('/route/:paramA/:paramB*', oapi.path({ + app.get('/route/:paramA/*paramB', oapi.path({ summary: 'Test route.', responses: { 200: { @@ -355,43 +291,11 @@ module.exports = function () { .expect(200, (err, res) => { assert(!err, err) assert.strictEqual(Object.keys((res.body.paths))[0], '/route/{paramA}/{paramB}') - assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters.length, 3) + assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters.length, 2) assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[0].name, 'paramA') assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[1].name, 'paramB') done() }) }) - - test('serve routes with a parameter and a * wildcard', function (done) { - const app = express() - - const oapi = openapi() - app.use(oapi) - app.get('/route/:param/*', oapi.path({ - summary: 'Test route.', - responses: { - 200: { - content: { - 'application/json': { - schema: { - type: 'string' - } - } - } - } - } - })) - - supertest(app) - .get(`${openapi.defaultRoutePrefix}.json`) - .expect(200, (err, res) => { - assert(!err, err) - assert.strictEqual(Object.keys((res.body.paths))[0], '/route/{param}/{0}') - assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters.length, 2) - assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[1].name, 0) - assert.strictEqual(res.body.paths[Object.keys((res.body.paths))[0]].get.parameters[0].name, 'param') - done() - }) - }) }) } From 05f1f678e27fe9e420ce07264c3c83b6ded03f1b Mon Sep 17 00:00:00 2001 From: Seth Wheeler <23089578+Megapixel99@users.noreply.github.com> Date: Fri, 29 Aug 2025 12:23:16 -0500 Subject: [PATCH 4/7] fix: add more tests --- test/index.js | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/test/index.js b/test/index.js index 36a2ff6..77cb235 100644 --- a/test/index.js +++ b/test/index.js @@ -451,6 +451,59 @@ suite(name, function () { }) }) + test('support express sub-routes with Router that has more than one route', (done) => { + const app = express() + const router = express.Router() + const oapi = openapi() + + const emptySchema = oapi.path({ + responses: { + 204: { + description: 'Successful response', + content: { + 'application/json': {} + } + } + } + }) + + router.get('/endpoint', emptySchema, (req, res) => { + res.status(204).send() + }) + + router.get('/test', emptySchema, (req, res) => { + res.status(204).send() + }) + + app.use(oapi) + app.use('/sub-route', router) + + supertest(app) + .get(`${openapi.defaultRoutePrefix}.json`) + .expect(200, (err, res) => { + assert(!err, err) + SwaggerParser.validate(res.body, (err, api) => { + if (err) { + logDocument(api) + + done(err) + } + + assert(api.paths['/sub-route/test']) + assert(api.paths['/sub-route/test'].get) + assert(api.paths['/sub-route/endpoint']) + assert(api.paths['/sub-route/endpoint'].get) + assert(api.paths['/sub-route/endpoint'].get.responses[204]) + assert.strictEqual( + api.paths['/sub-route/endpoint'].get.responses[204].description, + 'Successful response' + ) + + done() + }) + }) + }) + test('support express nested sub-routes with Router', (done) => { const app = express() const router = express.Router() @@ -500,6 +553,61 @@ suite(name, function () { }) }) + test('support express nested sub-routes with Router that has more than one route', (done) => { + const app = express() + const router = express.Router() + const subrouter = express.Router() + const oapi = openapi() + + const emptySchema = oapi.path({ + responses: { + 204: { + description: 'Successful response', + content: { + 'application/json': {} + } + } + } + }) + + subrouter.get('/endpoint', emptySchema, (req, res) => { + res.status(204).send() + }) + + subrouter.get('/test', emptySchema, (req, res) => { + res.status(204).send() + }) + + app.use(oapi) + app.use('/sub-route', router) + router.use('/sub-sub-route', subrouter) + + supertest(app) + .get(`${openapi.defaultRoutePrefix}.json`) + .expect(200, (err, res) => { + assert(!err, err) + SwaggerParser.validate(res.body, (err, api) => { + if (err) { + logDocument(api) + + done(err) + } + + assert(api.paths['/sub-route/sub-sub-route/endpoint']) + assert(api.paths['/sub-route/sub-sub-route/endpoint'].get) + assert(api.paths['/sub-route/sub-sub-route/test']) + assert(api.paths['/sub-route/sub-sub-route/test'].get) + assert(api.paths['/sub-route/sub-sub-route/endpoint'].get.responses[204]) + assert.strictEqual( + api.paths['/sub-route/sub-sub-route/endpoint'].get.responses[204].description, + 'Successful response' + ) + + done() + }) + }) + }) + test('support express nested sub-routes with base path', (done) => { const app = express() const router = express.Router() From 197b7b8ba2f80fbe150808d3e26d9be5cf74637b Mon Sep 17 00:00:00 2001 From: Seth Wheeler <23089578+Megapixel99@users.noreply.github.com> Date: Fri, 29 Aug 2025 12:34:28 -0500 Subject: [PATCH 5/7] feat!: add support for express 5, remove support for express 4 --- lib/generate-doc.js | 209 ++++++++++++++++++-------------------------- lib/validate.js | 18 ++++ package.json | 2 +- test/index.js | 11 +-- 4 files changed, 107 insertions(+), 133 deletions(-) diff --git a/lib/generate-doc.js b/lib/generate-doc.js index cb59904..7b3c162 100644 --- a/lib/generate-doc.js +++ b/lib/generate-doc.js @@ -1,147 +1,112 @@ 'use strict' -const pathToRegexp = require('path-to-regexp') + const minimumViableDocument = require('./minimum-doc') const { get: getSchema, set: setSchema } = require('./layer-schema') -module.exports = function generateDocument (baseDocument, router, basePath) { - // Merge document with select minimum defaults - const doc = Object.assign({ - openapi: minimumViableDocument.openapi - }, baseDocument, { - info: Object.assign({}, minimumViableDocument.info, baseDocument.info), - paths: Object.assign({}, minimumViableDocument.paths, baseDocument.paths) - }) +function assignRoutes (router, doc, basePath, prefixPath = '', iter = 0, stripped = false) { + // Retrieve all instances of middleware with prefix routes + const subRoutes = router.stack.filter((r) => r.name === 'router') - // Iterate the middleware stack and add any paths and schemas, etc - router && router.stack.forEach((_layer) => { - iterateStack('', null, _layer, (path, routeLayer, layer) => { - if (basePath && path.startsWith(basePath)) { - path = path.replace(basePath, '') - } - const schema = getSchema(layer.handle) - if (!schema || !layer.method) { - return + if (subRoutes.length > 0) { + subRoutes.forEach((routeLayer) => { + const p = router.getRoutes()[0].path.split('/')[1] + + if (prefixPath.replaceAll('/', '').trim() !== p || iter > 1) { + prefixPath += `/${p}` // Prevents basePath from being added twice + } else { + stripped = true // Set stripped to true for later } - const operation = Object.assign({}, schema) + iter += 1 + // Assign the routes to the OpenAPI document + doc = Object.assign(doc, assignRoutes(routeLayer.handle, doc, basePath, prefixPath, iter, stripped)) + }) + } + + // Retrieve all instances of middleware attached to a route + const routes = router.stack.filter((e) => e.route) - // Add route params to schema - if (routeLayer && routeLayer.keys && routeLayer.keys.length) { - const keys = {} + routes.forEach((routeLayer) => { + const paths = [routeLayer.route.path].flat() + const layers = routeLayer.route.stack.filter((s) => + ['OpenApiMiddleware', 'schemaMiddleware', 'validSchemaMiddleware'].includes(s.name) && s.method + ) - const params = routeLayer.keys.map((k, i) => { - const prev = i > 0 && routeLayer.keys[i - 1] - // do not count parameters without a name if they are next to a named parameter - if (typeof k.name === 'number' && prev && prev.offset + prev.name.length + 1 >= k.offset) { - return null - } - let param - if (schema.parameters) { - param = schema.parameters.find((p) => p.name === k.name && p.in === 'path') - } + if (layers.length > 0) { + layers.forEach((layer) => { + const schema = getSchema(layer.handle) // Retrieve the schema defined inside of the Openapi related middleware + if (!schema || !layer.method) return - // Reformat the path - keys[k.name] = '{' + k.name + '}' + paths.forEach((path) => { + const params = [] + let paramIndex = 0 - return Object.assign({ - name: k.name, - in: 'path', - required: !k.optional, - schema: k.schema || { type: 'string' } - }, param || {}) - }) - .filter((e) => e) + // Retrieve parameters defined inside the schema + if (schema.parameters && schema.parameters.length) { + params.push(...schema.parameters) + } - if (schema.parameters) { - schema.parameters.forEach((p) => { - if (!params.find((pp) => p.name === pp.name)) { - params.push(p) + // Iterate over parts of the path to find undefined parameters + path = `${prefixPath}${path}`.split('/').map((p) => { + let name = p.slice(1) + if (p && [':', '*'].includes(p[0])) { + if (p.length === 1 && p === '*') { + name = paramIndex++ + } + if (!params.some((param) => param.name === name && param.in === 'path')) { + params.push({ + name, + in: 'path', + required: true, + schema: { type: 'string' } + }) + } + return `{${name}}` // Format the parameter for OpenAPI } - }) - } + return p + }).join('/') - operation.parameters = params - path = pathToRegexp.compile(path.replace(/\*|\(\*\)/g, '(.*)'))(keys, { encode: (value) => value }) - } + // Remove basePath if the route has not been stripped + if (!stripped && basePath && path.startsWith(basePath)) { + path = path.slice(basePath.length) + } - doc.paths[path] = doc.paths[path] || {} - doc.paths[path][layer.method] = operation - setSchema(layer.handle, operation) - }) + // Assign the parameters back to the schema + schema.parameters = params + + // Object.assign schema to operation + const operation = Object.assign({}, schema) + + // Configure the OpenAPI documentation for the path and the operation + doc.paths[path] = doc.paths[path] || {} + doc.paths[path][layer.method] = operation + + // Reconfigure the schema with the operation + setSchema(layer.handle, operation) + }) + }) + } }) return doc } -function iterateStack (path, routeLayer, layer, cb) { - cb(path, routeLayer, layer) - if (layer.name === 'router') { - layer.handle.stack.forEach(l => { - path = path || '' - iterateStack(path + split(layer.regexp, layer.keys).join('/'), layer, l, cb) - }) - } - if (!layer.route) { - return - } - if (Array.isArray(layer.route.path)) { - const r = layer.regexp.toString() - layer.route.path.forEach((p, i) => iterateStack(path + p, layer, { - ...layer, - // Chacking if p is a string here since p may be a regex expression - keys: layer.keys.filter((k) => typeof p === 'string' ? p.includes(`/:${k.name}`) : false), - // There may be an issue here if the regex has a '|', but that seems to only be the case with user defined regex - regexp: new RegExp(`(${r.substring(2, r.length - 3).split('|')[i]})`), - route: { ...layer.route, path: '' } - }, cb)) - return +module.exports = function generateDocument (baseDocument, router, basePath) { + // Create the default OpenAPI document + let doc = { + ...minimumViableDocument, + ...baseDocument, + info: { ...minimumViableDocument.info, ...baseDocument.info }, + paths: { ...minimumViableDocument.paths, ...baseDocument.paths } } - layer.route.stack.forEach((l) => iterateStack(path + layer.route.path, layer, l, cb)) -} -function processComplexMatch (thing, keys) { - let i = 0 - - return thing - .toString() - // The replace below replaces the regex used by Express to match dynamic parameters - // (i.e. /:id, /:name, etc...) with the name(s) of those parameter(s) - // This could have been accomplished with replaceAll for Node version 15 and above - // no-useless-escape is disabled since we need three backslashes - .replace(/\(\?\:\(\[\^\\\/\]\+\?\)\)/g, () => `{${keys[i++].name}}`) // eslint-disable-line no-useless-escape - .replace(/\\(.)/g, '$1') - // The replace below removes the regex used at the start of the string and - // the regex used to match the query parameters - .replace(/\/\^|\/\?(.*)/g, '') - .split('/') -} + // Configure the base path if it was assigned + const base = basePath || '' -// https://github.com/expressjs/express/issues/3308#issuecomment-300957572 -function split (thing, keys) { - // In express v5 the router layers regexp (path-to-regexp@3.2.0) - // has some additional handling for end of lines, remove those - // - // layer.regexp - // v4 ^\\/sub-route\\/?(?=\\/|$) - // v5 ^\\/sub-route(?:\\/(?=$))?(?=\\/|$) - // - // l.regexp - // v4 ^\\/endpoint\\/?$ - // v5 ^\\/endpoint(?:\\/)?$ - if (typeof thing === 'string') { - return thing.split('/') - } else if (thing.fast_slash) { - return [] - } else { - const match = thing - .toString() - .replace('\\/?', '') - .replace('(?=\\/|$)', '$') - // Added this line to catch the express v5 case after the v4 part is stripped off - .replace('(?:\\/(?=$))?$', '$') - .match(/^\/\^((?:\\[.*+?^${}()|[\]\\/]|[^.*+?^${}()|[\]\\/])*)\$\//) - return match - ? match[1].replace(/\\(.)/g, '$1').split('/') - : processComplexMatch(thing, keys) + if (router) { + // When routes are defined, assign them to the final OpenAPI document + doc = Object.assign(doc, assignRoutes(router, doc, base)) } + + return doc } diff --git a/lib/validate.js b/lib/validate.js index 66fa648..204803f 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -94,6 +94,9 @@ module.exports = function makeValidatorMiddleware (middleware, schema, opts) { }) addFormats(ajv) + if (middleware.router && !middleware.router.handle) { + return next() + } if (opts.keywords) { addKeywords(ajv, opts.keywords) } } @@ -105,6 +108,21 @@ module.exports = function makeValidatorMiddleware (middleware, schema, opts) { let r = req if (opts.coerce !== true) { r = makeReqCopy(req) + } else { + // Redifine query and params as normal objects we can write to, so we can coerce the data + Object.defineProperty(req, 'query', { + value: { ...req.query }, + writable: true, + enumerable: true, + configurable: true + }) + + Object.defineProperty(req, 'params', { + value: { ...req.params }, + writable: true, + enumerable: true, + configurable: true + }) } const validationStatus = validate(r) if (validationStatus === true) { diff --git a/package.json b/package.json index bf35b8d..555081f 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ }, "scripts": { "test": "standard && mocha", - "prepublishOnly": "EXPRESS_MAJOR=5 mocha", + "prepublishOnly": "mocha", "postpublish": "git push origin && git push origin --tags" }, "devDependencies": { diff --git a/test/index.js b/test/index.js index 77cb235..390e562 100644 --- a/test/index.js +++ b/test/index.js @@ -7,16 +7,7 @@ const SwaggerParser = require('swagger-parser') const openapi = require('../') const { name } = require('../package.json') const YAML = require('yaml') - -// We support testing with different major versions of express -let spec = 'express' -if (process.env.EXPRESS_MAJOR) { - if (!['4', '5'].includes(process.env.EXPRESS_MAJOR)) { - throw new Error('EXPRESS_MAJOR contained an invalid value') - } - spec += process.env.EXPRESS_MAJOR -} -const express = require(spec) +const express = require('express') function logDocument (doc) { console.log(util.inspect(doc, { depth: null })) From 16da1581514c8f2a4fac5635a84d36cad67c3f47 Mon Sep 17 00:00:00 2001 From: Seth Wheeler <23089578+Megapixel99@users.noreply.github.com> Date: Fri, 29 Aug 2025 13:05:07 -0500 Subject: [PATCH 6/7] fix: move `express` to `peerDependencies` --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 555081f..e49f1a4 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,13 @@ "postpublish": "git push origin && git push origin --tags" }, "devDependencies": { - "express": "^5.1.0", "mocha": "^10.3.0", "standard": "^17.1.0", "supertest": "^6.3.4" }, + "peerDependencies": { + "express": "^5.1.0" + }, "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^2.1.1", From ef0a0658d6b7e664b7358e3f35e21e65abdae897 Mon Sep 17 00:00:00 2001 From: Seth Wheeler <23089578+Megapixel99@users.noreply.github.com> Date: Fri, 29 Aug 2025 13:32:01 -0500 Subject: [PATCH 7/7] fix: update supported node versions --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fdf2ed5..d529ac5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [16.x, 18.x, 20.x, 22.x] + node-version: [20.x, 22.x, 24.x] steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }}