Selaa lähdekoodia

Share HTTP API boolean query schema (#26615)

Kit Langton 3 kuukautta sitten
vanhempi
sitoutus
16866e1180

+ 1 - 1
packages/opencode/specs/openapi-translation-cleanup.md

@@ -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.
 
 

+ 2 - 7
packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts

@@ -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,

+ 8 - 0
packages/opencode/src/server/routes/instance/httpapi/groups/query.ts

@@ -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")),
+  }),
+)

+ 2 - 7
packages/opencode/src/server/routes/instance/httpapi/groups/session.ts

@@ -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 - 8
packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts

@@ -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"
-
-const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
-  Schema.decodeTo(Schema.Boolean, {
-    decode: SchemaGetter.transform((value) => value === "true"),
-    encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
-  }),
-)
+import { QueryBoolean } from "../query"
 
 
 export const SessionsQuery = Schema.Struct({
 export const SessionsQuery = Schema.Struct({
   ...WorkspaceRoutingQueryFields,
   ...WorkspaceRoutingQueryFields,

+ 4 - 1
packages/opencode/test/server/httpapi-exercise/index.ts

@@ -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")

+ 19 - 1
packages/opencode/test/server/httpapi-query-schema-drift.test.ts

@@ -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(() => {