fix(httpapi): document instance query parameters (#24809)
This commit is contained in:
@@ -118,7 +118,6 @@ export const PtyConnectApi = HttpApi.make("pty-connect").add(
|
|||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("connect", PtyPaths.connect, {
|
HttpApiEndpoint.get("connect", PtyPaths.connect, {
|
||||||
params: Params,
|
params: Params,
|
||||||
query: CursorQuery,
|
|
||||||
success: Schema.Boolean,
|
success: Schema.Boolean,
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
|
|||||||
@@ -17,6 +17,56 @@ import { SyncApi } from "./sync"
|
|||||||
import { TuiApi } from "./tui"
|
import { TuiApi } from "./tui"
|
||||||
import { WorkspaceApi } from "./workspace"
|
import { WorkspaceApi } from "./workspace"
|
||||||
|
|
||||||
|
type OpenApiParameter = {
|
||||||
|
name: string
|
||||||
|
in: string
|
||||||
|
required?: boolean
|
||||||
|
schema?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenApiOperation = {
|
||||||
|
parameters?: OpenApiParameter[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenApiPathItem = Partial<Record<"get" | "post" | "put" | "delete" | "patch", OpenApiOperation>>
|
||||||
|
|
||||||
|
type OpenApiSpec = {
|
||||||
|
paths?: Record<string, OpenApiPathItem>
|
||||||
|
}
|
||||||
|
|
||||||
|
const InstanceQueryParameters = [
|
||||||
|
{
|
||||||
|
name: "directory",
|
||||||
|
in: "query",
|
||||||
|
required: false,
|
||||||
|
schema: { type: "string" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "workspace",
|
||||||
|
in: "query",
|
||||||
|
required: false,
|
||||||
|
schema: { type: "string" },
|
||||||
|
},
|
||||||
|
] satisfies OpenApiParameter[]
|
||||||
|
|
||||||
|
function documentInstanceQueryParameters(input: Record<string, unknown>) {
|
||||||
|
const spec = input as OpenApiSpec
|
||||||
|
for (const [path, item] of Object.entries(spec.paths ?? {})) {
|
||||||
|
if (path.startsWith("/global/") || path.startsWith("/auth/")) continue
|
||||||
|
for (const method of ["get", "post", "put", "delete", "patch"] as const) {
|
||||||
|
const operation = item[method]
|
||||||
|
if (!operation) continue
|
||||||
|
operation.parameters = [
|
||||||
|
...InstanceQueryParameters,
|
||||||
|
...(operation.parameters ?? []).filter(
|
||||||
|
(param) => param.in !== "query" || (param.name !== "directory" && param.name !== "workspace"),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
export const PublicApi = HttpApi.make("opencode")
|
export const PublicApi = HttpApi.make("opencode")
|
||||||
.addHttpApi(ControlApi)
|
.addHttpApi(ControlApi)
|
||||||
.addHttpApi(GlobalApi)
|
.addHttpApi(GlobalApi)
|
||||||
@@ -41,5 +91,6 @@ export const PublicApi = HttpApi.make("opencode")
|
|||||||
title: "opencode",
|
title: "opencode",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
description: "opencode api",
|
description: "opencode api",
|
||||||
|
transform: documentInstanceQueryParameters,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -34,6 +34,32 @@ function openApiRouteKeys(spec: { paths: Record<string, Partial<Record<(typeof m
|
|||||||
.sort()
|
.sort()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openApiParameters(spec: { paths: Record<string, Partial<Record<(typeof methods)[number], Operation>>> }) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(spec.paths).flatMap(([path, item]) =>
|
||||||
|
methods
|
||||||
|
.filter((method) => item[method])
|
||||||
|
.map((method) => [
|
||||||
|
`${method.toUpperCase()} ${path}`,
|
||||||
|
(item[method]?.parameters ?? [])
|
||||||
|
.map(parameterKey)
|
||||||
|
.filter((param) => param !== undefined)
|
||||||
|
.sort(),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Operation = {
|
||||||
|
parameters?: unknown[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function parameterKey(param: unknown) {
|
||||||
|
if (!param || typeof param !== "object" || !("in" in param) || !("name" in param)) return
|
||||||
|
if (typeof param.in !== "string" || typeof param.name !== "string") return
|
||||||
|
return `${param.in}:${param.name}:${"required" in param && param.required === true}`
|
||||||
|
}
|
||||||
|
|
||||||
function authorization(username: string, password: string) {
|
function authorization(username: string, password: string) {
|
||||||
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
|
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
|
||||||
}
|
}
|
||||||
@@ -63,6 +89,17 @@ describe("HttpApi server", () => {
|
|||||||
expect(effectRoutes.filter((route) => !honoRoutes.includes(route))).toEqual([])
|
expect(effectRoutes.filter((route) => !honoRoutes.includes(route))).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("matches generated OpenAPI route parameters", async () => {
|
||||||
|
const hono = openApiParameters(await Server.openapi())
|
||||||
|
const effect = openApiParameters(OpenApi.fromApi(PublicApi))
|
||||||
|
|
||||||
|
expect(
|
||||||
|
Object.keys(hono)
|
||||||
|
.filter((route) => JSON.stringify(hono[route]) !== JSON.stringify(effect[route]))
|
||||||
|
.map((route) => ({ route, hono: hono[route], effect: effect[route] })),
|
||||||
|
).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
test("allows requests when auth is disabled", async () => {
|
test("allows requests when auth is disabled", async () => {
|
||||||
await using tmp = await tmpdir({ git: true })
|
await using tmp = await tmpdir({ git: true })
|
||||||
await Bun.write(`${tmp.path}/hello.txt`, "hello")
|
await Bun.write(`${tmp.path}/hello.txt`, "hello")
|
||||||
|
|||||||
Reference in New Issue
Block a user