Share HTTP API boolean query schema (#26615)
This commit is contained in:
@@ -84,7 +84,7 @@ Verification:
|
|||||||
|
|
||||||
Concrete first targets:
|
Concrete first targets:
|
||||||
|
|
||||||
- Replace `roots` / `archived` reliance on `QueryBooleanParameters` with explicit route schema helpers.
|
- `[x]` Consolidate `roots` / `archived` onto an explicit shared route schema helper. Keep `QueryBooleanParameters` until route-level schema metadata can preserve the SDK's `boolean | "true" | "false"` call shape without a global transform.
|
||||||
- Replace `start` / `cursor` / `limit` reliance on `QueryNumberParameters` with explicit route schema constraints where missing.
|
- Replace `start` / `cursor` / `limit` reliance on `QueryNumberParameters` with explicit route schema constraints where missing.
|
||||||
- Keep `GET /find/file limit`, `GET /session/{sessionID}/diff messageID`, and `GET /session/{sessionID}/message limit` overrides until their route schemas generate identical SDK types directly.
|
- Keep `GET /find/file limit`, `GET /session/{sessionID}/diff messageID`, and `GET /session/{sessionID}/message limit` overrides until their route schemas generate identical SDK types directly.
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ProviderID, ModelID } from "@/provider/schema"
|
|||||||
import { Session } from "@/session/session"
|
import { Session } from "@/session/session"
|
||||||
import { Worktree } from "@/worktree"
|
import { Worktree } from "@/worktree"
|
||||||
import { NonNegativeInt } from "@opencode-ai/core/schema"
|
import { NonNegativeInt } from "@opencode-ai/core/schema"
|
||||||
import { Schema, SchemaGetter } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Authorization } from "../middleware/authorization"
|
import { Authorization } from "../middleware/authorization"
|
||||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
WorkspaceRoutingQueryFields,
|
WorkspaceRoutingQueryFields,
|
||||||
} from "../middleware/workspace-routing"
|
} from "../middleware/workspace-routing"
|
||||||
import { described } from "./metadata"
|
import { described } from "./metadata"
|
||||||
|
import { QueryBoolean } from "./query"
|
||||||
|
|
||||||
const ConsoleStateResponse = Schema.Struct({
|
const ConsoleStateResponse = Schema.Struct({
|
||||||
consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)),
|
consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)),
|
||||||
@@ -52,12 +53,6 @@ export const ToolListQuery = Schema.Struct({
|
|||||||
model: ModelID,
|
model: ModelID,
|
||||||
})
|
})
|
||||||
|
|
||||||
const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
|
|
||||||
Schema.decodeTo(Schema.Boolean, {
|
|
||||||
decode: SchemaGetter.transform((value) => value === "true"),
|
|
||||||
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
const WorktreeList = Schema.Array(Schema.String)
|
const WorktreeList = Schema.Array(Schema.String)
|
||||||
export const SessionListQuery = Schema.Struct({
|
export const SessionListQuery = Schema.Struct({
|
||||||
...WorkspaceRoutingQueryFields,
|
...WorkspaceRoutingQueryFields,
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { Schema, SchemaGetter } from "effect"
|
||||||
|
|
||||||
|
export const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
|
||||||
|
Schema.decodeTo(Schema.Boolean, {
|
||||||
|
decode: SchemaGetter.transform((value) => value === "true"),
|
||||||
|
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
|
||||||
|
}),
|
||||||
|
)
|
||||||
@@ -10,7 +10,7 @@ import { SessionSummary } from "@/session/summary"
|
|||||||
import { Todo } from "@/session/todo"
|
import { Todo } from "@/session/todo"
|
||||||
import { MessageID, PartID, SessionID } from "@/session/schema"
|
import { MessageID, PartID, SessionID } from "@/session/schema"
|
||||||
import { Snapshot } from "@/snapshot"
|
import { Snapshot } from "@/snapshot"
|
||||||
import { Schema, SchemaGetter, Struct } from "effect"
|
import { Schema, Struct } from "effect"
|
||||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Authorization } from "../middleware/authorization"
|
import { Authorization } from "../middleware/authorization"
|
||||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||||
@@ -21,14 +21,9 @@ import {
|
|||||||
} from "../middleware/workspace-routing"
|
} from "../middleware/workspace-routing"
|
||||||
import { ApiNotFoundError } from "../errors"
|
import { ApiNotFoundError } from "../errors"
|
||||||
import { described } from "./metadata"
|
import { described } from "./metadata"
|
||||||
|
import { QueryBoolean } from "./query"
|
||||||
|
|
||||||
const root = "/session"
|
const root = "/session"
|
||||||
const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
|
|
||||||
Schema.decodeTo(Schema.Boolean, {
|
|
||||||
decode: SchemaGetter.transform((value) => value === "true"),
|
|
||||||
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
export const ListQuery = Schema.Struct({
|
export const ListQuery = Schema.Struct({
|
||||||
...WorkspaceRoutingQueryFields,
|
...WorkspaceRoutingQueryFields,
|
||||||
scope: Schema.optional(Schema.Literals(["project"])),
|
scope: Schema.optional(Schema.Literals(["project"])),
|
||||||
|
|||||||
@@ -2,17 +2,11 @@ import { SessionID } from "@/session/schema"
|
|||||||
import { SessionMessage } from "@/v2/session-message"
|
import { SessionMessage } from "@/v2/session-message"
|
||||||
import { Prompt } from "@/v2/session-prompt"
|
import { Prompt } from "@/v2/session-prompt"
|
||||||
import { SessionV2 } from "@/v2/session"
|
import { SessionV2 } from "@/v2/session"
|
||||||
import { Schema, SchemaGetter } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Authorization } from "../../middleware/authorization"
|
import { Authorization } from "../../middleware/authorization"
|
||||||
import { WorkspaceRoutingQuery, WorkspaceRoutingQueryFields } from "../../middleware/workspace-routing"
|
import { WorkspaceRoutingQuery, WorkspaceRoutingQueryFields } from "../../middleware/workspace-routing"
|
||||||
|
import { QueryBoolean } from "../query"
|
||||||
const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
|
|
||||||
Schema.decodeTo(Schema.Boolean, {
|
|
||||||
decode: SchemaGetter.transform((value) => value === "true"),
|
|
||||||
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const SessionsQuery = Schema.Struct({
|
export const SessionsQuery = Schema.Struct({
|
||||||
...WorkspaceRoutingQueryFields,
|
...WorkspaceRoutingQueryFields,
|
||||||
|
|||||||
@@ -524,7 +524,10 @@ const scenarios: Scenario[] = [
|
|||||||
yield* ctx.worktreeRemove(ctx.state.directory)
|
yield* ctx.worktreeRemove(ctx.state.directory)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
http.protected.get("/experimental/session", "experimental.session.list").json(200, array),
|
http.protected
|
||||||
|
.get("/experimental/session", "experimental.session.list")
|
||||||
|
.at((ctx) => ({ path: "/experimental/session?roots=false&archived=false", headers: ctx.headers() }))
|
||||||
|
.json(200, array),
|
||||||
http.protected.get("/experimental/resource", "experimental.resource.list").json(),
|
http.protected.get("/experimental/resource", "experimental.resource.list").json(),
|
||||||
http.protected
|
http.protected
|
||||||
.post("/sync/history", "sync.history.list")
|
.post("/sync/history", "sync.history.list")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { afterEach, describe, expect } from "bun:test"
|
import { afterEach, describe, expect } from "bun:test"
|
||||||
import { Effect } from "effect"
|
import { Effect, Schema } from "effect"
|
||||||
import { OpenApi } from "effect/unstable/httpapi"
|
import { OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
import { Server } from "../../src/server/server"
|
import { Server } from "../../src/server/server"
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
} from "../../src/server/routes/instance/httpapi/groups/session"
|
} from "../../src/server/routes/instance/httpapi/groups/session"
|
||||||
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 { 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"
|
||||||
@@ -106,6 +107,23 @@ describe("httpapi query schema drift", () => {
|
|||||||
expect(status, `route ${url} 400'd, query schema is missing routing fields`).not.toBe(400)
|
expect(status, `route ${url} 400'd, query schema is missing routing fields`).not.toBe(400)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"boolean query schema accepts only true and false strings",
|
||||||
|
Effect.sync(() => {
|
||||||
|
const decode = Schema.decodeUnknownSync(QueryBoolean)
|
||||||
|
const encode = Schema.encodeUnknownSync(QueryBoolean)
|
||||||
|
|
||||||
|
expect(decode("true")).toBe(true)
|
||||||
|
expect(decode("false")).toBe(false)
|
||||||
|
expect(encode(true)).toBe("true")
|
||||||
|
expect(encode(false)).toBe("false")
|
||||||
|
|
||||||
|
for (const input of ["1", "yes", "True", "", true, false]) {
|
||||||
|
expect(() => decode(input)).toThrow()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect(
|
it.effect(
|
||||||
"OpenAPI workspace query params are declared by runtime query schemas",
|
"OpenAPI workspace query params are declared by runtime query schemas",
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user