build(swagger): normalize and sort generated OpenAPI path keys #101
Reference in New Issue
Block a user
No description provided.
Delete Branch "build/swagger-normalize-paths"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What & why
Prepares the committed OpenAPI spec for the admin router domain split
(
docs/website/API_V2_PLAN.md§ Phase 2) by post-processing swagger-autogen's output inswagger/swagger.js. No route, handler or annotation changes — this is build tooling plus theregenerated artifact.
Two problems, both surfaced while implementing split PR 1:
1. Trailing slashes. swagger-autogen builds a path by string-concatenating the mount prefix with
the route argument. A capability router mounted at
/userswhose collection route isrouter.get('/')documents as/api/v1/admin/users/— advertising a URL no client calls, whilesilently dropping
/api/v1/admin/users, which the SPA, the Android app and the Discord bot alldo call. Express is indifferent (non-strict routing treats the two as one route, and
routes.manifest.jsonrecords 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.
#swagger.pathwas considered and rejected:it bypasses the mount prefix entirely, so each route would have to hardcode its full absolute path
in a comment — which silently lies the moment a mount moves.
2. Traversal-order churn. The generator emits path keys 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. That is the opposite of what PR #99's
manifest exists to provide. OpenAPI attaches no meaning to path order, and
scripts/routeManifest.jsalready sorts for exactly this reason.
A collision after normalization throws rather than silently dropping an operation.
How it was tested
npm run swagger— regenerated against the unsplit routers currently onmain.Verified inert. The regenerated spec is byte-for-byte the sorted form of the previously
committed one:
So the entire 4,644-line diff in
swagger-output.jsonis reordering. There were no trailing-slashkeys to strip yet — that guard is for the split PRs that follow.
npm test— 434/434 green.Reviewer note: the whole diff is
swagger.js(+41) and a sortedswagger-output.json. Thebyte-comparison above is the thing to trust; reading the 4.6k-line reorder is not a good use of time.
Follow-up: split PR 1 (admin
users/account/invites/auth/providers) branches off this andlands with a zero-line diff in
routes.manifest.json,routes.guards.jsonandswagger-output.json.Docs: RunicGateway/docs PR —
docs(website): document the OpenAPI path-key normalization.Checklist
AI-assisted contributions (required)
Claude Code (Opus 5). I have reviewed and understandevery change, and take responsibility for it. AI-authored commits are
marked with a
Co-Authored-By/Assisted-Bytrailer.License
(GNU GPL v3.0 or later), and I have the right to contribute it.
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>First of the five domain-split PRs in docs/website/API_V2_PLAN.md § Phase 2. Pure mechanical re-wiring: routes move between files, no handler, gate, validator or annotation changes, and not one URL moves. New src/router/v1/admin/index.js owns the two things the group shares — the `noindex, isLoggedIn, staffOnly` gate and the mount table — and declares no routes itself. The gate sits ahead of every mount so a capability router extracted in a later PR cannot silently ship without it. Four capability routers mount at the prefix they already owned inside the monolith: account.router.js 6 routes -> /admin/account (self-service, no adminOnly) users.router.js 15 routes -> /admin/users (adminOnly, router-level) invites.router.js 3 routes -> /admin/invites (adminOnly, per-route) authProviders.router.js 4 routes -> /admin/auth (adminOnly, per-route) admin.routes.js keeps the other 82 (6+15+3+4+82 = the 110 inventoried admin routes) and is mounted last at the group root; none of the four prefixes appears in it, so nothing depends on mount ordering. It disappears when PR 5 lands. Handlers still live in admin.controller.js and usersShard.controller.js — this re-wires routes, not logic. `adminOnly` moves with the routes that use it, and `usersRouter.use(adminOnly)` is exactly equivalent to the old `adminRouter.use('/users', adminOnly)` now that the router is mounted at /users. All three generated gates are zero-diff: routes.manifest.json unchanged (200 public + 2 internal) routes.guards.json unchanged — no route lost or gained a gate swagger-output.json unchanged, byte-for-byte The spec staying byte-identical depends on the path normalization landed in the preceding commit; without it the four collection routes would have documented as /api/v1/admin/{users,invites,account}/ with a trailing slash. Server tests green (434/434). Co-Authored-By: Claude <noreply@anthropic.com>