feat(httpapi): bridge workspace mutations (#24483)

This commit is contained in:
Kit Langton
2026-04-26 11:12:04 -04:00
committed by GitHub
parent 37c5eab6f8
commit aa5999b188
4 changed files with 189 additions and 28 deletions

View File

@@ -2,15 +2,28 @@ import { listAdaptors } from "@/control-plane/adaptors"
import { Workspace } from "@/control-plane/workspace"
import { WorkspaceAdaptorEntry } from "@/control-plane/types"
import * as InstanceState from "@/effect/instance-state"
import { Effect, Layer, Schema } from "effect"
import { Instance } from "@/project/instance"
import { Effect, Layer, Schema, Struct } from "effect"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "./auth"
const root = "/experimental/workspace"
const CreatePayload = Schema.Struct(Struct.omit(Workspace.CreateInput.fields, ["projectID"])).annotate({
identifier: "WorkspaceCreateInput",
})
const SessionRestorePayload = Schema.Struct(Struct.omit(Workspace.SessionRestoreInput.fields, ["workspaceID"])).annotate({
identifier: "WorkspaceSessionRestoreInput",
})
const SessionRestoreResponse = Schema.Struct({
total: Schema.Number,
}).annotate({ identifier: "WorkspaceSessionRestoreResponse" })
export const WorkspacePaths = {
adaptors: `${root}/adaptor`,
list: root,
status: `${root}/status`,
remove: `${root}/:id`,
sessionRestore: `${root}/:id/session-restore`,
} as const
export const WorkspaceApi = HttpApi.make("workspace")
@@ -35,6 +48,16 @@ export const WorkspaceApi = HttpApi.make("workspace")
description: "List all workspaces.",
}),
),
HttpApiEndpoint.post("create", WorkspacePaths.list, {
payload: CreatePayload,
success: Workspace.Info,
}).annotateMerge(
OpenApi.annotations({
identifier: "experimental.workspace.create",
summary: "Create workspace",
description: "Create a workspace for the current project.",
}),
),
HttpApiEndpoint.get("status", WorkspacePaths.status, {
success: Schema.Array(Workspace.ConnectionStatus),
}).annotateMerge(
@@ -44,6 +67,27 @@ export const WorkspaceApi = HttpApi.make("workspace")
description: "Get connection status for workspaces in the current project.",
}),
),
HttpApiEndpoint.delete("remove", WorkspacePaths.remove, {
params: { id: Workspace.Info.fields.id },
success: Schema.UndefinedOr(Workspace.Info),
}).annotateMerge(
OpenApi.annotations({
identifier: "experimental.workspace.remove",
summary: "Remove workspace",
description: "Remove an existing workspace.",
}),
),
HttpApiEndpoint.post("sessionRestore", WorkspacePaths.sessionRestore, {
params: { id: Workspace.Info.fields.id },
payload: SessionRestorePayload,
success: SessionRestoreResponse,
}).annotateMerge(
OpenApi.annotations({
identifier: "experimental.workspace.sessionRestore",
summary: "Restore session into workspace",
description: "Replay a session's sync events into the target workspace in batches.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
@@ -72,13 +116,51 @@ export const workspaceHandlers = Layer.unwrap(
return Workspace.list((yield* InstanceState.context).project)
})
const create = Effect.fn("WorkspaceHttpApi.create")(function* (ctx: { payload: typeof CreatePayload.Type }) {
const instance = yield* InstanceState.context
return yield* Effect.promise(() =>
Instance.restore(instance, () =>
Workspace.create({
...Schema.decodeUnknownSync(CreatePayload)(ctx.payload),
projectID: instance.project.id,
}),
),
)
})
const status = Effect.fn("WorkspaceHttpApi.status")(function* () {
const ids = new Set(Workspace.list((yield* InstanceState.context).project).map((item) => item.id))
return Workspace.status().filter((item) => ids.has(item.workspaceID))
})
const remove = Effect.fn("WorkspaceHttpApi.remove")(function* (ctx: { params: { id: Workspace.Info["id"] } }) {
const instance = yield* InstanceState.context
return yield* Effect.promise(() => Instance.restore(instance, () => Workspace.remove(ctx.params.id)))
})
const sessionRestore = Effect.fn("WorkspaceHttpApi.sessionRestore")(function* (ctx: {
params: { id: Workspace.Info["id"] }
payload: typeof SessionRestorePayload.Type
}) {
const instance = yield* InstanceState.context
return yield* Effect.promise(() =>
Instance.restore(instance, () =>
Workspace.sessionRestore({
workspaceID: ctx.params.id,
sessionID: ctx.payload.sessionID,
}),
),
)
})
return HttpApiBuilder.group(WorkspaceApi, "workspace", (handlers) =>
handlers.handle("adaptors", adaptors).handle("list", list).handle("status", status),
handlers
.handle("adaptors", adaptors)
.handle("list", list)
.handle("create", create)
.handle("status", status)
.handle("remove", remove)
.handle("sessionRestore", sessionRestore),
)
}),
)

View File

@@ -67,7 +67,10 @@ function create(opts: { cors?: string[] }) {
const context = Context.empty() as Context.Context<unknown>
workspaceApp.get(WorkspacePaths.adaptors, (c) => handler(c.req.raw, context))
workspaceApp.get(WorkspacePaths.list, (c) => handler(c.req.raw, context))
workspaceApp.post(WorkspacePaths.list, (c) => handler(c.req.raw, context))
workspaceApp.get(WorkspacePaths.status, (c) => handler(c.req.raw, context))
workspaceApp.delete(WorkspacePaths.remove, (c) => handler(c.req.raw, context))
workspaceApp.post(WorkspacePaths.sessionRestore, (c) => handler(c.req.raw, context))
}
workspaceApp.route("/", workspaceLegacyApp)