diff --git a/frontend/package.json b/frontend/package.json index 905bb63b..524f4a9d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,7 +14,7 @@ "preview": "vite preview", "lint": "eslint src", "typecheck": "tsc --noEmit", - "gen:api": "node scripts/build-openapi.mjs" + "gen:api": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/build-openapi.mjs" }, "dependencies": { "@ant-design/icons": "^6.2.3", diff --git a/frontend/scripts/build-openapi.mjs b/frontend/scripts/build-openapi.mjs index de64b86e..f89e1d66 100644 --- a/frontend/scripts/build-openapi.mjs +++ b/frontend/scripts/build-openapi.mjs @@ -3,7 +3,7 @@ import { writeFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; -import { sections } from '../src/pages/api-docs/endpoints.js'; +import { sections } from '../src/pages/api-docs/endpoints.ts'; const __dirname = dirname(fileURLToPath(import.meta.url)); const outPath = join(__dirname, '..', 'public', 'openapi.json'); diff --git a/frontend/src/pages/api-docs/endpoints.js b/frontend/src/pages/api-docs/endpoints.ts similarity index 98% rename from frontend/src/pages/api-docs/endpoints.js rename to frontend/src/pages/api-docs/endpoints.ts index 4efeefb3..6fc4e5a5 100644 --- a/frontend/src/pages/api-docs/endpoints.js +++ b/frontend/src/pages/api-docs/endpoints.ts @@ -1,29 +1,59 @@ -export function safeInlineHtml(input) { - if (!input) return ''; - const escape = (s) => s.replace(/&/g, '&').replace(//g, '>'); - const open = ''; - const close = ''; - let out = ''; - let i = 0; - while (i < input.length) { - const oIdx = input.indexOf(open, i); - if (oIdx === -1) { - out += escape(input.slice(i)); - break; - } - out += escape(input.slice(i, oIdx)); - const cIdx = input.indexOf(close, oIdx + open.length); - if (cIdx === -1) { - out += escape(input.slice(oIdx)); - break; - } - out += '' + escape(input.slice(oIdx + open.length, cIdx)) + ''; - i = cIdx + close.length; - } - return out; +export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'WS'; +export type ParamLocation = + | 'path' + | 'query' + | 'header' + | 'body' + | 'body (form)' + | 'body (json)' + | 'body (multipart)'; +export type ParamType = + | 'string' + | 'integer' + | 'integer[]' + | 'number' + | 'boolean' + | 'object' + | 'object[]' + | 'array' + | 'file'; + +export interface EndpointParam { + name: string; + in: ParamLocation; + type: ParamType; + desc?: string; + optional?: boolean; + defaultValue?: string | number | boolean; } -export const sections = [ +export interface Endpoint { + method: HttpMethod; + path: string; + summary: string; + description?: string; + deprecated?: boolean; + params?: EndpointParam[]; + body?: string; + response?: string; + errorResponse?: string; + errorStatus?: number; +} + +export interface SubscriptionHeader { + name: string; + desc: string; +} + +export interface Section { + id: string; + title: string; + description?: string; + subHeader?: SubscriptionHeader[]; + endpoints: Endpoint[]; +} + +export const sections: readonly Section[] = [ { id: 'authentication', title: 'Authentication', @@ -975,12 +1005,3 @@ export const sections = [ ], }, ]; - -export const methodColors = { - GET: 'blue', - POST: 'green', - PUT: 'orange', - PATCH: 'orange', - DELETE: 'red', - WS: 'purple', -}; diff --git a/web/controller/api_docs_test.go b/web/controller/api_docs_test.go index f91e4b7a..ad1f8d08 100644 --- a/web/controller/api_docs_test.go +++ b/web/controller/api_docs_test.go @@ -16,10 +16,10 @@ type routeDef struct { // routePattern matches route registrations like g.GET("/path", handler) or api.GET("/path", handler) var routePattern = regexp.MustCompile(`\b(g|api)\.(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\("([^"]+)"`) -// docRoutePattern matches { method: 'X', path: 'Y' ... } entries in endpoints.js. +// docRoutePattern matches { method: 'X', path: 'Y' ... } entries in endpoints.ts. var docRoutePattern = regexp.MustCompile(`method:\s*'([A-Z]+)'\s*,\s*path:\s*'([^']+)'`) -// buildDocSet parses frontend/src/pages/api-docs/endpoints.js and returns the +// buildDocSet parses frontend/src/pages/api-docs/endpoints.ts and returns the // set of documented "METHOD PATH" keys. WS pseudo-routes and subscription // placeholders (paths starting with /{...}) are skipped because they aren't // registered on the main Gin engine. @@ -29,10 +29,10 @@ func buildDocSet(t *testing.T) map[string]bool { if err != nil { t.Fatalf("failed to get current dir: %v", err) } - endpointsPath := filepath.Join(controllerDir, "..", "..", "frontend", "src", "pages", "api-docs", "endpoints.js") + endpointsPath := filepath.Join(controllerDir, "..", "..", "frontend", "src", "pages", "api-docs", "endpoints.ts") data, err := os.ReadFile(endpointsPath) if err != nil { - t.Fatalf("failed to read endpoints.js at %s: %v", endpointsPath, err) + t.Fatalf("failed to read endpoints.ts at %s: %v", endpointsPath, err) } docSet := make(map[string]bool) for _, m := range docRoutePattern.FindAllStringSubmatch(string(data), -1) { @@ -150,7 +150,7 @@ func TestAPIRoutesDocumented(t *testing.T) { foundInDoc++ } else { missingFromDocs++ - t.Errorf("Route not documented in endpoints.js: %s %s", r.Method, r.Path) + t.Errorf("Route not documented in endpoints.ts: %s %s", r.Method, r.Path) } } @@ -158,6 +158,6 @@ func TestAPIRoutesDocumented(t *testing.T) { len(sourceSet), len(docSet), foundInDoc, missingFromDocs) if missingFromDocs > 0 { - t.Errorf("Found %d undocumented route(s). Update endpoints.js to match.", missingFromDocs) + t.Errorf("Found %d undocumented route(s). Update endpoints.ts to match.", missingFromDocs) } }