build(swagger): normalize and sort generated OpenAPI path keys
Prepares the committed spec for the admin router domain split
(docs/website/API_V2_PLAN.md § Phase 2) by post-processing swagger-autogen's
output in swagger/swagger.js. No route, handler or annotation changes.
Trailing slashes are stripped from path keys. swagger-autogen builds a path by
string-concatenating the mount prefix with the route argument, so a capability
router mounted at /users whose collection route is router.get('/') documents as
/api/v1/admin/users/ — advertising a URL no client calls while dropping the one
the SPA, the Android app and the Discord bot all do. Express is indifferent
(non-strict routing treats the two as one route, and routes.manifest.json records
the canonical slash-less form), but the published spec is a contract. The split
creates one of these per capability router, so it is fixed once here rather than
by contorting the route declarations in every router file.
Path keys are also sorted. The generator emits them in router-traversal order, so
moving a route between files rewrites most of this ~5k-line committed artifact
even when the API is provably unchanged, burying the one line a reviewer needs to
see. OpenAPI attaches no meaning to path order, and scripts/routeManifest.js
already sorts for the same reason.
Verified inert: the regenerated spec is byte-for-byte the sorted form of the
previously committed one — same 198 operations, zero added or removed, and no
trailing-slash keys (there were none to strip yet; the guard is for the split).
A collision after normalization throws rather than silently dropping an
operation. Server tests green (434/434).
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,8 @@
|
||||
// Regenerate with: npm run swagger (from the server/ directory)
|
||||
// The generated JSON is committed so the docs work without a build step.
|
||||
|
||||
const fs = require('fs')
|
||||
|
||||
const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' })
|
||||
const pkg = require('../package.json')
|
||||
const brand = require('../src/config/brand')
|
||||
@@ -916,7 +918,46 @@ const doc = {
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize `/a/b/` → `/a/b` in the generated path keys.
|
||||
*
|
||||
* swagger-autogen builds a path by string-concatenating the mount prefix with the
|
||||
* route argument, so a capability router mounted at `/users` that declares its
|
||||
* collection route as `router.get('/')` documents as `/api/v1/admin/users/`.
|
||||
* Express itself does not care (non-strict routing treats the two as one route,
|
||||
* and server/routes.manifest.json records the canonical slash-less form), but the
|
||||
* *spec* would advertise a URL no client uses and stop documenting the one they
|
||||
* all call. The domain split (docs/website/API_V2_PLAN.md § Phase 2) creates one
|
||||
* of these per capability router, so it is fixed here once rather than by
|
||||
* contorting the route declarations in every router file.
|
||||
*
|
||||
* The path keys are also **sorted**. swagger-autogen emits them in router-traversal
|
||||
* order, so moving a route between files rewrites most of this 5k-line committed
|
||||
* artifact even when the API is provably unchanged — burying the one line a
|
||||
* reviewer needs to see. OpenAPI attaches no meaning to path order, and
|
||||
* scripts/routeManifest.js already sorts for the same reason.
|
||||
*/
|
||||
function normalizePaths(spec) {
|
||||
const paths = {}
|
||||
for (const [p, item] of Object.entries(spec.paths).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
|
||||
const key = p.length > 1 ? p.replace(/\/+$/, '') : p
|
||||
if (paths[key]) {
|
||||
// Two different declarations collapsed onto one path — merging would hide
|
||||
// whichever lost. Nothing in the tree does this today; fail loudly if it starts.
|
||||
throw new Error(
|
||||
`swagger: "${p}" and "${key}" collide after trailing-slash normalization. ` +
|
||||
'Two routes are documenting the same URL — reconcile them in the router.',
|
||||
)
|
||||
}
|
||||
paths[key] = item
|
||||
}
|
||||
spec.paths = paths
|
||||
return spec
|
||||
}
|
||||
|
||||
swaggerAutogen(outputFile, routes, doc).then(() => {
|
||||
const written = JSON.parse(fs.readFileSync(outputFile, 'utf8'))
|
||||
fs.writeFileSync(outputFile, `${JSON.stringify(normalizePaths(written), null, 2)}\n`)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('swagger-output.json generated.')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user