feat(console): add batch workspace block endpoints (#48491)
This commit is contained in:
parent
193de13a88
commit
5de2f241c6
3 changed files with 86 additions and 1 deletions
|
|
@ -0,0 +1,22 @@
|
|||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { Workspace } from "@opencode-ai/console-core/workspace.js"
|
||||
import { safeEqual } from "@opencode-ai/console-core/util/crypto.js"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import z from "zod"
|
||||
|
||||
const Body = z.object({ workspaceIDs: z.array(z.string().startsWith("wrk_")).min(1) })
|
||||
|
||||
export async function POST(event: APIEvent) {
|
||||
if (!safeEqual(event.request.headers.get("authorization") ?? "", `Bearer ${Resource.SUPPORT_API_KEY.value}`)) {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = Body.safeParse(await event.request.json().catch(() => undefined))
|
||||
if (!body.success) {
|
||||
return Response.json({ error: "Invalid request", issues: body.error.issues }, { status: 400 })
|
||||
}
|
||||
// Missing IDs do not fail the batch; they are reported per item in the result.
|
||||
return Workspace.blockBatch(body.data)
|
||||
.then((result) => Response.json({ success: true, message: "Workspaces blocked", result }))
|
||||
.catch((error) => Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 400 }))
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { Workspace } from "@opencode-ai/console-core/workspace.js"
|
||||
import { safeEqual } from "@opencode-ai/console-core/util/crypto.js"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import z from "zod"
|
||||
|
||||
const Body = z.object({ workspaceIDs: z.array(z.string().startsWith("wrk_")).min(1) })
|
||||
|
||||
export async function POST(event: APIEvent) {
|
||||
if (!safeEqual(event.request.headers.get("authorization") ?? "", `Bearer ${Resource.SUPPORT_API_KEY.value}`)) {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = Body.safeParse(await event.request.json().catch(() => undefined))
|
||||
if (!body.success) {
|
||||
return Response.json({ error: "Invalid request", issues: body.error.issues }, { status: 400 })
|
||||
}
|
||||
// Missing IDs do not fail the batch; they are reported per item in the result.
|
||||
return Workspace.unblockBatch(body.data)
|
||||
.then((result) => Response.json({ success: true, message: "Workspaces unblocked", result }))
|
||||
.catch((error) => Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 400 }))
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import { BillingTable } from "./schema/billing.sql"
|
|||
import { WorkspaceTable } from "./schema/workspace.sql"
|
||||
import { AccountTable } from "./schema/account.sql"
|
||||
import { Key } from "./key"
|
||||
import { and, eq, isNull, sql } from "drizzle-orm"
|
||||
import { and, eq, inArray, isNull, sql } from "drizzle-orm"
|
||||
|
||||
export namespace Workspace {
|
||||
export const Region = z.enum(["us", "eu", "sg", "cn"])
|
||||
|
|
@ -109,6 +109,26 @@ export namespace Workspace {
|
|||
},
|
||||
)
|
||||
|
||||
export const blockBatch = fn(
|
||||
z.object({
|
||||
workspaceIDs: z.array(Identifier.schema("workspace")).min(1),
|
||||
}),
|
||||
async (input) => {
|
||||
const { applied, notFound } = await setBlockedBatch(input, true)
|
||||
return { blocked: applied, notFound }
|
||||
},
|
||||
)
|
||||
|
||||
export const unblockBatch = fn(
|
||||
z.object({
|
||||
workspaceIDs: z.array(Identifier.schema("workspace")).min(1),
|
||||
}),
|
||||
async (input) => {
|
||||
const { applied, notFound } = await setBlockedBatch(input, false)
|
||||
return { unblocked: applied, notFound }
|
||||
},
|
||||
)
|
||||
|
||||
export const remove = fn(z.void(), async () => {
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
|
|
@ -118,3 +138,24 @@ export namespace Workspace {
|
|||
)
|
||||
})
|
||||
}
|
||||
|
||||
// No size cap by design; large IN lists degrade on Vitess, so chunks stay fixed-size and
|
||||
// sequential to share one connection inside ambient transactions. Existence comes from the
|
||||
// follow-up select rather than rowsAffected, so no-op rows (already in the target state)
|
||||
// still report as applied instead of "not found".
|
||||
const setBlockedBatch = async (input: { workspaceIDs: string[] }, isBlocked: boolean) => {
|
||||
const ids = [...new Set(input.workspaceIDs)]
|
||||
return await Database.use(async (tx) => {
|
||||
const applied = new Set<string>()
|
||||
for (let index = 0; index < ids.length; index += 500) {
|
||||
const chunk = ids.slice(index, index + 500)
|
||||
await tx.update(WorkspaceTable).set({ is_blocked: isBlocked }).where(inArray(WorkspaceTable.id, chunk))
|
||||
const rows = await tx.select({ id: WorkspaceTable.id }).from(WorkspaceTable).where(inArray(WorkspaceTable.id, chunk))
|
||||
for (const row of rows) applied.add(row.id)
|
||||
}
|
||||
return {
|
||||
applied: ids.filter((id) => applied.has(id)),
|
||||
notFound: ids.filter((id) => !applied.has(id)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue