chore(admin): typesafe API client + TanStack Query rails (#7638) (#2)

* docs(admin): design for typesafe API client + TanStack Query rails (#7638)

Rails-only scope: codegen toolchain, runtime client, and provider — no
call-site migrations. Admin endpoints are not yet covered by the OpenAPI
spec, so a separate issue will follow before any migration is useful.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(admin): implementation plan for typesafe API client rails (#7638)

Step-by-step task breakdown for the rails-only PR: codegen toolchain,
runtime client, TanStack Query provider, CI freshness check, docs. No
call-site migrations until admin endpoints are added to the OpenAPI
spec (separate follow-up).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(api): export generateDefinitionForVersion from openapi hook

Required by the admin codegen script (#7638) to dump the OpenAPI spec
without booting Express. No behavior change for the request hook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(admin): add OpenAPI codegen + TanStack Query deps (#7638)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(admin): add OpenAPI spec dump entry (#7638)

Loaded via tsx by gen-api.mjs in the next commit. Writes JSON to a file
path argument so log4js stdout output (from Settings init) does not
pollute the spec output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(admin): wire OpenAPI codegen into build (#7638)

Adds gen:api script and amends build/build-copy to regenerate
admin/src/api/schema.d.ts before compiling. The generated file is
checked in so it shows up in PR review and so a fresh checkout doesn't
need codegen to typecheck.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(admin): typed openapi-fetch + react-query client (#7638)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(admin): TanStack Query provider, dev-only devtools (#7638)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(admin): mount TanStack Query provider at root (#7638)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(admin): smoke test for typed openapi-fetch client (#7638)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(admin): verify generated OpenAPI schema is up to date (#7638)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(admin): document OpenAPI codegen workflow (#7638)

Replaces the default Vite scaffold README with admin-specific scripts
table and codegen workflow notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build(admin): exclude __tests__ from tsc include (#7638)

The smoke test imports node:test/node:assert which need @types/node.
Admin source is browser-only, so excluding __tests__ from the production
typecheck is cleaner than adding Node types to the bundle config. The
test still runs under tsx, which doesn't share this constraint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: regenerate lockfile with pnpm 10 to restore overrides block (#7638)

Adding admin deps with pnpm 11 stripped the top-level \`overrides:\`
section from pnpm-lock.yaml, which CI uses pnpm 10 to verify. Result:
ERR_PNPM_LOCKFILE_CONFIG_MISMATCH on every job. Re-running pnpm 10
\`install --lockfile-only\` restores the overrides block; the new admin
package entries land in the same commit. Two stale lockfile entries
not present in package.json (\`serialize-javascript\` version pin and
\`uuid@<14.0.0\`) were normalized by the regen — package.json is the
source of truth for those.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build(docker): preserve strictDepBuilds=false in trimmed workspace yaml (#7638)

Adding tsx as an admin devDep brings esbuild@0.27.x into the resolved
subgraph, and the Dockerfile's runtime stage was overwriting
pnpm-workspace.yaml with a stripped-down version that lost the
strictDepBuilds=false setting from the source repo. With pnpm 10's
default of strictDepBuilds=true, the install then errors on
ERR_PNPM_IGNORED_BUILDS for esbuild + scarf rather than warning.

Restore the strictDepBuilds=false and the @scarf/scarf ignore in the
trimmed yaml so the production install matches develop's behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(admin): point client baseUrl at /api/<version> via codegen (#7638)

Qodo flagged: with baseUrl='/' and schema paths like '/createGroup',
calls landed at /createGroup, but the backend mounts the FLAT-style
spec under /api/<version>/. So once a call site lands, every request
404s.

gen:api now also emits admin/src/api/version.ts containing
LATEST_API_VERSION (read from info.version in the spec) and a derived
API_BASE_URL = `/api/<version>`. client.ts imports API_BASE_URL.
Workflow freshness check covers both generated files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build(admin): cross-platform spawn in gen-api.mjs (#7638)

Windows CI failed because spawnSync('pnpm', ...) cannot resolve
pnpm.cmd without a shell. Set shell:true on win32 only so Linux/macOS
runs avoid Node's DEP0190 warning. All spawn args are literal strings,
so the shell variant is not an injection risk.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
John McLear 2026-05-07 22:21:41 +08:00
parent bcf5c91c15
commit b97baa3ef5
16 changed files with 5081 additions and 49 deletions

View file

@ -68,6 +68,16 @@ jobs:
name: Disable import/export rate limiting
run: |
sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 100000000/' -i settings.json
- name: Verify admin OpenAPI schema is up to date
working-directory: admin
run: |
pnpm gen:api
if ! git diff --exit-code src/api/schema.d.ts src/api/version.ts; then
echo ""
echo "::error::admin/src/api/schema.d.ts or version.ts is out of date."
echo "Run \`pnpm --filter admin gen:api\` and commit the result."
exit 1
fi
- name: Build admin frontend
working-directory: admin
run: |

View file

@ -1,30 +1,67 @@
# React + TypeScript + Vite
# Admin UI
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Vite + React 19 single-page app served at `/admin`. Talks to the backend over
socket.io for the existing settings / plugins / pads pages, and (when
endpoints are added to the OpenAPI spec) over a typed REST client.
Currently, two official plugins are available:
## Scripts
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
| Script | What it does |
| -------------------- | -------------------------------------------------------- |
| `pnpm dev` | Vite dev server. Expects an etherpad backend on :9001. |
| `pnpm gen:api` | Regenerates `src/api/schema.d.ts` from the OpenAPI spec. |
| `pnpm build` | `gen:api` + `tsc` + `vite build`. |
| `pnpm build-copy` | Same, but writes into `../src/templates/admin`. |
| `pnpm test` | Smoke tests for the API client wiring. |
| `pnpm lint` | ESLint. |
## Expanding the ESLint configuration
## Typed API client
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
The admin uses [`openapi-typescript`] to generate types from
`src/node/hooks/express/openapi.ts`, [`openapi-fetch`] for typed requests, and
[`openapi-react-query`] for TanStack Query bindings.
- Configure the top-level `parserOptions` property like this:
[`openapi-typescript`]: https://github.com/openapi-ts/openapi-typescript
[`openapi-fetch`]: https://github.com/openapi-ts/openapi-typescript/tree/main/packages/openapi-fetch
[`openapi-react-query`]: https://github.com/openapi-ts/openapi-typescript/tree/main/packages/openapi-react-query
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
### Regenerating the schema
```sh
pnpm --filter admin gen:api
```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
This runs `admin/scripts/gen-api.mjs`, which loads
`src/node/hooks/express/openapi.ts`, calls `generateDefinitionForVersion` for
the latest API version, pipes the JSON through `openapi-typescript`, and
writes the result to `admin/src/api/schema.d.ts`. The latest API version
read from the spec is also emitted to `admin/src/api/version.ts` so
`client.ts` can build the right `/api/<version>/` baseUrl. Both generated
files are checked in.
Run `gen:api` after any change to:
- `src/node/hooks/express/openapi.ts`
- `src/node/handler/APIHandler.ts` (changes to `latestApiVersion`)
- the resource definitions referenced by `openapi.ts`
### CI freshness check
`.github/workflows/frontend-admin-tests.yml` runs `pnpm gen:api` and fails the
build if `admin/src/api/schema.d.ts` is out of date. If you see the failure
locally, run `pnpm --filter admin gen:api` and commit the regenerated file.
### Using the client
```tsx
import { $api } from './api/client';
const SettingsPanel = () => {
const { data } = $api.useQuery('get', '/admin/settings'); // example
return <pre>{JSON.stringify(data, null, 2)}</pre>;
};
```
The admin endpoints are not yet present in the OpenAPI spec — this client is
in place to support upcoming work (see issue #7638 follow-up). For now, it is
exercised only by the smoke test.

View file

@ -5,13 +5,19 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"gen:api": "node scripts/gen-api.mjs",
"build": "pnpm gen:api && tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"build-copy": "tsc && vite build --outDir ../src/templates/admin --emptyOutDir",
"preview": "vite preview"
"build-copy": "pnpm gen:api && tsc && vite build --outDir ../src/templates/admin --emptyOutDir",
"preview": "vite preview",
"test": "tsx --test src/api/__tests__/client.test.ts"
},
"dependencies": {
"@radix-ui/react-switch": "^1.2.6"
"@radix-ui/react-switch": "^1.2.6",
"@tanstack/react-query": "^5.100.9",
"@tanstack/react-query-devtools": "^5.100.9",
"openapi-fetch": "^0.17.0",
"openapi-react-query": "^0.5.4"
},
"devDependencies": {
"@radix-ui/react-dialog": "^1.1.15",
@ -28,12 +34,14 @@
"i18next": "^26.0.9",
"i18next-browser-languagedetector": "^8.2.1",
"lucide-react": "^1.14.0",
"openapi-typescript": "^7.13.0",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-hook-form": "^7.75.0",
"react-i18next": "^17.0.6",
"react-router-dom": "^7.15.0",
"socket.io-client": "^4.8.3",
"tsx": "^4.21.0",
"typescript": "^6.0.3",
"vite": "^8.0.10",
"vite-plugin-babel": "^1.6.0",

View file

@ -0,0 +1,46 @@
// admin/scripts/dump-spec.ts
//
// Imports the OpenAPI spec builder from the etherpad source and writes the
// flat-style spec for the latest API version as JSON to the file path passed
// as argv[2]. Invoked by admin/scripts/gen-api.mjs via `tsx`.
//
// Why a file argument instead of stdout: importing `openapi.ts` triggers
// `Settings` init, which configures log4js to write INFO/WARN lines to
// stdout. Capturing stdout would mix logs with JSON.
import { writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const outFile = process.argv[2];
if (!outFile) {
process.stderr.write('Usage: tsx scripts/dump-spec.ts <output-path>\n');
process.exit(2);
}
const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(here, '..', '..');
const apiHandlerPath = path.join(repoRoot, 'src', 'node', 'handler', 'APIHandler.ts');
const openapiPath = path.join(repoRoot, 'src', 'node', 'hooks', 'express', 'openapi.ts');
// `openapi.ts` and `APIHandler.ts` use CommonJS-style `exports.*`. Under tsx's
// ESM dynamic import, the whole `module.exports` is exposed as `default`.
type ApiHandlerModule = { latestApiVersion: string };
type OpenApiModule = {
generateDefinitionForVersion: (version: string, style?: string) => unknown;
APIPathStyle: { FLAT: string; REST: string };
};
const apiHandlerMod = await import(pathToFileURL(apiHandlerPath).href);
const openapiMod = await import(pathToFileURL(openapiPath).href);
const apiHandler = (apiHandlerMod.default ?? apiHandlerMod) as ApiHandlerModule;
const openapi = (openapiMod.default ?? openapiMod) as OpenApiModule;
const spec = openapi.generateDefinitionForVersion(
apiHandler.latestApiVersion,
openapi.APIPathStyle.FLAT,
);
writeFileSync(path.resolve(outFile), JSON.stringify(spec, null, 2), 'utf8');

78
admin/scripts/gen-api.mjs Normal file
View file

@ -0,0 +1,78 @@
// admin/scripts/gen-api.mjs
//
// Regenerates admin/src/api/schema.d.ts from the live OpenAPI spec exported
// by src/node/hooks/express/openapi.ts. Run via `pnpm --filter admin gen:api`.
import { spawnSync } from 'node:child_process';
import { mkdtempSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const adminRoot = path.resolve(here, '..');
const outFile = path.join(adminRoot, 'src', 'api', 'schema.d.ts');
const tmpDir = mkdtempSync(path.join(tmpdir(), 'etherpad-openapi-'));
const specPath = path.join(tmpDir, 'spec.json');
// On Windows pnpm resolves to pnpm.cmd, which spawnSync can only find via a
// shell. Use shell on Windows only to avoid Node's DEP0190 warning elsewhere.
// Every argument here is fixed (no user input) so the shell:true variant is
// not an injection risk.
const spawnOpts = {
cwd: adminRoot,
stdio: 'inherit',
shell: process.platform === 'win32',
};
try {
const dump = spawnSync(
'pnpm',
['exec', 'tsx', 'scripts/dump-spec.ts', specPath],
spawnOpts,
);
if (dump.status !== 0) {
console.error(`dump-spec.ts failed with exit code ${dump.status}`);
process.exit(dump.status ?? 1);
}
const gen = spawnSync(
'pnpm',
['exec', 'openapi-typescript', specPath, '-o', outFile],
spawnOpts,
);
if (gen.status !== 0) {
console.error(`openapi-typescript failed with exit code ${gen.status}`);
process.exit(gen.status ?? 1);
}
const header =
`// GENERATED — do not edit. Run \`pnpm --filter admin gen:api\` to regenerate.\n` +
`// Source: src/node/hooks/express/openapi.ts (#7638)\n\n`;
const body = readFileSync(outFile, 'utf8');
writeFileSync(outFile, header + body, 'utf8');
// Emit a runtime-side version constant so client.ts can build the right
// baseUrl. Generated paths are unprefixed (e.g. "/createGroup"), but the
// backend mounts the FLAT-style spec under /api/<version>/.
const spec = JSON.parse(readFileSync(specPath, 'utf8'));
const apiVersion = spec?.info?.version;
if (typeof apiVersion !== 'string' || apiVersion.length === 0) {
console.error('OpenAPI spec is missing info.version; cannot emit version.ts');
process.exit(1);
}
const versionFile = path.join(adminRoot, 'src', 'api', 'version.ts');
writeFileSync(
versionFile,
header +
`export const LATEST_API_VERSION = ${JSON.stringify(apiVersion)};\n` +
`export const API_BASE_URL = \`/api/\${LATEST_API_VERSION}\`;\n`,
'utf8',
);
console.log(`Wrote ${path.relative(process.cwd(), outFile)}`);
console.log(`Wrote ${path.relative(process.cwd(), versionFile)}`);
} finally {
rmSync(tmpDir, { recursive: true, force: true });
}

View file

@ -0,0 +1,40 @@
// admin/src/api/QueryProvider.tsx
//
// TanStack Query provider for the admin UI. Devtools are loaded lazily and
// only in dev builds so they don't ship to production.
import { lazy, Suspense, useState, type ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const Devtools = import.meta.env.DEV
? lazy(() =>
import('@tanstack/react-query-devtools').then((m) => ({
default: m.ReactQueryDevtools,
})),
)
: null;
export const QueryProvider = ({ children }: { children: ReactNode }) => {
const [client] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
refetchOnWindowFocus: true,
},
},
}),
);
return (
<QueryClientProvider client={client}>
{children}
{Devtools && (
<Suspense fallback={null}>
<Devtools initialIsOpen={false} />
</Suspense>
)}
</QueryClientProvider>
);
};

View file

@ -0,0 +1,16 @@
// admin/src/api/__tests__/client.test.ts
//
// Smoke test that the OpenAPI client module loads and exposes the expected
// surface. Catches toolchain wiring regressions (missing peer deps,
// generator output that doesn't export `paths`, etc.).
import { test } from 'node:test';
import assert from 'node:assert/strict';
test('client module exports fetchClient and $api', async () => {
const mod = await import('../client.ts');
assert.ok(mod.fetchClient, 'fetchClient export is present');
assert.ok(mod.$api, '$api export is present');
assert.equal(typeof mod.fetchClient.GET, 'function', 'fetchClient.GET is a function');
assert.equal(typeof mod.$api.useQuery, 'function', '$api.useQuery is a function');
});

12
admin/src/api/client.ts Normal file
View file

@ -0,0 +1,12 @@
// admin/src/api/client.ts
//
// Typed HTTP client and TanStack Query hooks derived from the generated
// OpenAPI schema. Regenerate the schema with `pnpm --filter admin gen:api`.
import createClient from 'openapi-fetch';
import createQueryHooks from 'openapi-react-query';
import type { paths } from './schema';
import { API_BASE_URL } from './version';
export const fetchClient = createClient<paths>({ baseUrl: API_BASE_URL });
export const $api = createQueryHooks(fetchClient);

3567
admin/src/api/schema.d.ts vendored Normal file

File diff suppressed because it is too large Load diff

5
admin/src/api/version.ts Normal file
View file

@ -0,0 +1,5 @@
// GENERATED — do not edit. Run `pnpm --filter admin gen:api` to regenerate.
// Source: src/node/hooks/express/openapi.ts (#7638)
export const LATEST_API_VERSION = "1.3.1";
export const API_BASE_URL = `/api/${LATEST_API_VERSION}`;

View file

@ -14,6 +14,7 @@ import {PadPage} from "./pages/PadPage.tsx";
import {ToastDialog} from "./utils/Toast.tsx";
import {ShoutPage} from "./pages/ShoutPage.tsx";
import {UpdatePage} from "./pages/UpdatePage.tsx";
import {QueryProvider} from './api/QueryProvider.tsx';
const router = createBrowserRouter(createRoutesFromElements(
<><Route element={<App/>}>
@ -34,11 +35,13 @@ const router = createBrowserRouter(createRoutesFromElements(
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<I18nextProvider i18n={i18n}>
<Toast.Provider>
<ToastDialog/>
<RouterProvider router={router}/>
</Toast.Provider>
</I18nextProvider>
<QueryProvider>
<I18nextProvider i18n={i18n}>
<Toast.Provider>
<ToastDialog/>
<RouterProvider router={router}/>
</Toast.Provider>
</I18nextProvider>
</QueryProvider>
</React.StrictMode>,
)

View file

@ -21,5 +21,6 @@
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"exclude": ["src/**/__tests__/**"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View file

@ -0,0 +1,804 @@
# Issue 7638 — Typesafe Admin API Client + TanStack Query Rails Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Lay down the codegen toolchain, runtime client, and TanStack Query provider for the admin UI. No call-site migrations.
**Architecture:** A small Node script imports the OpenAPI spec builder from `src/node/hooks/express/openapi.ts`, writes the JSON to a temp file, and runs `openapi-typescript` to produce a checked-in `admin/src/api/schema.d.ts`. The runtime exposes a typed `openapi-fetch` client and `openapi-react-query` hooks via `admin/src/api/client.ts`, mounted under a `<QueryProvider>` at the admin root. CI re-runs codegen and fails if the working tree is dirty.
**Tech Stack:** TypeScript, React 19, Vite (rolldown-vite), `openapi-typescript`, `openapi-fetch`, `openapi-react-query`, `@tanstack/react-query`, `@tanstack/react-query-devtools`, `tsx` (devDep, runs the codegen script against TS source).
**Spec:** `docs/superpowers/specs/2026-05-01-issue-7638-admin-typesafe-api-design.md`
**Branch:** `chore/admin-typesafe-api-7638` (already cut off `origin/develop`, design doc committed as `41d2babf4`).
**Working directory for all commands:** `/home/jose/etherpad/etherpad-lite` unless otherwise stated.
---
## File Structure
**Create:**
- `admin/scripts/gen-api.mjs` — orchestrator script. Invokes `tsx` to run a small TS entry that prints the spec JSON, captures stdout to a temp file, then shells out to `openapi-typescript`.
- `admin/scripts/dump-spec.ts` — TS entry that imports `generateDefinitionForVersion` from the etherpad source and writes the JSON to stdout.
- `admin/src/api/schema.d.ts` — generated. Checked in.
- `admin/src/api/client.ts``openapi-fetch` + `openapi-react-query` instances.
- `admin/src/api/QueryProvider.tsx` — TanStack Query provider, dev-only devtools.
- `admin/src/api/__tests__/client.test.ts` — module-load smoke test.
- `admin/README.md` — codegen docs (file does not currently exist).
**Modify:**
- `src/node/hooks/express/openapi.ts` — add `export { generateDefinitionForVersion }` at the end so external scripts can call the spec builder. Surgical change, no behavior delta.
- `admin/package.json` — add deps and `gen:api` script; amend `build` to run `gen:api` first.
- `admin/src/main.tsx` — wrap router subtree in `<QueryProvider>`.
- `.github/workflows/frontend-admin-tests.yml` — add a freshness-check step before the existing admin build step.
**Conventions to honor:**
- Per project memory, the PR will go to `johnmclear/etherpad-lite`, not `ether/etherpad-lite`.
- Commit at the end of each task.
- Run `pnpm ts-check` and admin's lint at the end before declaring done.
---
## Task 1: Export the spec builder from `openapi.ts`
**Files:**
- Modify: `src/node/hooks/express/openapi.ts:422` (and end of file)
The script needs to call `generateDefinitionForVersion` from outside the module. It is currently only used within the file. Adding a CommonJS-style export keeps the existing `exports.expressPreSession` style consistent.
- [ ] **Step 1: Read the current export style at the bottom of the file**
Run: `grep -n "^exports\." src/node/hooks/express/openapi.ts`
Expected output: a line like `578:exports.expressPreSession = async (hookName:string, {app}:any) => {`
- [ ] **Step 2: Add the export**
Append at the end of `src/node/hooks/express/openapi.ts` (after the existing hook export, after line 771):
```ts
exports.generateDefinitionForVersion = generateDefinitionForVersion;
exports.APIPathStyle = APIPathStyle;
```
(Both are needed: the script will call `generateDefinitionForVersion(apiHandler.latestApiVersion, APIPathStyle.FLAT)` and we want a single import surface.)
- [ ] **Step 3: Verify ts-check still passes**
Run: `pnpm ts-check`
Expected: no new errors. (If pre-existing errors are present, confirm none are in `openapi.ts`.)
- [ ] **Step 4: Commit**
```bash
git add src/node/hooks/express/openapi.ts
git commit -m "$(cat <<'EOF'
feat(api): export generateDefinitionForVersion from openapi hook
Required by the admin codegen script (#7638) to dump the OpenAPI spec
without booting Express. No behavior change for the request hook.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
---
## Task 2: Add admin dependencies
**Files:**
- Modify: `admin/package.json`
- [ ] **Step 1: Read the current `admin/package.json`**
Run: `cat admin/package.json`
Expected: confirm there is a `dependencies` block and a `devDependencies` block.
- [ ] **Step 2: Install runtime deps**
Run:
```bash
pnpm --filter admin add @tanstack/react-query @tanstack/react-query-devtools openapi-fetch openapi-react-query
```
Expected: deps added under `dependencies`. `pnpm-lock.yaml` updated at repo root.
- [ ] **Step 3: Install dev deps**
Run:
```bash
pnpm --filter admin add -D openapi-typescript tsx
```
Expected: deps added under `devDependencies`.
- [ ] **Step 4: Sanity check the diff**
Run: `git diff admin/package.json`
Expected: six new entries (4 deps, 2 devDeps), no other changes.
- [ ] **Step 5: Commit**
```bash
git add admin/package.json pnpm-lock.yaml
git commit -m "$(cat <<'EOF'
chore(admin): add OpenAPI codegen + TanStack Query deps (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
---
## Task 3: Write the spec-dump entry
**Files:**
- Create: `admin/scripts/dump-spec.ts`
This file is intentionally tiny. It runs under `tsx` so it can resolve the etherpad-lite TypeScript source directly.
- [ ] **Step 1: Create the file**
```ts
// admin/scripts/dump-spec.ts
//
// Imports the OpenAPI spec builder from the etherpad source and writes the
// flat-style spec for the latest API version as JSON to stdout. Invoked by
// admin/scripts/gen-api.mjs via `tsx`.
import path from 'node:path';
import { pathToFileURL } from 'node:url';
const repoRoot = path.resolve(__dirname, '..', '..');
// `openapi.ts` uses CommonJS-style `exports.*` despite living in an ESM repo,
// so we go through createRequire to load it cleanly.
import { createRequire } from 'node:module';
const require = createRequire(pathToFileURL(path.join(repoRoot, 'src', 'node', 'hooks', 'express', 'openapi.ts')).toString());
const apiHandler = require('../../src/node/handler/APIHandler');
const { generateDefinitionForVersion, APIPathStyle } =
require('../../src/node/hooks/express/openapi') as {
generateDefinitionForVersion: (version: string, style?: string) => unknown;
APIPathStyle: { FLAT: string; REST: string };
};
const spec = generateDefinitionForVersion(apiHandler.latestApiVersion, APIPathStyle.FLAT);
process.stdout.write(JSON.stringify(spec, null, 2));
```
- [ ] **Step 2: Smoke-test the entry**
Run:
```bash
cd admin && pnpm exec tsx scripts/dump-spec.ts > /tmp/etherpad-spec.json
echo "exit: $?"
head -c 200 /tmp/etherpad-spec.json
```
Expected: exit 0; the head output starts with `{` and contains `"openapi"` and `"paths"`.
If the script fails because importing `openapi.ts` triggers errors from `Settings`, debug by running `pnpm exec tsx -e "require('../src/node/hooks/express/openapi.ts')"` from `admin/` to isolate. The most likely fix is to set `EP_LOG_DESTINATION=stderr` or similar; do not refactor `Settings` from this PR — note the issue and ask before expanding scope.
- [ ] **Step 3: Commit**
```bash
git add admin/scripts/dump-spec.ts
git commit -m "$(cat <<'EOF'
chore(admin): add OpenAPI spec dump entry (#7638)
Loaded via tsx by gen-api.mjs in the next commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
---
## Task 4: Write the codegen orchestrator
**Files:**
- Create: `admin/scripts/gen-api.mjs`
- [ ] **Step 1: Create the file**
```js
// admin/scripts/gen-api.mjs
//
// Regenerates admin/src/api/schema.d.ts from the live OpenAPI spec exported
// by src/node/hooks/express/openapi.ts. Run via `pnpm --filter admin gen:api`.
import { spawnSync } from 'node:child_process';
import { mkdtempSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const adminRoot = path.resolve(here, '..');
const outFile = path.join(adminRoot, 'src', 'api', 'schema.d.ts');
const tmpDir = mkdtempSync(path.join(tmpdir(), 'etherpad-openapi-'));
const specPath = path.join(tmpDir, 'spec.json');
try {
const dump = spawnSync('pnpm', ['exec', 'tsx', 'scripts/dump-spec.ts'], {
cwd: adminRoot,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'inherit'],
});
if (dump.status !== 0) {
console.error(`dump-spec.ts failed with exit code ${dump.status}`);
process.exit(dump.status ?? 1);
}
writeFileSync(specPath, dump.stdout, 'utf8');
const gen = spawnSync(
'pnpm',
['exec', 'openapi-typescript', specPath, '-o', outFile],
{ cwd: adminRoot, stdio: 'inherit' },
);
if (gen.status !== 0) {
console.error(`openapi-typescript failed with exit code ${gen.status}`);
process.exit(gen.status ?? 1);
}
const header =
`// GENERATED — do not edit. Run \`pnpm --filter admin gen:api\` to regenerate.\n` +
`// Source: src/node/hooks/express/openapi.ts (#7638)\n\n`;
const body = readFileSync(outFile, 'utf8');
writeFileSync(outFile, header + body, 'utf8');
console.log(`Wrote ${path.relative(process.cwd(), outFile)}`);
} finally {
rmSync(tmpDir, { recursive: true, force: true });
}
```
- [ ] **Step 2: Add the `gen:api` script and amend `build`**
In `admin/package.json`, edit the `scripts` block. Before:
```json
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"build-copy": "tsc && vite build --outDir ../src/templates/admin --emptyOutDir",
"preview": "vite preview"
}
```
After:
```json
"scripts": {
"dev": "vite",
"gen:api": "node scripts/gen-api.mjs",
"build": "pnpm gen:api && tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"build-copy": "pnpm gen:api && tsc && vite build --outDir ../src/templates/admin --emptyOutDir",
"preview": "vite preview"
}
```
- [ ] **Step 3: Run codegen and confirm output**
Run:
```bash
mkdir -p admin/src/api
pnpm --filter admin gen:api
ls -la admin/src/api/schema.d.ts
head -10 admin/src/api/schema.d.ts
```
Expected:
- exit 0
- `schema.d.ts` exists, > 1 KB
- first two lines are the generated header
- subsequent lines contain `export interface paths` and entries like `"/api/{version}/createGroup"`
- [ ] **Step 4: Commit script + package.json + generated schema**
```bash
git add admin/scripts/gen-api.mjs admin/package.json admin/src/api/schema.d.ts
git commit -m "$(cat <<'EOF'
chore(admin): wire OpenAPI codegen into build (#7638)
Adds `gen:api` script and amends `build`/`build-copy` to regenerate
admin/src/api/schema.d.ts before compiling. The generated file is
checked in so it shows up in PR review and so a fresh checkout doesn't
need codegen to typecheck.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
---
## Task 5: Runtime client module
**Files:**
- Create: `admin/src/api/client.ts`
- [ ] **Step 1: Create the file**
```ts
// admin/src/api/client.ts
//
// Typed HTTP client and TanStack Query hooks derived from the generated
// OpenAPI schema. Regenerate the schema with `pnpm --filter admin gen:api`.
import createClient from 'openapi-fetch';
import createQueryHooks from 'openapi-react-query';
import type { paths } from './schema';
export const fetchClient = createClient<paths>({ baseUrl: '/' });
export const $api = createQueryHooks(fetchClient);
```
- [ ] **Step 2: Confirm typecheck passes**
Run: `pnpm --filter admin exec tsc --noEmit`
Expected: no errors. If `paths` is missing from `schema.d.ts`, rerun `pnpm --filter admin gen:api` (it should have produced an `export interface paths` already in Task 4).
- [ ] **Step 3: Commit**
```bash
git add admin/src/api/client.ts
git commit -m "$(cat <<'EOF'
feat(admin): typed openapi-fetch + react-query client (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
---
## Task 6: Query provider with dev-only devtools
**Files:**
- Create: `admin/src/api/QueryProvider.tsx`
- [ ] **Step 1: Create the file**
```tsx
// admin/src/api/QueryProvider.tsx
//
// TanStack Query provider for the admin UI. Devtools are loaded lazily and
// only in dev builds so they don't ship to production.
import { lazy, Suspense, useState, type ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const Devtools = import.meta.env.DEV
? lazy(() =>
import('@tanstack/react-query-devtools').then((m) => ({
default: m.ReactQueryDevtools,
})),
)
: null;
export const QueryProvider = ({ children }: { children: ReactNode }) => {
const [client] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
refetchOnWindowFocus: true,
},
},
}),
);
return (
<QueryClientProvider client={client}>
{children}
{Devtools && (
<Suspense fallback={null}>
<Devtools initialIsOpen={false} />
</Suspense>
)}
</QueryClientProvider>
);
};
```
- [ ] **Step 2: Typecheck**
Run: `pnpm --filter admin exec tsc --noEmit`
Expected: no errors.
- [ ] **Step 3: Commit**
```bash
git add admin/src/api/QueryProvider.tsx
git commit -m "$(cat <<'EOF'
feat(admin): TanStack Query provider, dev-only devtools (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
---
## Task 7: Mount the provider at the admin root
**Files:**
- Modify: `admin/src/main.tsx`
- [ ] **Step 1: Read the file to confirm current shape**
Run: `cat admin/src/main.tsx`
Expected: matches the structure where `<I18nextProvider>` wraps `<Toast.Provider>` wraps `<RouterProvider>` inside `<React.StrictMode>`.
- [ ] **Step 2: Edit `admin/src/main.tsx`**
Add the import after the existing imports:
```tsx
import { QueryProvider } from './api/QueryProvider.tsx';
```
Wrap the existing `<I18nextProvider>...</I18nextProvider>` subtree in `<QueryProvider>`. The render block becomes:
```tsx
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryProvider>
<I18nextProvider i18n={i18n}>
<Toast.Provider>
<ToastDialog/>
<RouterProvider router={router}/>
</Toast.Provider>
</I18nextProvider>
</QueryProvider>
</React.StrictMode>,
)
```
(Provider order matters only for context lookups; placing `QueryProvider` outside `I18nextProvider` is fine because it does not consume i18n.)
- [ ] **Step 3: Typecheck**
Run: `pnpm --filter admin exec tsc --noEmit`
Expected: no errors.
- [ ] **Step 4: Build the admin bundle**
Run: `pnpm --filter admin run build`
Expected: build succeeds. Output indicates one bundle (no extra chunk for devtools in production — confirm by grepping the `dist/` for `query-devtools` strings; should be absent).
```bash
grep -rn "ReactQueryDevtools" admin/dist/ 2>/dev/null | head
```
Expected: no matches (production bundle excludes devtools).
- [ ] **Step 5: Commit**
```bash
git add admin/src/main.tsx
git commit -m "$(cat <<'EOF'
feat(admin): mount TanStack Query provider at root (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
---
## Task 8: Smoke test for the client module
**Files:**
- Create: `admin/src/api/__tests__/client.test.ts`
The admin package does not yet ship a unit test runner. Reuse whatever the rest of admin uses for tests if anything; otherwise, this test runs under `tsx --test` (Node's built-in test runner, no extra deps). Confirm at Step 1.
- [ ] **Step 1: Detect the test runner**
Run:
```bash
grep -E '"(test|vitest|jest)"' admin/package.json
ls admin/vitest.config.* admin/jest.config.* 2>/dev/null
```
If admin has no runner configured, use Node's built-in `node:test` (which `tsx` supports).
- [ ] **Step 2: Create the test file**
```ts
// admin/src/api/__tests__/client.test.ts
//
// Smoke test that the OpenAPI client module loads and exposes the expected
// surface. Catches toolchain wiring regressions (missing peer deps,
// generator output that doesn't export `paths`, etc.).
import { test } from 'node:test';
import assert from 'node:assert/strict';
test('client module exports fetchClient and $api', async () => {
const mod = await import('../client.ts');
assert.ok(mod.fetchClient, 'fetchClient export is present');
assert.ok(mod.$api, '$api export is present');
assert.equal(typeof mod.fetchClient.GET, 'function', 'fetchClient.GET is a function');
assert.equal(typeof mod.$api.useQuery, 'function', '$api.useQuery is a function');
});
```
- [ ] **Step 3: Add a `test` script to `admin/package.json`** (only if one does not already exist)
If `admin/package.json` has no `"test"` script, add:
```json
"test": "tsx --test src/api/__tests__/client.test.ts"
```
If admin already has a test runner (e.g. `vitest`), skip the script addition and instead place the test at the location the existing runner picks up (`*.test.ts` is conventional for both vitest and node:test).
- [ ] **Step 4: Run the test**
Run: `pnpm --filter admin test`
Expected: 1 test passing.
- [ ] **Step 5: Commit**
```bash
git add admin/src/api/__tests__/client.test.ts admin/package.json
git commit -m "$(cat <<'EOF'
test(admin): smoke test for typed openapi-fetch client (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
---
## Task 9: CI freshness check
**Files:**
- Modify: `.github/workflows/frontend-admin-tests.yml`
Add a step before the existing `Build admin frontend` step that runs codegen and fails if the working tree changed.
- [ ] **Step 1: Read the current workflow**
Run: `grep -n "Build admin frontend" .github/workflows/frontend-admin-tests.yml`
Expected: a single match around the build step that runs `pnpm run build` from `working-directory: admin`.
- [ ] **Step 2: Insert the freshness check**
Insert immediately before the `Build admin frontend` step:
```yaml
- name: Verify admin OpenAPI schema is up to date
working-directory: admin
run: |
pnpm gen:api
if ! git diff --exit-code src/api/schema.d.ts; then
echo ""
echo "::error::admin/src/api/schema.d.ts is out of date."
echo "Run \`pnpm --filter admin gen:api\` and commit the result."
exit 1
fi
```
- [ ] **Step 3: Lint the YAML**
Run: `python3 -c "import yaml,sys; yaml.safe_load(open('.github/workflows/frontend-admin-tests.yml'))" && echo OK`
Expected: `OK`.
- [ ] **Step 4: Commit**
```bash
git add .github/workflows/frontend-admin-tests.yml
git commit -m "$(cat <<'EOF'
ci(admin): verify generated OpenAPI schema is up to date (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
---
## Task 10: Documentation
**Files:**
- Create: `admin/README.md`
- [ ] **Step 1: Create the file**
```markdown
# Admin UI
Vite + React 19 single-page app served at `/admin`. Talks to the backend over
socket.io for the existing settings / plugins / pads pages, and (when
endpoints are added to the OpenAPI spec) over a typed REST client.
## Scripts
| Script | What it does |
| -------------------- | -------------------------------------------------------- |
| `pnpm dev` | Vite dev server. Expects an etherpad backend on :9001. |
| `pnpm gen:api` | Regenerates `src/api/schema.d.ts` from the OpenAPI spec. |
| `pnpm build` | `gen:api` + `tsc` + `vite build`. |
| `pnpm build-copy` | Same, but writes into `../src/templates/admin`. |
| `pnpm test` | Smoke tests for the API client wiring. |
| `pnpm lint` | ESLint. |
## Typed API client
The admin uses [`openapi-typescript`] to generate types from
`src/node/hooks/express/openapi.ts`, [`openapi-fetch`] for typed requests, and
[`openapi-react-query`] for TanStack Query bindings.
[`openapi-typescript`]: https://github.com/openapi-ts/openapi-typescript
[`openapi-fetch`]: https://github.com/openapi-ts/openapi-typescript/tree/main/packages/openapi-fetch
[`openapi-react-query`]: https://github.com/openapi-ts/openapi-typescript/tree/main/packages/openapi-react-query
### Regenerating the schema
```sh
pnpm --filter admin gen:api
```
This runs `admin/scripts/gen-api.mjs`, which loads
`src/node/hooks/express/openapi.ts`, calls `generateDefinitionForVersion` for
the latest API version, pipes the JSON through `openapi-typescript`, and
writes the result to `admin/src/api/schema.d.ts`. The generated file is
checked in.
Run `gen:api` after any change to:
- `src/node/hooks/express/openapi.ts`
- `src/node/handler/APIHandler.ts` (changes to `latestApiVersion`)
- the resource definitions referenced by `openapi.ts`
### CI freshness check
`.github/workflows/frontend-admin-tests.yml` runs `pnpm gen:api` and fails the
build if `admin/src/api/schema.d.ts` is out of date. If you see the failure
locally, run `pnpm --filter admin gen:api` and commit the regenerated file.
### Using the client
```tsx
import { $api } from './api/client';
const SettingsPanel = () => {
const { data } = $api.useQuery('get', '/admin/settings'); // example
return <pre>{JSON.stringify(data, null, 2)}</pre>;
};
```
The admin endpoints are not yet present in the OpenAPI spec — this client is
in place to support upcoming work (see issue #7638 follow-up). For now, it is
exercised only by the smoke test.
```
- [ ] **Step 2: Commit**
```bash
git add admin/README.md
git commit -m "$(cat <<'EOF'
docs(admin): document OpenAPI codegen workflow (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
---
## Task 11: Full verification pass
No new files — this task confirms the work is green end-to-end before pushing.
- [ ] **Step 1: Clean rebuild**
Run:
```bash
pnpm --filter admin gen:api
pnpm --filter admin run build
```
Expected: both succeed.
- [ ] **Step 2: Repo-wide typecheck**
Run: `pnpm ts-check`
Expected: no new errors versus baseline. If there are pre-existing errors, confirm none are in files this PR touched.
- [ ] **Step 3: Admin tests**
Run: `pnpm --filter admin test`
Expected: 1 test passing.
- [ ] **Step 4: Backend unit tests** (sanity — `openapi.ts` change)
Run: `pnpm test` (or the narrowest available suite covering the API hook; if the full suite is slow, run specs that exercise `openapi.ts` only).
Expected: green.
- [ ] **Step 5: Confirm devtools absent from production bundle**
Run: `grep -rn "ReactQueryDevtools" admin/dist/ 2>/dev/null`
Expected: zero matches.
- [ ] **Step 6: Manual smoke**
Per project convention (memory: install plugin/branch for manual test), install this branch on a local etherpad and:
- Open `/admin/` in a dev build (`pnpm --filter admin dev`). Confirm the React Query devtools panel button appears in the bottom corner.
- Open `/admin/` in the production-built bundle. Confirm devtools panel is absent.
- Click through plugins / settings / pads / shout pages and confirm no regression versus pre-PR behavior (existing socket.io flows unchanged).
Document the smoke results in the PR description.
- [ ] **Step 7: Push**
```bash
git push -u fork chore/admin-typesafe-api-7638
```
- [ ] **Step 8: Open PR**
```bash
gh pr create \
--repo johnmclear/etherpad-lite \
--title "chore(admin): typesafe API client + TanStack Query rails (#7638)" \
--body "$(cat <<'EOF'
## Summary
Lays down the rails for a typesafe, OpenAPI-derived admin API client backed by TanStack Query. Closes #7638.
- Codegen toolchain (`pnpm --filter admin gen:api`) producing `admin/src/api/schema.d.ts` from `src/node/hooks/express/openapi.ts`.
- Runtime client (`openapi-fetch` + `openapi-react-query`).
- `<QueryProvider>` mounted at the admin root with dev-only devtools.
- CI freshness check on the generated schema.
- `admin/README.md` documenting the workflow.
**No call sites migrated.** Admin endpoints aren't in the OpenAPI spec yet — that gap is filed as a follow-up issue and must land before any migration is useful. #7601 should rebase onto this branch.
**Semver:** patch — build tooling + currently-unused runtime libs, no observable behavior change.
## Test plan
- [x] `pnpm --filter admin gen:api` runs clean
- [x] `pnpm --filter admin run build` succeeds
- [x] `pnpm --filter admin test` passes (smoke test)
- [x] `pnpm ts-check` clean
- [x] Production bundle does not contain devtools
- [x] Manual smoke: dev build shows devtools, prod build hides them, existing socket.io pages unaffected
🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
```
- [ ] **Step 9: Trigger Qodo review** (per project convention)
```bash
gh pr comment <PR-number> --repo johnmclear/etherpad-lite --body "/review"
```
- [ ] **Step 10: File the spec-coverage follow-up issue**
Create a new issue on `ether/etherpad` titled "Document admin endpoints in the OpenAPI spec" and link from the PR body. The issue should note that 7638 rails are unused until admin endpoints are added.
---
## Risk register (carried from spec)
- **`openapi.ts` not cleanly importable.** If `dump-spec.ts` fails to import the module due to side effects (Settings, log4js init), pause and ask before refactoring `Settings`. A common workaround is to set `EP_LOG_DESTINATION=stderr` or set `NODE_ENV=production`. Do not silently expand scope.
- **Generated schema differs by Node version.** `openapi-typescript` output is deterministic, but if a contributor sees a phantom diff, confirm Node major matches the CI matrix (22/24/25 today; CI uses 24 on PRs).
- **Bundle size.** ~12 KB gzipped added to the admin bundle even with no call sites. Acceptable; flagged in the PR body for transparency.
## Out of scope (do not pull in)
- Adding admin endpoints to the OpenAPI spec.
- Migrating any `fetch()` site in `admin/src/`.
- Backend handler changes.
- Pad-side frontend changes.

View file

@ -0,0 +1,198 @@
# Issue 7638 — Typesafe Admin API Client + TanStack Query Rails
**Status:** design approved 2026-05-01
**Issue:** https://github.com/ether/etherpad/issues/7638
**Related:** #7601 (introduces new admin REST sites that will adopt these rails)
## Goal
Lay down the toolchain and runtime rails for a typesafe, OpenAPI-derived admin
API client backed by TanStack Query. Do not migrate any existing call sites.
## Why rails-only
The issue's framing ("migrate every `useEffect`+`fetch` site") overstates what is
actually present in `admin/src/` today.
- The only REST `fetch()` sites are `App.tsx` and `LoginScreen.tsx` (both POST to
`/admin-auth/`) and `i18n.ts` (locale loading).
- All admin pages with real data flow (Settings, Plugins, Pads, Shout) run over
socket.io + zustand, not REST.
- The OpenAPI spec produced by `src/node/hooks/express/openapi.ts` only covers
the public Etherpad HTTP API under `/api/{version}/*`. It documents zero admin
endpoints — no `/admin-auth/`, no future `/admin/*` REST endpoints from #7601.
So the generated client has nothing in `admin/src/` to type today. The value of
landing this PR now is to get the rails in place so #7601 (and any subsequent
admin REST work) can adopt them on day one.
A separate issue will be filed to add admin endpoint coverage to the OpenAPI
spec; until that lands, no migrations are useful.
## Out of scope
- Admin endpoint coverage in the OpenAPI spec (separate issue).
- Migrating any existing `fetch()` call site.
- Backend changes.
- Pad-side frontend.
## Toolchain
| Package | Type | Purpose |
| -------------------------------- | -------------- | ---------------------------------------- |
| `openapi-typescript` | devDependency | Generates `.d.ts` from the OpenAPI spec |
| `openapi-fetch` | dependency | Typed `fetch` wrapper |
| `openapi-react-query` | dependency | TanStack Query bindings over the client |
| `@tanstack/react-query` | dependency | Query runtime |
| `@tanstack/react-query-devtools` | dependency | Dev-only devtools panel |
All added to `admin/package.json`. No version pinning beyond standard caret
ranges; pick the latest stable at implementation time.
## Codegen (option 3, hybrid)
One checked-in artifact, CI-enforced freshness.
### Script: `admin/scripts/gen-api.mjs`
1. Imports the spec-building entry point from
`src/node/hooks/express/openapi.ts` (or a thin wrapper module that calls
the spec builder without booting Express). Writes the resulting spec JSON
to a temp file in `os.tmpdir()`.
2. Shells out:
`openapi-typescript <tmp> -o admin/src/api/schema.d.ts`.
3. Prepends a generated header comment to the output:
`// GENERATED — do not edit. Run \`pnpm gen:api\` to regenerate.`
4. Removes the temp file.
If `openapi.ts` cannot be loaded as an ES module without side effects (e.g.
because it imports settings or boots an Express app at import time), the
implementation must extract the pure spec-builder into a dedicated module so
the script can call it cleanly. That refactor is in scope; the touch should be
minimal.
### Wiring
- `admin/package.json`:
- `"scripts": { "gen:api": "node scripts/gen-api.mjs", ... }`.
- `"build"` is amended to run `gen:api` before `tsc && vite build` so a
fresh checkout builds without manual steps.
- Root `package.json`: existing admin build entry point invokes the same
script (or relies on `admin/package.json`'s amended `build`).
### Generated output
- Path: `admin/src/api/schema.d.ts`.
- Checked in.
- First line: generated-header comment.
### CI freshness check
A CI job (folded into the existing admin lint workflow if practical, otherwise
a new step) runs:
```bash
pnpm --filter admin gen:api
git diff --exit-code admin/src/api/schema.d.ts
```
If the diff is non-empty, CI fails with a message instructing the contributor
to run `pnpm --filter admin gen:api` and commit the result.
## Runtime client
### `admin/src/api/client.ts`
```ts
import createClient from "openapi-fetch";
import createQueryHooks from "openapi-react-query";
import type { paths } from "./schema";
export const fetchClient = createClient<paths>({ baseUrl: "/" });
export const $api = createQueryHooks(fetchClient);
```
### `admin/src/api/QueryProvider.tsx`
- Wraps children in `QueryClientProvider`.
- Single shared `QueryClient` constructed once (module-level or `useState`
initializer), with defaults:
- `staleTime: 30_000`
- `refetchOnWindowFocus: true`
- Other defaults left at library defaults.
- Mounts `ReactQueryDevtools` only when `import.meta.env.DEV` is true. Use a
dynamic `import()` so devtools do not ship in the production bundle.
### `admin/src/main.tsx`
Wrap `<App />` in `<QueryProvider>`. No other changes.
## Documentation
`admin/README.md` (create or extend) documents:
- How to regenerate: `pnpm --filter admin gen:api`.
- When to regenerate: after any change to `src/node/hooks/express/openapi.ts`
or anything that affects the spec it builds.
- What gets regenerated: `admin/src/api/schema.d.ts` only.
- The CI freshness check and how to recover from a failing check.
- A short "how to use the client" snippet showing
`$api.useQuery("get", "/some/path")` once admin endpoints are in the spec.
## Tests
- **Module-load smoke test** (`admin/src/api/__tests__/client.test.ts` or
similar, matching whatever test infra `admin/` already uses): imports
`$api` and `fetchClient`, asserts both are defined. This catches toolchain
wiring breakage (missing peer deps, bad export shape, etc.).
- **CI freshness check** (above) is the test for spec/schema sync.
- **Manual smoke after PR install:** install the branch on the local
Etherpad, open `/admin`, confirm:
- Existing socket.io flows (settings, plugins, pads) still work — no
regressions from the `<QueryProvider>` wrap.
- React Query devtools panel appears in a dev build (`pnpm --filter admin
dev`) and is absent from a production build.
Note: per project convention, the user expects automated tests before manual
verification, but the manual smoke is unavoidable here because devtools
visibility and provider wrap are runtime concerns. The smoke check is a
secondary safety net, not the primary test strategy.
## Branch / PR plan
- Fork: `johnmclear/etherpad-lite` (per project convention; never commit
directly to `ether/etherpad-lite`).
- Branch: `chore/admin-typesafe-api-7638`.
- Base: latest `main` of the fork, after syncing from `ether/etherpad-lite`.
- PR title: `chore(admin): typesafe API client + TanStack Query rails`.
- PR body declares semver: **patch** (build tooling + unused runtime libs;
no observable behavior change).
- PR body links #7638 and notes:
- Rails-only — no call site migrations.
- Separate spec-coverage issue to follow.
- #7601 should rebase onto this branch once merged.
## Risks
- **`openapi.ts` not cleanly importable.** If pulling the spec builder out
requires touching production paths, that risk needs a small refactor PR
first. Mitigation: keep the extraction surgical; if it grows, split into
its own PR and rebase 7638 on top.
- **Bundle size.** TanStack Query + react-query bindings add ~12 KB gzipped
to the admin bundle even with no call sites using it. Acceptable for an
internal admin UI; flag in PR body for transparency.
- **Provider wrap regressions.** `<QueryProvider>` wrapping `<App />` should
be inert for socket.io paths but the manual smoke confirms.
## Definition of done
- `pnpm --filter admin gen:api` runs cleanly on a fresh checkout.
- `pnpm --filter admin build` succeeds.
- `admin/src/api/schema.d.ts` is checked in with the generated header.
- `<QueryProvider>` wraps `<App />`; devtools visible in dev, absent in
production build.
- CI freshness check is wired and passing.
- `admin/README.md` documents the codegen workflow.
- Manual smoke confirms no regression in existing socket.io-driven pages.
- PR opened against `johnmclear/etherpad-lite`, semver labelled patch,
Qodo `/review` triggered after push.

240
pnpm-lock.yaml generated
View file

@ -46,6 +46,18 @@ importers:
'@radix-ui/react-switch':
specifier: ^1.2.6
version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@tanstack/react-query':
specifier: ^5.100.9
version: 5.100.9(react@19.2.5)
'@tanstack/react-query-devtools':
specifier: ^5.100.9
version: 5.100.9(@tanstack/react-query@5.100.9(react@19.2.5))(react@19.2.5)
openapi-fetch:
specifier: ^0.17.0
version: 0.17.0
openapi-react-query:
specifier: ^0.5.4
version: 0.5.4(@tanstack/react-query@5.100.9(react@19.2.5))(openapi-fetch@0.17.0)
devDependencies:
'@radix-ui/react-dialog':
specifier: ^1.1.15
@ -89,6 +101,9 @@ importers:
lucide-react:
specifier: ^1.14.0
version: 1.14.0(react@19.2.5)
openapi-typescript:
specifier: ^7.13.0
version: 7.13.0(typescript@6.0.3)
react:
specifier: ^19.2.5
version: 19.2.5
@ -107,6 +122,9 @@ importers:
socket.io-client:
specifier: ^4.8.3
version: 4.8.3
tsx:
specifier: ^4.21.0
version: 4.21.0
typescript:
specifier: ^6.0.3
version: 6.0.3
@ -159,7 +177,7 @@ importers:
version: 0.129.0
vitepress:
specifier: ^2.0.0-alpha.17
version: 2.0.0-alpha.17(@types/node@25.6.0)(jwt-decode@4.0.0)(lightningcss@1.32.0)(oxc-minify@0.129.0)(postcss@8.5.14)(tsx@4.21.0)(typescript@6.0.3)
version: 2.0.0-alpha.17(@types/node@25.6.0)(change-case@5.4.4)(jwt-decode@4.0.0)(lightningcss@1.32.0)(oxc-minify@0.129.0)(postcss@8.5.14)(tsx@4.21.0)(typescript@6.0.3)
src:
dependencies:
@ -1615,6 +1633,16 @@ packages:
peerDependencies:
'@redis/client': ^5.12.1
'@redocly/ajv@8.11.2':
resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
'@redocly/config@0.22.0':
resolution: {integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==}
'@redocly/openapi-core@1.34.14':
resolution: {integrity: sha512-y+xFx+Zz54Xhr8jUdnLENYnt7Y7GEDL6Q03ga7rTtX8DVwefX9H+hQEPgJp1nda7vdH+wJ9/HBVvyfBuW9x6rA==}
engines: {node: '>=18.17.0', npm: '>=9.5.0'}
'@rolldown/binding-android-arm64@1.0.0-rc.17':
resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@ -1918,6 +1946,23 @@ packages:
'@swc/helpers@0.5.21':
resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==}
'@tanstack/query-core@5.100.9':
resolution: {integrity: sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ==}
'@tanstack/query-devtools@5.100.9':
resolution: {integrity: sha512-gqiptrTIhbK2PuCaPRHmWXfJG1NGYVFpAr0HqogEqiSBNB5xDz6fmesQt7w4WgMOqOQPnPHJ3ZDMuhDaXvNO8g==}
'@tanstack/react-query-devtools@5.100.9':
resolution: {integrity: sha512-mM3slaVGXJmz+pOLgXdANj75ikgQCyudyl3kmFvm6brI1JyVeY/+IeD17uDHIvZrD8hfoO2sdZ54RFsHdYAuhA==}
peerDependencies:
'@tanstack/react-query': ^5.100.9
react: ^18 || ^19
'@tanstack/react-query@5.100.9':
resolution: {integrity: sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A==}
peerDependencies:
react: ^18 || ^19
'@tediousjs/connection-string@1.1.0':
resolution: {integrity: sha512-z9ZBWEG+8pIB5V1zYzlRPXx0oRJ5H7coPnMQK8EZOw03UTPI9Umn6viL36f5w+CuqkKsnCM50RVStpjZmR0Bng==}
@ -2552,6 +2597,10 @@ packages:
ajv@8.18.0:
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
@ -2772,6 +2821,9 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
change-case@5.4.4:
resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
character-entities-html4@2.1.0:
resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
@ -2808,6 +2860,9 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
colorette@1.4.0:
resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
@ -3575,9 +3630,6 @@ packages:
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
engines: {node: '>= 0.4'}
get-tsconfig@4.13.0:
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
get-tsconfig@4.14.0:
resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==}
@ -3762,6 +3814,10 @@ packages:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
index-to-position@1.2.0:
resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==}
engines: {node: '>=18'}
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
@ -3946,6 +4002,10 @@ packages:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
js-levenshtein@1.1.6:
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
engines: {node: '>=0.10.0'}
js-md4@0.3.2:
resolution: {integrity: sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==}
@ -4292,6 +4352,10 @@ packages:
minimatch@3.1.5:
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
minimatch@5.1.9:
resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==}
engines: {node: '>=10'}
minimatch@9.0.9:
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
engines: {node: '>=16 || 14 >=14.17'}
@ -4502,6 +4566,15 @@ packages:
resolution: {integrity: sha512-1tfLpC+7CajKv08vuFOLm4t8rJvJyqKuyau5IIIrGg3YuQYhmP7JqDL6p6WnbDCusmh3krCrKXoHB6hLF/iHcQ==}
engines: {node: '>=20.0.0'}
openapi-fetch@0.17.0:
resolution: {integrity: sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig==}
openapi-react-query@0.5.4:
resolution: {integrity: sha512-V9lRiozjHot19/BYSgXYoyznDxDJQhEBSdi26+SJ0UqjMANLQhkni4XG+Z7e3Ag7X46ZLMrL9VxYkghU3QvbWg==}
peerDependencies:
'@tanstack/react-query': ^5.80.0
openapi-fetch: ^0.17.0
openapi-schema-validation@0.4.2:
resolution: {integrity: sha512-K8LqLpkUf2S04p2Nphq9L+3bGFh/kJypxIG2NVGKX0ffzT4NQI9HirhiY6Iurfej9lCu7y4Ndm4tv+lm86Ck7w==}
@ -4511,6 +4584,15 @@ packages:
openapi-types@12.1.3:
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
openapi-typescript-helpers@0.1.0:
resolution: {integrity: sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw==}
openapi-typescript@7.13.0:
resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==}
hasBin: true
peerDependencies:
typescript: ^5.x
optional-js@2.3.0:
resolution: {integrity: sha512-B0LLi+Vg+eko++0z/b8zIv57kp7HKEzaPJo7LowJXMUKYdf+3XJGu/cw03h/JhIOsLnP+cG5QnTHAuicjA5fMw==}
@ -4545,6 +4627,10 @@ packages:
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
parse-json@8.3.0:
resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
engines: {node: '>=18'}
parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
@ -4638,6 +4724,10 @@ packages:
engines: {node: '>=18'}
hasBin: true
pluralize@8.0.0:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'}
possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
@ -5229,6 +5319,10 @@ packages:
resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==}
engines: {node: '>=14.18.0'}
supports-color@10.2.2:
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
engines: {node: '>=18'}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@ -5387,6 +5481,10 @@ packages:
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
engines: {node: '>=10'}
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
type-is@2.0.1:
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
engines: {node: '>= 0.6'}
@ -5490,6 +5588,9 @@ packages:
peerDependencies:
browserslist: '>= 4.21.0'
uri-js-replace@1.0.1:
resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==}
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@ -5821,6 +5922,9 @@ packages:
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
engines: {node: '>=18'}
yaml-ast-parser@0.0.43:
resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==}
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
@ -6866,6 +6970,29 @@ snapshots:
dependencies:
'@redis/client': 5.12.1(@opentelemetry/api@1.9.1)
'@redocly/ajv@8.11.2':
dependencies:
fast-deep-equal: 3.1.3
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
uri-js-replace: 1.0.1
'@redocly/config@0.22.0': {}
'@redocly/openapi-core@1.34.14(supports-color@10.2.2)':
dependencies:
'@redocly/ajv': 8.11.2
'@redocly/config': 0.22.0
colorette: 1.4.0
https-proxy-agent: 7.0.6(supports-color@10.2.2)
js-levenshtein: 1.1.6
js-yaml: 4.1.1
minimatch: 5.1.9
pluralize: 8.0.0
yaml-ast-parser: 0.0.43
transitivePeerDependencies:
- supports-color
'@rolldown/binding-android-arm64@1.0.0-rc.17':
optional: true
@ -7069,6 +7196,21 @@ snapshots:
dependencies:
tslib: 2.8.1
'@tanstack/query-core@5.100.9': {}
'@tanstack/query-devtools@5.100.9': {}
'@tanstack/react-query-devtools@5.100.9(@tanstack/react-query@5.100.9(react@19.2.5))(react@19.2.5)':
dependencies:
'@tanstack/query-devtools': 5.100.9
'@tanstack/react-query': 5.100.9(react@19.2.5)
react: 19.2.5
'@tanstack/react-query@5.100.9(react@19.2.5)':
dependencies:
'@tanstack/query-core': 5.100.9
react: 19.2.5
'@tediousjs/connection-string@1.1.0': {}
'@tootallnate/quickjs-emscripten@0.23.0': {}
@ -7458,7 +7600,7 @@ snapshots:
debug: 4.4.3(supports-color@8.1.1)
globby: 11.1.0
is-glob: 4.0.3
minimatch: 9.0.9
minimatch: 10.2.5
semver: 7.7.4
ts-api-utils: 1.4.3(typescript@6.0.3)
optionalDependencies:
@ -7698,12 +7840,13 @@ snapshots:
'@vueuse/shared': 14.2.1(vue@3.5.30(typescript@6.0.3))
vue: 3.5.30(typescript@6.0.3)
'@vueuse/integrations@14.2.1(focus-trap@8.0.0)(jwt-decode@4.0.0)(vue@3.5.30(typescript@6.0.3))':
'@vueuse/integrations@14.2.1(change-case@5.4.4)(focus-trap@8.0.0)(jwt-decode@4.0.0)(vue@3.5.30(typescript@6.0.3))':
dependencies:
'@vueuse/core': 14.2.1(vue@3.5.30(typescript@6.0.3))
'@vueuse/shared': 14.2.1(vue@3.5.30(typescript@6.0.3))
vue: 3.5.30(typescript@6.0.3)
optionalDependencies:
change-case: 5.4.4
focus-trap: 8.0.0
jwt-decode: 4.0.0
@ -7757,6 +7900,8 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
ansi-colors@4.1.3: {}
ansi-regex@5.0.1: {}
ansi-regex@6.2.2: {}
@ -7996,6 +8141,8 @@ snapshots:
ansi-styles: 4.3.0
supports-color: 7.2.0
change-case@5.4.4: {}
character-entities-html4@2.1.0: {}
character-entities-legacy@3.0.0: {}
@ -8026,6 +8173,8 @@ snapshots:
color-name@1.1.4: {}
colorette@1.4.0: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
@ -8148,6 +8297,12 @@ snapshots:
dependencies:
ms: 2.1.3
debug@4.4.3(supports-color@10.2.2):
dependencies:
ms: 2.1.3
optionalDependencies:
supports-color: 10.2.2
debug@4.4.3(supports-color@8.1.1):
dependencies:
ms: 2.1.3
@ -8469,7 +8624,7 @@ snapshots:
'@rushstack/eslint-patch': 1.16.1
'@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@10.3.0)(typescript@6.0.3))(eslint@10.3.0)(typescript@6.0.3)
'@typescript-eslint/parser': 7.18.0(eslint@10.3.0)(typescript@6.0.3)
eslint-import-resolver-typescript: 3.9.1(eslint-plugin-import@2.32.0)(eslint@10.3.0)
eslint-import-resolver-typescript: 3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.3.0)(typescript@6.0.3))(eslint@10.3.0))(eslint@10.3.0)
eslint-plugin-cypress: 2.15.2(eslint@10.3.0)
eslint-plugin-eslint-comments: 3.2.0(eslint@10.3.0)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.3.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.1)(eslint@10.3.0)
@ -8493,7 +8648,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0)(eslint@10.3.0):
eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.3.0)(typescript@6.0.3))(eslint@10.3.0))(eslint@10.3.0):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3(supports-color@8.1.1)
@ -8508,14 +8663,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@7.18.0(eslint@10.3.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.9.1)(eslint@10.3.0):
eslint-module-utils@2.12.1(@typescript-eslint/parser@7.18.0(eslint@10.3.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.3.0)(typescript@6.0.3))(eslint@10.3.0))(eslint@10.3.0))(eslint@10.3.0):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 7.18.0(eslint@10.3.0)(typescript@6.0.3)
eslint: 10.3.0
eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.9.1(eslint-plugin-import@2.32.0)(eslint@10.3.0)
eslint-import-resolver-typescript: 3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.3.0)(typescript@6.0.3))(eslint@10.3.0))(eslint@10.3.0)
transitivePeerDependencies:
- supports-color
@ -8548,7 +8703,7 @@ snapshots:
doctrine: 2.1.0
eslint: 10.3.0
eslint-import-resolver-node: 0.3.10
eslint-module-utils: 2.12.1(@typescript-eslint/parser@7.18.0(eslint@10.3.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.9.1)(eslint@10.3.0)
eslint-module-utils: 2.12.1(@typescript-eslint/parser@7.18.0(eslint@10.3.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.3.0)(typescript@6.0.3))(eslint@10.3.0))(eslint@10.3.0))(eslint@10.3.0)
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@ -8944,10 +9099,6 @@ snapshots:
es-errors: 1.3.0
get-intrinsic: 1.3.0
get-tsconfig@4.13.0:
dependencies:
resolve-pkg-maps: 1.0.0
get-tsconfig@4.14.0:
dependencies:
resolve-pkg-maps: 1.0.0
@ -9157,6 +9308,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.6(supports-color@10.2.2):
dependencies:
agent-base: 7.1.4
debug: 4.4.3(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
i18next-browser-languagedetector@8.2.1:
dependencies:
'@babel/runtime': 7.28.6
@ -9177,6 +9335,8 @@ snapshots:
imurmurhash@0.1.4: {}
index-to-position@1.2.0: {}
inherits@2.0.4: {}
internal-slot@1.1.0:
@ -9348,6 +9508,8 @@ snapshots:
js-cookie@3.0.5: {}
js-levenshtein@1.1.6: {}
js-md4@0.3.2: {}
js-tokens@4.0.0: {}
@ -9701,6 +9863,10 @@ snapshots:
dependencies:
brace-expansion: 1.1.14
minimatch@5.1.9:
dependencies:
brace-expansion: 5.0.5
minimatch@9.0.9:
dependencies:
brace-expansion: 2.1.0
@ -9946,6 +10112,16 @@ snapshots:
openapi-types: 12.1.3
qs: 6.15.0
openapi-fetch@0.17.0:
dependencies:
openapi-typescript-helpers: 0.1.0
openapi-react-query@0.5.4(@tanstack/react-query@5.100.9(react@19.2.5))(openapi-fetch@0.17.0):
dependencies:
'@tanstack/react-query': 5.100.9(react@19.2.5)
openapi-fetch: 0.17.0
openapi-typescript-helpers: 0.1.0
openapi-schema-validation@0.4.2:
dependencies:
jsonschema: 1.2.4
@ -9961,6 +10137,18 @@ snapshots:
openapi-types@12.1.3: {}
openapi-typescript-helpers@0.1.0: {}
openapi-typescript@7.13.0(typescript@6.0.3):
dependencies:
'@redocly/openapi-core': 1.34.14(supports-color@10.2.2)
ansi-colors: 4.1.3
change-case: 5.4.4
parse-json: 8.3.0
supports-color: 10.2.2
typescript: 6.0.3
yargs-parser: 21.1.1
optional-js@2.3.0: {}
optionator@0.9.4:
@ -10029,6 +10217,12 @@ snapshots:
package-json-from-dist@1.0.1: {}
parse-json@8.3.0:
dependencies:
'@babel/code-frame': 7.29.0
index-to-position: 1.2.0
type-fest: 4.41.0
parse5@7.3.0:
dependencies:
entities: 6.0.1
@ -10107,6 +10301,8 @@ snapshots:
optionalDependencies:
fsevents: 2.3.2
pluralize@8.0.0: {}
possible-typed-array-names@1.1.0: {}
postcss@8.5.14:
@ -10830,6 +11026,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
supports-color@10.2.2: {}
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@ -10967,7 +11165,7 @@ snapshots:
tsx@4.21.0:
dependencies:
esbuild: 0.27.1
get-tsconfig: 4.13.0
get-tsconfig: 4.14.0
optionalDependencies:
fsevents: 2.3.3
@ -10981,6 +11179,8 @@ snapshots:
type-fest@0.20.2: {}
type-fest@4.41.0: {}
type-is@2.0.1:
dependencies:
content-type: 1.0.5
@ -11132,6 +11332,8 @@ snapshots:
escalade: 3.2.0
picocolors: 1.1.1
uri-js-replace@1.0.1: {}
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
@ -11208,7 +11410,7 @@ snapshots:
fsevents: 2.3.3
tsx: 4.21.0
vitepress@2.0.0-alpha.17(@types/node@25.6.0)(jwt-decode@4.0.0)(lightningcss@1.32.0)(oxc-minify@0.129.0)(postcss@8.5.14)(tsx@4.21.0)(typescript@6.0.3):
vitepress@2.0.0-alpha.17(@types/node@25.6.0)(change-case@5.4.4)(jwt-decode@4.0.0)(lightningcss@1.32.0)(oxc-minify@0.129.0)(postcss@8.5.14)(tsx@4.21.0)(typescript@6.0.3):
dependencies:
'@docsearch/css': 4.6.0
'@docsearch/js': 4.6.0
@ -11222,7 +11424,7 @@ snapshots:
'@vue/devtools-api': 8.1.0
'@vue/shared': 3.5.30
'@vueuse/core': 14.2.1(vue@3.5.30(typescript@6.0.3))
'@vueuse/integrations': 14.2.1(focus-trap@8.0.0)(jwt-decode@4.0.0)(vue@3.5.30(typescript@6.0.3))
'@vueuse/integrations': 14.2.1(change-case@5.4.4)(focus-trap@8.0.0)(jwt-decode@4.0.0)(vue@3.5.30(typescript@6.0.3))
focus-trap: 8.0.0
mark.js: 8.11.1
minisearch: 7.2.0
@ -11417,6 +11619,8 @@ snapshots:
yallist@5.0.0: {}
yaml-ast-parser@0.0.43: {}
yargs-parser@21.1.1: {}
yargs-unparser@2.0.0:

View file

@ -769,3 +769,6 @@ const generateServerForApiVersion = (apiRoot:string, req:any): {
} => ({
url: `${settings.ssl ? 'https' : 'http'}://${req.headers.host}${apiRoot}`,
});
exports.generateDefinitionForVersion = generateDefinitionForVersion;
exports.APIPathStyle = APIPathStyle;