From b97baa3ef524cbe3c24559ca4d95c31a338ca786 Mon Sep 17 00:00:00 2001 From: John McLear Date: Thu, 7 May 2026 22:21:41 +0800 Subject: [PATCH] chore(admin): typesafe API client + TanStack Query rails (#7638) (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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) * 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) * 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) * chore(admin): add OpenAPI codegen + TanStack Query deps (#7638) Co-Authored-By: Claude Opus 4.7 (1M context) * 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) * 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) * feat(admin): typed openapi-fetch + react-query client (#7638) Co-Authored-By: Claude Opus 4.7 (1M context) * feat(admin): TanStack Query provider, dev-only devtools (#7638) Co-Authored-By: Claude Opus 4.7 (1M context) * feat(admin): mount TanStack Query provider at root (#7638) Co-Authored-By: Claude Opus 4.7 (1M context) * test(admin): smoke test for typed openapi-fetch client (#7638) Co-Authored-By: Claude Opus 4.7 (1M context) * ci(admin): verify generated OpenAPI schema is up to date (#7638) Co-Authored-By: Claude Opus 4.7 (1M context) * 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) * 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) * 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) * 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) * fix(admin): point client baseUrl at /api/ 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//. 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/`. client.ts imports API_BASE_URL. Workflow freshness check covers both generated files. Co-Authored-By: Claude Opus 4.7 (1M context) * 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) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .github/workflows/frontend-admin-tests.yml | 10 + admin/README.md | 79 +- admin/package.json | 16 +- admin/scripts/dump-spec.ts | 46 + admin/scripts/gen-api.mjs | 78 + admin/src/api/QueryProvider.tsx | 40 + admin/src/api/__tests__/client.test.ts | 16 + admin/src/api/client.ts | 12 + admin/src/api/schema.d.ts | 3567 +++++++++++++++++ admin/src/api/version.ts | 5 + admin/src/main.tsx | 15 +- admin/tsconfig.json | 1 + ...026-05-01-issue-7638-admin-typesafe-api.md | 804 ++++ ...01-issue-7638-admin-typesafe-api-design.md | 198 + pnpm-lock.yaml | 240 +- src/node/hooks/express/openapi.ts | 3 + 16 files changed, 5081 insertions(+), 49 deletions(-) create mode 100644 admin/scripts/dump-spec.ts create mode 100644 admin/scripts/gen-api.mjs create mode 100644 admin/src/api/QueryProvider.tsx create mode 100644 admin/src/api/__tests__/client.test.ts create mode 100644 admin/src/api/client.ts create mode 100644 admin/src/api/schema.d.ts create mode 100644 admin/src/api/version.ts create mode 100644 docs/superpowers/plans/2026-05-01-issue-7638-admin-typesafe-api.md create mode 100644 docs/superpowers/specs/2026-05-01-issue-7638-admin-typesafe-api-design.md diff --git a/.github/workflows/frontend-admin-tests.yml b/.github/workflows/frontend-admin-tests.yml index 544f801b3..b67812eac 100644 --- a/.github/workflows/frontend-admin-tests.yml +++ b/.github/workflows/frontend-admin-tests.yml @@ -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: | diff --git a/admin/README.md b/admin/README.md index 0d6babedd..51b7a9703 100644 --- a/admin/README.md +++ b/admin/README.md @@ -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//` 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
{JSON.stringify(data, null, 2)}
; +}; +``` + +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. diff --git a/admin/package.json b/admin/package.json index 8bfb022db..34d946d93 100644 --- a/admin/package.json +++ b/admin/package.json @@ -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", diff --git a/admin/scripts/dump-spec.ts b/admin/scripts/dump-spec.ts new file mode 100644 index 000000000..2f2c388a5 --- /dev/null +++ b/admin/scripts/dump-spec.ts @@ -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 \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'); diff --git a/admin/scripts/gen-api.mjs b/admin/scripts/gen-api.mjs new file mode 100644 index 000000000..d96383e25 --- /dev/null +++ b/admin/scripts/gen-api.mjs @@ -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//. + 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 }); +} diff --git a/admin/src/api/QueryProvider.tsx b/admin/src/api/QueryProvider.tsx new file mode 100644 index 000000000..54ee2d95c --- /dev/null +++ b/admin/src/api/QueryProvider.tsx @@ -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 ( + + {children} + {Devtools && ( + + + + )} + + ); +}; diff --git a/admin/src/api/__tests__/client.test.ts b/admin/src/api/__tests__/client.test.ts new file mode 100644 index 000000000..9e9b664d7 --- /dev/null +++ b/admin/src/api/__tests__/client.test.ts @@ -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'); +}); diff --git a/admin/src/api/client.ts b/admin/src/api/client.ts new file mode 100644 index 000000000..4f1037316 --- /dev/null +++ b/admin/src/api/client.ts @@ -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({ baseUrl: API_BASE_URL }); +export const $api = createQueryHooks(fetchClient); diff --git a/admin/src/api/schema.d.ts b/admin/src/api/schema.d.ts new file mode 100644 index 000000000..b4c0cb7c5 --- /dev/null +++ b/admin/src/api/schema.d.ts @@ -0,0 +1,3567 @@ +// GENERATED — do not edit. Run `pnpm --filter admin gen:api` to regenerate. +// Source: src/node/hooks/express/openapi.ts (#7638) + +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/createGroup": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** creates a new group */ + get: operations["createGroupUsingGET"]; + put?: never; + /** creates a new group */ + post: operations["createGroupUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/createGroupIfNotExistsFor": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** this functions helps you to map your application group ids to Etherpad group ids */ + get: operations["createGroupIfNotExistsForUsingGET"]; + put?: never; + /** this functions helps you to map your application group ids to Etherpad group ids */ + post: operations["createGroupIfNotExistsForUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/deleteGroup": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** deletes a group */ + get: operations["deleteGroupUsingGET"]; + put?: never; + /** deletes a group */ + post: operations["deleteGroupUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listPads": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** returns all pads of this group */ + get: operations["listPadsUsingGET"]; + put?: never; + /** returns all pads of this group */ + post: operations["listPadsUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/createPad": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description creates a new (non-group) pad. Note that if you need to create a group Pad, you should call createGroupPad */ + get: operations["createPadUsingGET"]; + put?: never; + /** @description creates a new (non-group) pad. Note that if you need to create a group Pad, you should call createGroupPad */ + post: operations["createPadUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/createGroupPad": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** creates a new pad in this group */ + get: operations["createGroupPadUsingGET"]; + put?: never; + /** creates a new pad in this group */ + post: operations["createGroupPadUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/createAuthor": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** creates a new author */ + get: operations["createAuthorUsingGET"]; + put?: never; + /** creates a new author */ + post: operations["createAuthorUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/createAuthorIfNotExistsFor": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** this functions helps you to map your application author ids to Etherpad author ids */ + get: operations["createAuthorIfNotExistsForUsingGET"]; + put?: never; + /** this functions helps you to map your application author ids to Etherpad author ids */ + post: operations["createAuthorIfNotExistsForUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listPadsOfAuthor": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** returns an array of all pads this author contributed to */ + get: operations["listPadsOfAuthorUsingGET"]; + put?: never; + /** returns an array of all pads this author contributed to */ + post: operations["listPadsOfAuthorUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/createSession": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** creates a new session. validUntil is an unix timestamp in seconds */ + get: operations["createSessionUsingGET"]; + put?: never; + /** creates a new session. validUntil is an unix timestamp in seconds */ + post: operations["createSessionUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/deleteSession": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** deletes a session */ + get: operations["deleteSessionUsingGET"]; + put?: never; + /** deletes a session */ + post: operations["deleteSessionUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/getSessionInfo": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** returns information about a session */ + get: operations["getSessionInfoUsingGET"]; + put?: never; + /** returns information about a session */ + post: operations["getSessionInfoUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listSessionsOfGroup": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listSessionsOfGroupUsingGET"]; + put?: never; + post: operations["listSessionsOfGroupUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listSessionsOfAuthor": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** returns all sessions of an author */ + get: operations["listSessionsOfAuthorUsingGET"]; + put?: never; + /** returns all sessions of an author */ + post: operations["listSessionsOfAuthorUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/getText": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** returns the text of a pad */ + get: operations["getTextUsingGET"]; + put?: never; + /** returns the text of a pad */ + post: operations["getTextUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/setText": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** sets the text of a pad */ + get: operations["setTextUsingGET"]; + put?: never; + /** sets the text of a pad */ + post: operations["setTextUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/getHTML": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** returns the text of a pad formatted as HTML */ + get: operations["getHTMLUsingGET"]; + put?: never; + /** returns the text of a pad formatted as HTML */ + post: operations["getHTMLUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/setHTML": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** sets the text of a pad with HTML */ + get: operations["setHTMLUsingGET"]; + put?: never; + /** sets the text of a pad with HTML */ + post: operations["setHTMLUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/getRevisionsCount": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** returns the number of revisions of this pad */ + get: operations["getRevisionsCountUsingGET"]; + put?: never; + /** returns the number of revisions of this pad */ + post: operations["getRevisionsCountUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/getLastEdited": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** returns the timestamp of the last revision of the pad */ + get: operations["getLastEditedUsingGET"]; + put?: never; + /** returns the timestamp of the last revision of the pad */ + post: operations["getLastEditedUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/deletePad": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** deletes a pad */ + get: operations["deletePadUsingGET"]; + put?: never; + /** deletes a pad */ + post: operations["deletePadUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/getReadOnlyID": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** returns the read only link of a pad */ + get: operations["getReadOnlyIDUsingGET"]; + put?: never; + /** returns the read only link of a pad */ + post: operations["getReadOnlyIDUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/setPublicStatus": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** sets a boolean for the public status of a pad */ + get: operations["setPublicStatusUsingGET"]; + put?: never; + /** sets a boolean for the public status of a pad */ + post: operations["setPublicStatusUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/getPublicStatus": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** return true of false */ + get: operations["getPublicStatusUsingGET"]; + put?: never; + /** return true of false */ + post: operations["getPublicStatusUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listAuthorsOfPad": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** returns an array of authors who contributed to this pad */ + get: operations["listAuthorsOfPadUsingGET"]; + put?: never; + /** returns an array of authors who contributed to this pad */ + post: operations["listAuthorsOfPadUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/padUsersCount": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** returns the number of user that are currently editing this pad */ + get: operations["padUsersCountUsingGET"]; + put?: never; + /** returns the number of user that are currently editing this pad */ + post: operations["padUsersCountUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/getAuthorName": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Returns the Author Name of the author */ + get: operations["getAuthorNameUsingGET"]; + put?: never; + /** Returns the Author Name of the author */ + post: operations["getAuthorNameUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/padUsers": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** returns the list of users that are currently editing this pad */ + get: operations["padUsersUsingGET"]; + put?: never; + /** returns the list of users that are currently editing this pad */ + post: operations["padUsersUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sendClientsMessage": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** sends a custom message of type msg to the pad */ + get: operations["sendClientsMessageUsingGET"]; + put?: never; + /** sends a custom message of type msg to the pad */ + post: operations["sendClientsMessageUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listAllGroups": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listAllGroupsUsingGET"]; + put?: never; + post: operations["listAllGroupsUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/checkToken": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** returns ok when the current api token is valid */ + get: operations["checkTokenUsingGET"]; + put?: never; + /** returns ok when the current api token is valid */ + post: operations["checkTokenUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listAllPads": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** list all the pads */ + get: operations["listAllPadsUsingGET"]; + put?: never; + /** list all the pads */ + post: operations["listAllPadsUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/createDiffHTML": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["createDiffHTMLUsingGET"]; + put?: never; + post: operations["createDiffHTMLUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/getChatHistory": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** returns the chat history */ + get: operations["getChatHistoryUsingGET"]; + put?: never; + /** returns the chat history */ + post: operations["getChatHistoryUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/getChatHead": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** returns the chatHead (chat-message) of the pad */ + get: operations["getChatHeadUsingGET"]; + put?: never; + /** returns the chatHead (chat-message) of the pad */ + post: operations["getChatHeadUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/getAttributePool": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getAttributePoolUsingGET"]; + put?: never; + post: operations["getAttributePoolUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/getRevisionChangeset": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getRevisionChangesetUsingGET"]; + put?: never; + post: operations["getRevisionChangesetUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/copyPad": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["copyPadUsingGET"]; + put?: never; + post: operations["copyPadUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/movePad": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["movePadUsingGET"]; + put?: never; + post: operations["movePadUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/getPadID": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getPadIDUsingGET"]; + put?: never; + post: operations["getPadIDUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/getSavedRevisionsCount": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getSavedRevisionsCountUsingGET"]; + put?: never; + post: operations["getSavedRevisionsCountUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/listSavedRevisions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listSavedRevisionsUsingGET"]; + put?: never; + post: operations["listSavedRevisionsUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/saveRevision": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["saveRevisionUsingGET"]; + put?: never; + post: operations["saveRevisionUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/restoreRevision": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["restoreRevisionUsingGET"]; + put?: never; + post: operations["restoreRevisionUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/appendChatMessage": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** appends a chat message */ + get: operations["appendChatMessageUsingGET"]; + put?: never; + /** appends a chat message */ + post: operations["appendChatMessageUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/appendText": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["appendTextUsingGET"]; + put?: never; + post: operations["appendTextUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/getStats": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getStatsUsingGET"]; + put?: never; + post: operations["getStatsUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/copyPadWithoutHistory": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["copyPadWithoutHistoryUsingGET"]; + put?: never; + post: operations["copyPadWithoutHistoryUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/compactPad": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["compactPadUsingGET"]; + put?: never; + post: operations["compactPadUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/anonymizeAuthor": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["anonymizeAuthorUsingGET"]; + put?: never; + post: operations["anonymizeAuthorUsingPOST"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + SessionInfo: { + id?: string; + authorID?: string; + groupID?: string; + validUntil?: number; + }; + UserInfo: { + id?: string; + colorId?: string; + name?: string; + timestamp?: number; + }; + Message: { + text?: string; + userId?: string; + userName?: string; + time?: number; + }; + }; + responses: { + /** @description ok (code 0) */ + Success: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + /** @example null */ + data?: Record; + }; + }; + }; + /** @description generic api error (code 1) */ + ApiError: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 1 */ + code?: number; + /** @example error message */ + message?: string; + /** @example null */ + data?: Record; + }; + }; + }; + /** @description internal api error (code 2) */ + InternalError: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 2 */ + code?: number; + /** @example internal error */ + message?: string; + /** @example null */ + data?: Record; + }; + }; + }; + /** @description no such function (code 4) */ + NotFound: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 3 */ + code?: number; + /** @example no such function */ + message?: string; + /** @example null */ + data?: Record; + }; + }; + }; + /** @description no or wrong API key (code 4) */ + Unauthorized: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 4 */ + code?: number; + /** @example no or wrong API key */ + message?: string; + /** @example null */ + data?: Record; + }; + }; + }; + }; + parameters: { + groupMapper: string; + groupID: string; + padID: string; + text: string; + authorId: string; + padName: string; + name: string; + authorMapper: string; + authorID: string; + validUntil: string; + sessionID: string; + rev: string; + html: string; + deletionToken: string; + publicStatus: string; + msg: string; + startRev: string; + endRev: string; + start: string; + end: string; + sourceID: string; + destinationID: string; + force: string; + roID: string; + time: string; + keepRevisions: string; + }; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + createGroupUsingGET: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + groupID?: string; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + createGroupUsingPOST: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + groupID?: string; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + createGroupIfNotExistsForUsingGET: { + parameters: { + query?: { + groupMapper?: components["parameters"]["groupMapper"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + groupID?: string; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + createGroupIfNotExistsForUsingPOST: { + parameters: { + query?: { + groupMapper?: components["parameters"]["groupMapper"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + groupID?: string; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + deleteGroupUsingGET: { + parameters: { + query?: { + groupID?: components["parameters"]["groupID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + deleteGroupUsingPOST: { + parameters: { + query?: { + groupID?: components["parameters"]["groupID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + listPadsUsingGET: { + parameters: { + query?: { + groupID?: components["parameters"]["groupID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + padIDs?: string[]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + listPadsUsingPOST: { + parameters: { + query?: { + groupID?: components["parameters"]["groupID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + padIDs?: string[]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + createPadUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + text?: components["parameters"]["text"]; + authorId?: components["parameters"]["authorId"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + createPadUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + text?: components["parameters"]["text"]; + authorId?: components["parameters"]["authorId"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + createGroupPadUsingGET: { + parameters: { + query?: { + groupID?: components["parameters"]["groupID"]; + padName?: components["parameters"]["padName"]; + text?: components["parameters"]["text"]; + authorId?: components["parameters"]["authorId"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + createGroupPadUsingPOST: { + parameters: { + query?: { + groupID?: components["parameters"]["groupID"]; + padName?: components["parameters"]["padName"]; + text?: components["parameters"]["text"]; + authorId?: components["parameters"]["authorId"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + createAuthorUsingGET: { + parameters: { + query?: { + name?: components["parameters"]["name"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + authorID?: string; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + createAuthorUsingPOST: { + parameters: { + query?: { + name?: components["parameters"]["name"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + authorID?: string; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + createAuthorIfNotExistsForUsingGET: { + parameters: { + query?: { + authorMapper?: components["parameters"]["authorMapper"]; + name?: components["parameters"]["name"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + authorID?: string; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + createAuthorIfNotExistsForUsingPOST: { + parameters: { + query?: { + authorMapper?: components["parameters"]["authorMapper"]; + name?: components["parameters"]["name"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + authorID?: string; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + listPadsOfAuthorUsingGET: { + parameters: { + query?: { + authorID?: components["parameters"]["authorID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + padIDs?: string[]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + listPadsOfAuthorUsingPOST: { + parameters: { + query?: { + authorID?: components["parameters"]["authorID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + padIDs?: string[]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + createSessionUsingGET: { + parameters: { + query?: { + groupID?: components["parameters"]["groupID"]; + authorID?: components["parameters"]["authorID"]; + validUntil?: components["parameters"]["validUntil"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + sessionID?: string; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + createSessionUsingPOST: { + parameters: { + query?: { + groupID?: components["parameters"]["groupID"]; + authorID?: components["parameters"]["authorID"]; + validUntil?: components["parameters"]["validUntil"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + sessionID?: string; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + deleteSessionUsingGET: { + parameters: { + query?: { + sessionID?: components["parameters"]["sessionID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + deleteSessionUsingPOST: { + parameters: { + query?: { + sessionID?: components["parameters"]["sessionID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getSessionInfoUsingGET: { + parameters: { + query?: { + sessionID?: components["parameters"]["sessionID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + info?: components["schemas"]["SessionInfo"]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getSessionInfoUsingPOST: { + parameters: { + query?: { + sessionID?: components["parameters"]["sessionID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + info?: components["schemas"]["SessionInfo"]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + listSessionsOfGroupUsingGET: { + parameters: { + query?: { + groupID?: components["parameters"]["groupID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + sessions?: components["schemas"]["SessionInfo"][]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + listSessionsOfGroupUsingPOST: { + parameters: { + query?: { + groupID?: components["parameters"]["groupID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + sessions?: components["schemas"]["SessionInfo"][]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + listSessionsOfAuthorUsingGET: { + parameters: { + query?: { + authorID?: components["parameters"]["authorID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + sessions?: components["schemas"]["SessionInfo"][]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + listSessionsOfAuthorUsingPOST: { + parameters: { + query?: { + authorID?: components["parameters"]["authorID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + sessions?: components["schemas"]["SessionInfo"][]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getTextUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + rev?: components["parameters"]["rev"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + text?: string; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getTextUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + rev?: components["parameters"]["rev"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + text?: string; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + setTextUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + text?: components["parameters"]["text"]; + authorId?: components["parameters"]["authorId"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + setTextUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + text?: components["parameters"]["text"]; + authorId?: components["parameters"]["authorId"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getHTMLUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + rev?: components["parameters"]["rev"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + html?: string; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getHTMLUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + rev?: components["parameters"]["rev"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + html?: string; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + setHTMLUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + html?: components["parameters"]["html"]; + authorId?: components["parameters"]["authorId"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + setHTMLUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + html?: components["parameters"]["html"]; + authorId?: components["parameters"]["authorId"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getRevisionsCountUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + revisions?: number; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getRevisionsCountUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + revisions?: number; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getLastEditedUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + lastEdited?: number; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getLastEditedUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + lastEdited?: number; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + deletePadUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + deletionToken?: components["parameters"]["deletionToken"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + deletePadUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + deletionToken?: components["parameters"]["deletionToken"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getReadOnlyIDUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + readOnlyID?: string; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getReadOnlyIDUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + readOnlyID?: string; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + setPublicStatusUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + publicStatus?: components["parameters"]["publicStatus"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + setPublicStatusUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + publicStatus?: components["parameters"]["publicStatus"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getPublicStatusUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + publicStatus?: boolean; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getPublicStatusUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + publicStatus?: boolean; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + listAuthorsOfPadUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + authorIDs?: string[]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + listAuthorsOfPadUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + authorIDs?: string[]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + padUsersCountUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + padUsersCount?: number; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + padUsersCountUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + padUsersCount?: number; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getAuthorNameUsingGET: { + parameters: { + query?: { + authorID?: components["parameters"]["authorID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + info?: components["schemas"]["UserInfo"]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getAuthorNameUsingPOST: { + parameters: { + query?: { + authorID?: components["parameters"]["authorID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + info?: components["schemas"]["UserInfo"]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + padUsersUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + padUsers?: components["schemas"]["UserInfo"][]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + padUsersUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + padUsers?: components["schemas"]["UserInfo"][]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + sendClientsMessageUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + msg?: components["parameters"]["msg"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + sendClientsMessageUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + msg?: components["parameters"]["msg"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + listAllGroupsUsingGET: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + groupIDs?: string[]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + listAllGroupsUsingPOST: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + groupIDs?: string[]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + checkTokenUsingGET: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + checkTokenUsingPOST: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + listAllPadsUsingGET: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + padIDs?: string[]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + listAllPadsUsingPOST: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + padIDs?: string[]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + createDiffHTMLUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + startRev?: components["parameters"]["startRev"]; + endRev?: components["parameters"]["endRev"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: Record; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + createDiffHTMLUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + startRev?: components["parameters"]["startRev"]; + endRev?: components["parameters"]["endRev"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: Record; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getChatHistoryUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + start?: components["parameters"]["start"]; + end?: components["parameters"]["end"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + messages?: components["schemas"]["Message"][]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getChatHistoryUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + start?: components["parameters"]["start"]; + end?: components["parameters"]["end"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + messages?: components["schemas"]["Message"][]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getChatHeadUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + chatHead?: components["schemas"]["Message"]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getChatHeadUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ok (code 0) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 0 */ + code?: number; + /** @example ok */ + message?: string; + data?: { + chatHead?: components["schemas"]["Message"]; + }; + }; + }; + }; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getAttributePoolUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getAttributePoolUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getRevisionChangesetUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + rev?: components["parameters"]["rev"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getRevisionChangesetUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + rev?: components["parameters"]["rev"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + copyPadUsingGET: { + parameters: { + query?: { + sourceID?: components["parameters"]["sourceID"]; + destinationID?: components["parameters"]["destinationID"]; + force?: components["parameters"]["force"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + copyPadUsingPOST: { + parameters: { + query?: { + sourceID?: components["parameters"]["sourceID"]; + destinationID?: components["parameters"]["destinationID"]; + force?: components["parameters"]["force"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + movePadUsingGET: { + parameters: { + query?: { + sourceID?: components["parameters"]["sourceID"]; + destinationID?: components["parameters"]["destinationID"]; + force?: components["parameters"]["force"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + movePadUsingPOST: { + parameters: { + query?: { + sourceID?: components["parameters"]["sourceID"]; + destinationID?: components["parameters"]["destinationID"]; + force?: components["parameters"]["force"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getPadIDUsingGET: { + parameters: { + query?: { + roID?: components["parameters"]["roID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getPadIDUsingPOST: { + parameters: { + query?: { + roID?: components["parameters"]["roID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getSavedRevisionsCountUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getSavedRevisionsCountUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + listSavedRevisionsUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + listSavedRevisionsUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + saveRevisionUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + rev?: components["parameters"]["rev"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + saveRevisionUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + rev?: components["parameters"]["rev"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + restoreRevisionUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + rev?: components["parameters"]["rev"]; + authorId?: components["parameters"]["authorId"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + restoreRevisionUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + rev?: components["parameters"]["rev"]; + authorId?: components["parameters"]["authorId"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + appendChatMessageUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + text?: components["parameters"]["text"]; + authorID?: components["parameters"]["authorID"]; + time?: components["parameters"]["time"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + appendChatMessageUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + text?: components["parameters"]["text"]; + authorID?: components["parameters"]["authorID"]; + time?: components["parameters"]["time"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + appendTextUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + text?: components["parameters"]["text"]; + authorId?: components["parameters"]["authorId"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + appendTextUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + text?: components["parameters"]["text"]; + authorId?: components["parameters"]["authorId"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getStatsUsingGET: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + getStatsUsingPOST: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + copyPadWithoutHistoryUsingGET: { + parameters: { + query?: { + sourceID?: components["parameters"]["sourceID"]; + destinationID?: components["parameters"]["destinationID"]; + force?: components["parameters"]["force"]; + authorId?: components["parameters"]["authorId"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + copyPadWithoutHistoryUsingPOST: { + parameters: { + query?: { + sourceID?: components["parameters"]["sourceID"]; + destinationID?: components["parameters"]["destinationID"]; + force?: components["parameters"]["force"]; + authorId?: components["parameters"]["authorId"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + compactPadUsingGET: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + keepRevisions?: components["parameters"]["keepRevisions"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + compactPadUsingPOST: { + parameters: { + query?: { + padID?: components["parameters"]["padID"]; + keepRevisions?: components["parameters"]["keepRevisions"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + anonymizeAuthorUsingGET: { + parameters: { + query?: { + authorID?: components["parameters"]["authorID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + anonymizeAuthorUsingPOST: { + parameters: { + query?: { + authorID?: components["parameters"]["authorID"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["Success"]; + 400: components["responses"]["ApiError"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; +} diff --git a/admin/src/api/version.ts b/admin/src/api/version.ts new file mode 100644 index 000000000..8e5a03f14 --- /dev/null +++ b/admin/src/api/version.ts @@ -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}`; diff --git a/admin/src/main.tsx b/admin/src/main.tsx index c7dcc456b..e5f6c8ab4 100644 --- a/admin/src/main.tsx +++ b/admin/src/main.tsx @@ -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( <>}> @@ -34,11 +35,13 @@ const router = createBrowserRouter(createRoutesFromElements( ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - - + + + + + + + + , ) diff --git a/admin/tsconfig.json b/admin/tsconfig.json index a7fc6fbf2..ae96c41eb 100644 --- a/admin/tsconfig.json +++ b/admin/tsconfig.json @@ -21,5 +21,6 @@ "noFallthroughCasesInSwitch": true }, "include": ["src"], + "exclude": ["src/**/__tests__/**"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/docs/superpowers/plans/2026-05-01-issue-7638-admin-typesafe-api.md b/docs/superpowers/plans/2026-05-01-issue-7638-admin-typesafe-api.md new file mode 100644 index 000000000..49623f4c6 --- /dev/null +++ b/docs/superpowers/plans/2026-05-01-issue-7638-admin-typesafe-api.md @@ -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 `` 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 ``. +- `.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) +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) +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) +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) +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({ 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) +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 ( + + {children} + {Devtools && ( + + + + )} + + ); +}; +``` + +- [ ] **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) +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 `` wraps `` wraps `` inside ``. + +- [ ] **Step 2: Edit `admin/src/main.tsx`** + +Add the import after the existing imports: + +```tsx +import { QueryProvider } from './api/QueryProvider.tsx'; +``` + +Wrap the existing `...` subtree in ``. The render block becomes: + +```tsx +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + + + + , +) +``` + +(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) +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) +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) +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
{JSON.stringify(data, null, 2)}
; +}; +``` + +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) +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`). +- `` 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 --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. diff --git a/docs/superpowers/specs/2026-05-01-issue-7638-admin-typesafe-api-design.md b/docs/superpowers/specs/2026-05-01-issue-7638-admin-typesafe-api-design.md new file mode 100644 index 000000000..35a0fa1a5 --- /dev/null +++ b/docs/superpowers/specs/2026-05-01-issue-7638-admin-typesafe-api-design.md @@ -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 -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({ 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 `` in ``. 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 `` 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.** `` wrapping `` 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. +- `` wraps ``; 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. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae526f738..e7fafd9e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/src/node/hooks/express/openapi.ts b/src/node/hooks/express/openapi.ts index e07daf6d8..6eb420f28 100644 --- a/src/node/hooks/express/openapi.ts +++ b/src/node/hooks/express/openapi.ts @@ -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;