build(swagger): normalize and sort generated OpenAPI path keys #101

Merged
whitlocktech merged 3 commits from build/swagger-normalize-paths into main 2026-07-27 21:00:59 +00:00
Member

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 in
swagger/swagger.js. No route, handler or annotation changes — this is build tooling plus the
regenerated 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 /users whose collection route is
router.get('/') documents as /api/v1/admin/users/ — advertising a URL no client calls, while
silently dropping /api/v1/admin/users, which the SPA, the Android app and the Discord bot all
do call. 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. #swagger.path was 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.js
already 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 on main.

  • Verified inert. The regenerated spec is byte-for-byte the sorted form of the previously
    committed one:

    new spec === sorted(committed spec), byte-for-byte:  true
    operations:            198 -> 198  (delta: none added, none removed)
    trailing-slash keys:   0
    

    So the entire 4,644-line diff in swagger-output.json is reordering. There were no trailing-slash
    keys 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 sorted swagger-output.json. The
byte-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 and
lands with a zero-line diff in routes.manifest.json, routes.guards.json and
swagger-output.json.

Docs: RunicGateway/docs PR — docs(website): document the OpenAPI path-key normalization.

Checklist

  • I have read CONTRIBUTING.md.
  • The change builds and existing tests/checks pass locally.
  • I have added or updated tests/docs where it makes sense.
  • My commits are reasonably scoped with clear messages.

AI-assisted contributions (required)

  • No AI tools were used to produce this contribution.
  • AI tools were used. Tool(s): Claude Code (Opus 5). I have reviewed and understand
    every change, and take responsibility for it. AI-authored commits are
    marked with a Co-Authored-By / Assisted-By trailer.

License

  • I agree that my contribution is licensed under this project's license
    (GNU GPL v3.0 or later), and I have the right to contribute it.
## 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 in `swagger/swagger.js`. **No route, handler or annotation changes** — this is build tooling plus the regenerated 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 `/users` whose collection route is `router.get('/')` documents as `/api/v1/admin/users/` — advertising a URL no client calls, while silently *dropping* `/api/v1/admin/users`, which the SPA, the Android app and the Discord bot all do call. 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. `#swagger.path` was 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.js` already 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 on `main`. - **Verified inert.** The regenerated spec is byte-for-byte the sorted form of the previously committed one: ``` new spec === sorted(committed spec), byte-for-byte: true operations: 198 -> 198 (delta: none added, none removed) trailing-slash keys: 0 ``` So the entire 4,644-line diff in `swagger-output.json` is reordering. There were no trailing-slash keys 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 sorted `swagger-output.json`. The byte-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 and lands with a **zero-line diff** in `routes.manifest.json`, `routes.guards.json` *and* `swagger-output.json`. Docs: RunicGateway/docs PR — `docs(website): document the OpenAPI path-key normalization`. ## Checklist - [x] I have read [CONTRIBUTING.md](CONTRIBUTING.md). - [x] The change builds and existing tests/checks pass locally. - [x] I have added or updated tests/docs where it makes sense. - [x] My commits are reasonably scoped with clear messages. ## AI-assisted contributions (required) - [ ] No AI tools were used to produce this contribution. - [x] AI tools were used. Tool(s): `Claude Code (Opus 5)`. I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a `Co-Authored-By` / `Assisted-By` trailer. ## License - [x] I agree that my contribution is licensed under this project's license (**GNU GPL v3.0 or later**), and I have the right to contribute it.
wtclaude added 1 commit 2026-07-27 20:51:00 +00:00
build(swagger): normalize and sort generated OpenAPI path keys
All checks were successful
PR Checks / bot-install (pull_request) Successful in 15s
PR Checks / client-build (pull_request) Successful in 23s
PR Checks / server-tests (pull_request) Successful in 9m16s
1a61cd1638
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>
whitlocktech approved these changes 2026-07-27 20:52:35 +00:00
whitlocktech scheduled this pull request to auto merge when all checks succeed 2026-07-27 20:52:39 +00:00
whitlocktech added 2 commits 2026-07-27 21:00:09 +00:00
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>
Merge pull request 'refactor(server): split admin users, account, invites and auth providers into capability routers' (#102) from refactor/admin-router-split-1 into build/swagger-normalize-paths
All checks were successful
PR Checks / bot-install (pull_request) Successful in 21s
PR Checks / client-build (pull_request) Successful in 28s
PR Checks / server-tests (pull_request) Successful in 41s
f7c98b8ba3
Reviewed-on: #102
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
whitlocktech merged commit 0e11e28cca into main 2026-07-27 21:00:59 +00:00
whitlocktech deleted branch build/swagger-normalize-paths 2026-07-27 21:01:00 +00:00
Sign in to join this conversation.
No description provided.