Scope boolean query overrides

This commit is contained in:
Kit Langton
2026-05-10 11:57:52 -04:00
committed by GitHub
parent c104098a66
commit 11030c627b
3 changed files with 33 additions and 30 deletions
@@ -6,3 +6,7 @@ export const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
encode: SchemaGetter.transform((value) => (value ? "true" : "false")), encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
}), }),
) )
export const QueryBooleanOpenApi = {
anyOf: [{ type: "boolean" }, { type: "string", enum: ["true", "false"] }],
}
@@ -1,5 +1,6 @@
import { OpenApi } from "effect/unstable/httpapi" import { OpenApi } from "effect/unstable/httpapi"
import { OpenCodeHttpApi } from "./api" import { OpenCodeHttpApi } from "./api"
import { QueryBooleanOpenApi } from "./groups/query"
type OpenApiParameter = { type OpenApiParameter = {
name: string name: string
@@ -54,17 +55,20 @@ type OpenApiResponse = {
// Query schemas describe decoded Effect values, but the generated SDK needs the // Query schemas describe decoded Effect values, but the generated SDK needs the
// public call shape. These keep SDK callers passing numbers/booleans while the // public call shape. These keep SDK callers passing numbers/booleans while the
// server still decodes string query params at runtime. // server still decodes string query params at runtime.
const QueryBooleanParameters = new Set(["roots", "archived"])
const QueryParameterSchemas: Record<string, OpenApiSchema> = { const QueryParameterSchemas: Record<string, OpenApiSchema> = {
"GET /experimental/session start": { type: "number" }, "GET /experimental/session start": { type: "number" },
"GET /experimental/session roots": QueryBooleanOpenApi,
"GET /experimental/session archived": QueryBooleanOpenApi,
"GET /find/file limit": { type: "integer", minimum: 1, maximum: 200 }, "GET /find/file limit": { type: "integer", minimum: 1, maximum: 200 },
"GET /experimental/session cursor": { type: "number" }, "GET /experimental/session cursor": { type: "number" },
"GET /experimental/session limit": { type: "number" }, "GET /experimental/session limit": { type: "number" },
"GET /session start": { type: "number" }, "GET /session start": { type: "number" },
"GET /session roots": QueryBooleanOpenApi,
"GET /session limit": { type: "number" }, "GET /session limit": { type: "number" },
"GET /session/{sessionID}/message limit": { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER }, "GET /session/{sessionID}/message limit": { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
"GET /api/session limit": { type: "number" }, "GET /api/session limit": { type: "number" },
"GET /api/session start": { type: "number" }, "GET /api/session start": { type: "number" },
"GET /api/session roots": QueryBooleanOpenApi,
"GET /api/session/{sessionID}/message limit": { type: "number" }, "GET /api/session/{sessionID}/message limit": { type: "number" },
} }
@@ -486,12 +490,6 @@ function normalizeParameter(param: OpenApiParameter, route: string) {
param.schema = override param.schema = override
return return
} }
if (QueryBooleanParameters.has(param.name)) {
param.schema = {
anyOf: [{ type: "boolean" }, { type: "string", enum: ["true", "false"] }],
}
return
}
} }
param.schema = stripOptionalNull(param.schema) param.schema = stripOptionalNull(param.schema)
} }
@@ -26,7 +26,7 @@ import {
import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty" import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty"
import { MessagesQuery as V2MessagesQuery } from "../../src/server/routes/instance/httpapi/groups/v2/message" import { MessagesQuery as V2MessagesQuery } from "../../src/server/routes/instance/httpapi/groups/v2/message"
import { SessionsQuery as V2SessionsQuery } from "../../src/server/routes/instance/httpapi/groups/v2/session" import { SessionsQuery as V2SessionsQuery } from "../../src/server/routes/instance/httpapi/groups/v2/session"
import { QueryBoolean } from "../../src/server/routes/instance/httpapi/groups/query" import { QueryBoolean, QueryBooleanOpenApi } from "../../src/server/routes/instance/httpapi/groups/query"
import { resetDatabase } from "../fixture/db" import { resetDatabase } from "../fixture/db"
import { disposeAllInstances, tmpdir } from "../fixture/fixture" import { disposeAllInstances, tmpdir } from "../fixture/fixture"
import { it } from "../lib/effect" import { it } from "../lib/effect"
@@ -36,6 +36,8 @@ const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
type Method = "get" | "post" | "put" | "delete" | "patch" type Method = "get" | "post" | "put" | "delete" | "patch"
type QuerySchema = { readonly fields: Record<string, unknown> } type QuerySchema = { readonly fields: Record<string, unknown> }
type OpenApiSchema = { type OpenApiSchema = {
readonly anyOf?: readonly OpenApiSchema[]
readonly enum?: readonly string[]
readonly maximum?: number readonly maximum?: number
readonly minimum?: number readonly minimum?: number
readonly pattern?: string readonly pattern?: string
@@ -75,6 +77,13 @@ const numericSdkQueryParams = [
{ method: "get", path: "/api/session/:sessionID/message", name: "limit", schema: { type: "number" } }, { method: "get", path: "/api/session/:sessionID/message", name: "limit", schema: { type: "number" } },
] satisfies Array<{ method: Method; path: string; name: string; schema: OpenApiSchema }> ] satisfies Array<{ method: Method; path: string; name: string; schema: OpenApiSchema }>
const booleanSdkQueryParams = [
{ method: "get", path: ExperimentalPaths.session, name: "roots" },
{ method: "get", path: ExperimentalPaths.session, name: "archived" },
{ method: "get", path: SessionPaths.list, name: "roots" },
{ method: "get", path: "/api/session", name: "roots" },
] satisfies Array<{ method: Method; path: string; name: string }>
const queryParamPatterns = [ const queryParamPatterns = [
{ method: "get", path: SessionPaths.diff, name: "messageID", pattern: "^msg" }, { method: "get", path: SessionPaths.diff, name: "messageID", pattern: "^msg" },
] satisfies Array<{ method: Method; path: string; name: string; pattern: string }> ] satisfies Array<{ method: Method; path: string; name: string; pattern: string }>
@@ -174,20 +183,7 @@ describe("httpapi query schema drift", () => {
) )
it.effect( it.effect(
"OpenAPI query parameter patterns come from runtime schemas", "OpenAPI query params are declared by runtime query schemas",
Effect.sync(() => {
const spec = OpenApi.fromApi(PublicApi)
for (const expected of queryParamPatterns) {
expect(
queryParameter(spec.paths[openApiPath(expected.path)]?.[expected.method], expected.name)?.schema,
`${expected.method.toUpperCase()} ${expected.path} ${expected.name}`,
).toEqual({ type: "string", pattern: expected.pattern })
}
}),
)
it.effect(
"OpenAPI workspace query params are declared by runtime query schemas",
Effect.sync(() => { Effect.sync(() => {
const spec = OpenApi.fromApi(PublicApi) const spec = OpenApi.fromApi(PublicApi)
for (const route of openApiDriftRoutes) { for (const route of openApiDriftRoutes) {
@@ -200,7 +196,7 @@ describe("httpapi query schema drift", () => {
) )
it.effect( it.effect(
"OpenAPI numeric query params preserve generated SDK call shapes", "OpenAPI query and path schemas preserve compatibility metadata",
Effect.sync(() => { Effect.sync(() => {
const spec = OpenApi.fromApi(PublicApi) const spec = OpenApi.fromApi(PublicApi)
for (const expected of numericSdkQueryParams) { for (const expected of numericSdkQueryParams) {
@@ -209,13 +205,18 @@ describe("httpapi query schema drift", () => {
`${expected.method.toUpperCase()} ${expected.path} ${expected.name}`, `${expected.method.toUpperCase()} ${expected.path} ${expected.name}`,
).toEqual(expected.schema) ).toEqual(expected.schema)
} }
}), for (const expected of booleanSdkQueryParams) {
) expect(
queryParameter(spec.paths[openApiPath(expected.path)]?.[expected.method], expected.name)?.schema,
it.effect( `${expected.method.toUpperCase()} ${expected.path} ${expected.name}`,
"OpenAPI path parameter patterns come from runtime schemas", ).toEqual(QueryBooleanOpenApi)
Effect.sync(() => { }
const spec = OpenApi.fromApi(PublicApi) for (const expected of queryParamPatterns) {
expect(
queryParameter(spec.paths[openApiPath(expected.path)]?.[expected.method], expected.name)?.schema,
`${expected.method.toUpperCase()} ${expected.path} ${expected.name}`,
).toEqual({ type: "string", pattern: expected.pattern })
}
for (const expected of pathParamPatterns) { for (const expected of pathParamPatterns) {
expect( expect(
pathParameter(spec.paths[openApiPath(expected.path)]?.[expected.method], expected.name)?.schema, pathParameter(spec.paths[openApiPath(expected.path)]?.[expected.method], expected.name)?.schema,