mirror of
https://github.com/linsa-io/linsa.git
synced 2026-03-29 13:21:59 +02:00
Add URL management schema, route, and related updates for URLs feature
This commit is contained in:
34
packages/worker/src/db.ts
Normal file
34
packages/worker/src/db.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import postgres from "postgres"
|
||||
import { drizzle } from "drizzle-orm/postgres-js"
|
||||
import * as schema from "../../web/src/db/schema"
|
||||
|
||||
export type Hyperdrive = {
|
||||
connectionString: string
|
||||
}
|
||||
|
||||
export type WorkerEnv = {
|
||||
DATABASE_URL?: string
|
||||
HYPERDRIVE?: Hyperdrive
|
||||
}
|
||||
|
||||
const getConnectionString = (env?: WorkerEnv): string => {
|
||||
if (env?.DATABASE_URL) {
|
||||
return env.DATABASE_URL
|
||||
}
|
||||
|
||||
if (env?.HYPERDRIVE?.connectionString) {
|
||||
return env.HYPERDRIVE.connectionString
|
||||
}
|
||||
|
||||
if (process.env.DATABASE_URL) {
|
||||
return process.env.DATABASE_URL
|
||||
}
|
||||
|
||||
throw new Error("No database connection available. Set DATABASE_URL or HYPERDRIVE.")
|
||||
}
|
||||
|
||||
export const getDb = (env?: WorkerEnv) => {
|
||||
const connectionString = getConnectionString(env)
|
||||
const sql = postgres(connectionString, { prepare: false })
|
||||
return drizzle(sql, { schema, casing: "snake_case" })
|
||||
}
|
||||
@@ -1,11 +1,111 @@
|
||||
import { Hono } from "hono"
|
||||
import { Hono, type Context, type MiddlewareHandler } from "hono"
|
||||
import { cors } from "hono/cors"
|
||||
import { eq } from "drizzle-orm"
|
||||
import {
|
||||
browser_session_tabs,
|
||||
browser_sessions,
|
||||
canvas,
|
||||
canvas_images,
|
||||
chat_messages,
|
||||
chat_threads,
|
||||
context_items,
|
||||
thread_context_items,
|
||||
} from "../../web/src/db/schema"
|
||||
import { getDb, type Hyperdrive } from "./db"
|
||||
|
||||
type Env = {
|
||||
ADMIN_API_KEY?: string
|
||||
DATABASE_URL?: string
|
||||
HYPERDRIVE?: Hyperdrive
|
||||
}
|
||||
|
||||
// Create a new Hono app
|
||||
const app = new Hono()
|
||||
type AppEnv = { Bindings: Env }
|
||||
const app = new Hono<AppEnv>()
|
||||
|
||||
// Enable CORS for all routes
|
||||
app.use("/*", cors())
|
||||
app.use(
|
||||
"/*",
|
||||
cors({
|
||||
origin: "*",
|
||||
allowHeaders: ["Authorization", "Content-Type"],
|
||||
allowMethods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
|
||||
}),
|
||||
)
|
||||
|
||||
const requireAdmin: MiddlewareHandler<AppEnv> = async (c, next) => {
|
||||
if (c.req.method === "OPTIONS") {
|
||||
return next()
|
||||
}
|
||||
|
||||
const apiKey = c.env.ADMIN_API_KEY
|
||||
if (!apiKey) {
|
||||
return next()
|
||||
}
|
||||
|
||||
const authHeader = c.req.header("Authorization")
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return c.json({ error: "Missing Authorization header" }, 401)
|
||||
}
|
||||
|
||||
const providedKey = authHeader.slice(7)
|
||||
if (providedKey !== apiKey) {
|
||||
return c.json({ error: "Invalid API key" }, 401)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
app.use("/api/v1/admin/*", requireAdmin)
|
||||
|
||||
const parseBody = async (c: Context<AppEnv>) => {
|
||||
return (await c.req.json().catch(() => ({}))) as Record<string, unknown>
|
||||
}
|
||||
|
||||
const parseInteger = (value: unknown) => {
|
||||
const numberValue =
|
||||
typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN
|
||||
return Number.isInteger(numberValue) ? numberValue : null
|
||||
}
|
||||
|
||||
const parseDate = (value: unknown) => {
|
||||
if (typeof value !== "string" && typeof value !== "number") {
|
||||
return null
|
||||
}
|
||||
const date = new Date(value)
|
||||
return Number.isNaN(date.getTime()) ? null : date
|
||||
}
|
||||
|
||||
const parsePosition = (value: unknown) => {
|
||||
if (
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
"x" in value &&
|
||||
"y" in value &&
|
||||
typeof (value as { x: unknown }).x === "number" &&
|
||||
typeof (value as { y: unknown }).y === "number"
|
||||
) {
|
||||
return { x: (value as { x: number }).x, y: (value as { y: number }).y }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const parseSize = (value: unknown) => {
|
||||
if (
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
"width" in value &&
|
||||
"height" in value &&
|
||||
typeof (value as { width: unknown }).width === "number" &&
|
||||
typeof (value as { height: unknown }).height === "number"
|
||||
) {
|
||||
return {
|
||||
width: (value as { width: number }).width,
|
||||
height: (value as { height: number }).height,
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Health check endpoint
|
||||
app.get("/health", (c) => {
|
||||
@@ -19,6 +119,7 @@ app.get("/", (c) => {
|
||||
endpoints: {
|
||||
health: "/health",
|
||||
api: "/api/v1",
|
||||
admin: "/api/v1/admin",
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -29,6 +130,634 @@ app.get("/api/v1/hello", (c) => {
|
||||
return c.json({ message: `Hello, ${name}!` })
|
||||
})
|
||||
|
||||
// Canvas endpoints
|
||||
app.post("/api/v1/admin/canvas", async (c) => {
|
||||
const body = await parseBody(c)
|
||||
const ownerId = typeof body.ownerId === "string" ? body.ownerId.trim() : ""
|
||||
if (!ownerId) {
|
||||
return c.json({ error: "ownerId required" }, 400)
|
||||
}
|
||||
|
||||
const values: typeof canvas.$inferInsert = {
|
||||
owner_id: ownerId,
|
||||
}
|
||||
|
||||
if (typeof body.name === "string" && body.name.trim()) {
|
||||
values.name = body.name.trim()
|
||||
}
|
||||
if (typeof body.width === "number" && Number.isFinite(body.width)) {
|
||||
values.width = body.width
|
||||
}
|
||||
if (typeof body.height === "number" && Number.isFinite(body.height)) {
|
||||
values.height = body.height
|
||||
}
|
||||
if (typeof body.defaultModel === "string") {
|
||||
values.default_model = body.defaultModel
|
||||
}
|
||||
if (typeof body.defaultStyle === "string") {
|
||||
values.default_style = body.defaultStyle
|
||||
}
|
||||
if (body.backgroundPrompt === null) {
|
||||
values.background_prompt = null
|
||||
} else if (typeof body.backgroundPrompt === "string") {
|
||||
values.background_prompt = body.backgroundPrompt
|
||||
}
|
||||
|
||||
try {
|
||||
const database = getDb(c.env)
|
||||
const [record] = await database.insert(canvas).values(values).returning()
|
||||
return c.json({ canvas: record }, 201)
|
||||
} catch (error) {
|
||||
console.error("[worker] create canvas failed", error)
|
||||
return c.json({ error: "Failed to create canvas" }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.patch("/api/v1/admin/canvas/:canvasId", async (c) => {
|
||||
const canvasId = c.req.param("canvasId")
|
||||
if (!canvasId) {
|
||||
return c.json({ error: "canvasId required" }, 400)
|
||||
}
|
||||
|
||||
const body = await parseBody(c)
|
||||
const updates: Partial<typeof canvas.$inferInsert> = {
|
||||
updated_at: new Date(),
|
||||
}
|
||||
|
||||
if (typeof body.name === "string") {
|
||||
updates.name = body.name
|
||||
}
|
||||
if (typeof body.width === "number" && Number.isFinite(body.width)) {
|
||||
updates.width = body.width
|
||||
}
|
||||
if (typeof body.height === "number" && Number.isFinite(body.height)) {
|
||||
updates.height = body.height
|
||||
}
|
||||
if (typeof body.defaultModel === "string") {
|
||||
updates.default_model = body.defaultModel
|
||||
}
|
||||
if (typeof body.defaultStyle === "string") {
|
||||
updates.default_style = body.defaultStyle
|
||||
}
|
||||
if (body.backgroundPrompt === null) {
|
||||
updates.background_prompt = null
|
||||
} else if (typeof body.backgroundPrompt === "string") {
|
||||
updates.background_prompt = body.backgroundPrompt
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length <= 1) {
|
||||
return c.json({ error: "No updates provided" }, 400)
|
||||
}
|
||||
|
||||
try {
|
||||
const database = getDb(c.env)
|
||||
const [record] = await database
|
||||
.update(canvas)
|
||||
.set(updates)
|
||||
.where(eq(canvas.id, canvasId))
|
||||
.returning()
|
||||
if (!record) {
|
||||
return c.json({ error: "Canvas not found" }, 404)
|
||||
}
|
||||
return c.json({ canvas: record })
|
||||
} catch (error) {
|
||||
console.error("[worker] update canvas failed", error)
|
||||
return c.json({ error: "Failed to update canvas" }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.post("/api/v1/admin/canvas/:canvasId/images", async (c) => {
|
||||
const canvasId = c.req.param("canvasId")
|
||||
if (!canvasId) {
|
||||
return c.json({ error: "canvasId required" }, 400)
|
||||
}
|
||||
|
||||
const body = await parseBody(c)
|
||||
const position = parsePosition(body.position)
|
||||
const size = parseSize(body.size)
|
||||
|
||||
const values: typeof canvas_images.$inferInsert = {
|
||||
canvas_id: canvasId,
|
||||
}
|
||||
|
||||
if (typeof body.name === "string") values.name = body.name
|
||||
if (typeof body.prompt === "string") values.prompt = body.prompt
|
||||
if (typeof body.modelId === "string") values.model_id = body.modelId
|
||||
if (typeof body.modelUsed === "string") values.model_used = body.modelUsed
|
||||
if (typeof body.styleId === "string") values.style_id = body.styleId
|
||||
if (typeof body.rotation === "number" && Number.isFinite(body.rotation)) {
|
||||
values.rotation = body.rotation
|
||||
}
|
||||
if (position) values.position = position
|
||||
if (size) {
|
||||
values.width = size.width
|
||||
values.height = size.height
|
||||
}
|
||||
if (body.metadata !== undefined) {
|
||||
values.metadata =
|
||||
body.metadata && typeof body.metadata === "object" ? body.metadata : null
|
||||
}
|
||||
if (body.branchParentId === null) {
|
||||
values.branch_parent_id = null
|
||||
} else if (typeof body.branchParentId === "string") {
|
||||
values.branch_parent_id = body.branchParentId
|
||||
}
|
||||
if (body.contentBase64 === null) {
|
||||
values.content_base64 = null
|
||||
} else if (typeof body.contentBase64 === "string") {
|
||||
values.content_base64 = body.contentBase64
|
||||
} else if (typeof body.content_base64 === "string") {
|
||||
values.content_base64 = body.content_base64
|
||||
}
|
||||
if (body.imageUrl === null) {
|
||||
values.image_url = null
|
||||
} else if (typeof body.imageUrl === "string") {
|
||||
values.image_url = body.imageUrl
|
||||
}
|
||||
|
||||
try {
|
||||
const database = getDb(c.env)
|
||||
const [image] = await database.insert(canvas_images).values(values).returning()
|
||||
return c.json({ image }, 201)
|
||||
} catch (error) {
|
||||
console.error("[worker] create canvas image failed", error)
|
||||
return c.json({ error: "Failed to create canvas image" }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.patch("/api/v1/admin/canvas/images/:imageId", async (c) => {
|
||||
const imageId = c.req.param("imageId")
|
||||
if (!imageId) {
|
||||
return c.json({ error: "imageId required" }, 400)
|
||||
}
|
||||
|
||||
const body = await parseBody(c)
|
||||
const updates: Partial<typeof canvas_images.$inferInsert> = {
|
||||
updated_at: new Date(),
|
||||
}
|
||||
const position = parsePosition(body.position)
|
||||
const size = parseSize(body.size)
|
||||
|
||||
if (typeof body.name === "string") updates.name = body.name
|
||||
if (typeof body.prompt === "string") updates.prompt = body.prompt
|
||||
if (typeof body.modelId === "string") updates.model_id = body.modelId
|
||||
if (typeof body.modelUsed === "string") updates.model_used = body.modelUsed
|
||||
if (typeof body.styleId === "string") updates.style_id = body.styleId
|
||||
if (typeof body.rotation === "number" && Number.isFinite(body.rotation)) {
|
||||
updates.rotation = body.rotation
|
||||
}
|
||||
if (position) updates.position = position
|
||||
if (size) {
|
||||
updates.width = size.width
|
||||
updates.height = size.height
|
||||
}
|
||||
if (body.metadata !== undefined) {
|
||||
updates.metadata =
|
||||
body.metadata && typeof body.metadata === "object" ? body.metadata : null
|
||||
}
|
||||
if (body.branchParentId === null) {
|
||||
updates.branch_parent_id = null
|
||||
} else if (typeof body.branchParentId === "string") {
|
||||
updates.branch_parent_id = body.branchParentId
|
||||
}
|
||||
if (body.contentBase64 === null) {
|
||||
updates.content_base64 = null
|
||||
} else if (typeof body.contentBase64 === "string") {
|
||||
updates.content_base64 = body.contentBase64
|
||||
} else if (typeof body.content_base64 === "string") {
|
||||
updates.content_base64 = body.content_base64
|
||||
}
|
||||
if (body.imageUrl === null) {
|
||||
updates.image_url = null
|
||||
} else if (typeof body.imageUrl === "string") {
|
||||
updates.image_url = body.imageUrl
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length <= 1) {
|
||||
return c.json({ error: "No updates provided" }, 400)
|
||||
}
|
||||
|
||||
try {
|
||||
const database = getDb(c.env)
|
||||
const [image] = await database
|
||||
.update(canvas_images)
|
||||
.set(updates)
|
||||
.where(eq(canvas_images.id, imageId))
|
||||
.returning()
|
||||
if (!image) {
|
||||
return c.json({ error: "Image not found" }, 404)
|
||||
}
|
||||
return c.json({ image })
|
||||
} catch (error) {
|
||||
console.error("[worker] update canvas image failed", error)
|
||||
return c.json({ error: "Failed to update canvas image" }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.delete("/api/v1/admin/canvas/images/:imageId", async (c) => {
|
||||
const imageId = c.req.param("imageId")
|
||||
if (!imageId) {
|
||||
return c.json({ error: "imageId required" }, 400)
|
||||
}
|
||||
|
||||
try {
|
||||
const database = getDb(c.env)
|
||||
const [deleted] = await database
|
||||
.delete(canvas_images)
|
||||
.where(eq(canvas_images.id, imageId))
|
||||
.returning()
|
||||
if (!deleted) {
|
||||
return c.json({ error: "Image not found" }, 404)
|
||||
}
|
||||
return c.json({ id: imageId })
|
||||
} catch (error) {
|
||||
console.error("[worker] delete canvas image failed", error)
|
||||
return c.json({ error: "Failed to delete canvas image" }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
// Chat endpoints
|
||||
app.post("/api/v1/admin/chat/threads", async (c) => {
|
||||
const body = await parseBody(c)
|
||||
const title =
|
||||
typeof body.title === "string" && body.title.trim() ? body.title.trim() : "New chat"
|
||||
const userId = typeof body.userId === "string" ? body.userId : null
|
||||
|
||||
try {
|
||||
const database = getDb(c.env)
|
||||
const [thread] = await database
|
||||
.insert(chat_threads)
|
||||
.values({ title, user_id: userId })
|
||||
.returning()
|
||||
return c.json({ thread }, 201)
|
||||
} catch (error) {
|
||||
console.error("[worker] create thread failed", error)
|
||||
return c.json({ error: "Failed to create thread" }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.patch("/api/v1/admin/chat/threads/:threadId", async (c) => {
|
||||
const threadId = parseInteger(c.req.param("threadId"))
|
||||
if (!threadId) {
|
||||
return c.json({ error: "threadId required" }, 400)
|
||||
}
|
||||
|
||||
const body = await parseBody(c)
|
||||
const title = typeof body.title === "string" ? body.title.trim() : ""
|
||||
if (!title) {
|
||||
return c.json({ error: "title required" }, 400)
|
||||
}
|
||||
|
||||
try {
|
||||
const database = getDb(c.env)
|
||||
const [thread] = await database
|
||||
.update(chat_threads)
|
||||
.set({ title })
|
||||
.where(eq(chat_threads.id, threadId))
|
||||
.returning()
|
||||
if (!thread) {
|
||||
return c.json({ error: "Thread not found" }, 404)
|
||||
}
|
||||
return c.json({ thread })
|
||||
} catch (error) {
|
||||
console.error("[worker] update thread failed", error)
|
||||
return c.json({ error: "Failed to update thread" }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.post("/api/v1/admin/chat/messages", async (c) => {
|
||||
const body = await parseBody(c)
|
||||
const threadId = parseInteger(body.threadId)
|
||||
const role = typeof body.role === "string" ? body.role.trim() : ""
|
||||
const content = typeof body.content === "string" ? body.content.trim() : ""
|
||||
const createdAt = parseDate(body.createdAt)
|
||||
|
||||
if (!threadId || !role || !content) {
|
||||
return c.json({ error: "threadId, role, and content are required" }, 400)
|
||||
}
|
||||
|
||||
try {
|
||||
const database = getDb(c.env)
|
||||
const values: typeof chat_messages.$inferInsert = {
|
||||
thread_id: threadId,
|
||||
role,
|
||||
content,
|
||||
}
|
||||
if (createdAt) {
|
||||
values.created_at = createdAt
|
||||
}
|
||||
const [message] = await database
|
||||
.insert(chat_messages)
|
||||
.values(values)
|
||||
.returning()
|
||||
return c.json({ message }, 201)
|
||||
} catch (error) {
|
||||
console.error("[worker] add message failed", error)
|
||||
return c.json({ error: "Failed to add message" }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
// Context items endpoints
|
||||
app.post("/api/v1/admin/context-items", async (c) => {
|
||||
const body = await parseBody(c)
|
||||
const userId = typeof body.userId === "string" ? body.userId.trim() : ""
|
||||
const type = typeof body.type === "string" ? body.type.trim() : ""
|
||||
const url = typeof body.url === "string" ? body.url.trim() : null
|
||||
const name =
|
||||
typeof body.name === "string" && body.name.trim()
|
||||
? body.name.trim()
|
||||
: url
|
||||
? (() => {
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
return `${parsed.hostname}${parsed.pathname}`
|
||||
} catch {
|
||||
return url
|
||||
}
|
||||
})()
|
||||
: "Untitled context"
|
||||
const threadId = parseInteger(body.threadId)
|
||||
const parentId = parseInteger(body.parentId)
|
||||
|
||||
if (!userId) {
|
||||
return c.json({ error: "userId required" }, 400)
|
||||
}
|
||||
if (type !== "url" && type !== "file") {
|
||||
return c.json({ error: "type must be 'url' or 'file'" }, 400)
|
||||
}
|
||||
|
||||
const values: typeof context_items.$inferInsert = {
|
||||
user_id: userId,
|
||||
type,
|
||||
name,
|
||||
}
|
||||
|
||||
if (url) values.url = url
|
||||
if (body.content === null) {
|
||||
values.content = null
|
||||
} else if (typeof body.content === "string") {
|
||||
values.content = body.content
|
||||
}
|
||||
if (typeof body.refreshing === "boolean") {
|
||||
values.refreshing = body.refreshing
|
||||
}
|
||||
if (parentId) {
|
||||
values.parent_id = parentId
|
||||
}
|
||||
|
||||
try {
|
||||
const database = getDb(c.env)
|
||||
const [item] = await database.insert(context_items).values(values).returning()
|
||||
|
||||
if (threadId) {
|
||||
await database.insert(thread_context_items).values({
|
||||
thread_id: threadId,
|
||||
context_item_id: item.id,
|
||||
})
|
||||
}
|
||||
|
||||
return c.json({ item }, 201)
|
||||
} catch (error) {
|
||||
console.error("[worker] create context item failed", error)
|
||||
return c.json({ error: "Failed to create context item" }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.patch("/api/v1/admin/context-items/:itemId", async (c) => {
|
||||
const itemId = parseInteger(c.req.param("itemId"))
|
||||
if (!itemId) {
|
||||
return c.json({ error: "itemId required" }, 400)
|
||||
}
|
||||
|
||||
const body = await parseBody(c)
|
||||
const updates: Partial<typeof context_items.$inferInsert> = {
|
||||
updated_at: new Date(),
|
||||
}
|
||||
const parentId = parseInteger(body.parentId)
|
||||
|
||||
if (typeof body.name === "string") updates.name = body.name
|
||||
if (typeof body.type === "string") {
|
||||
const nextType = body.type.trim()
|
||||
if (nextType !== "url" && nextType !== "file") {
|
||||
return c.json({ error: "type must be 'url' or 'file'" }, 400)
|
||||
}
|
||||
updates.type = nextType
|
||||
}
|
||||
if (body.url === null) {
|
||||
updates.url = null
|
||||
} else if (typeof body.url === "string") {
|
||||
updates.url = body.url
|
||||
}
|
||||
if (body.content === null) {
|
||||
updates.content = null
|
||||
} else if (typeof body.content === "string") {
|
||||
updates.content = body.content
|
||||
}
|
||||
if (typeof body.refreshing === "boolean") {
|
||||
updates.refreshing = body.refreshing
|
||||
}
|
||||
if (body.parentId === null) {
|
||||
updates.parent_id = null
|
||||
} else if (parentId) {
|
||||
updates.parent_id = parentId
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length <= 1) {
|
||||
return c.json({ error: "No updates provided" }, 400)
|
||||
}
|
||||
|
||||
try {
|
||||
const database = getDb(c.env)
|
||||
const [item] = await database
|
||||
.update(context_items)
|
||||
.set(updates)
|
||||
.where(eq(context_items.id, itemId))
|
||||
.returning()
|
||||
if (!item) {
|
||||
return c.json({ error: "Context item not found" }, 404)
|
||||
}
|
||||
return c.json({ item })
|
||||
} catch (error) {
|
||||
console.error("[worker] update context item failed", error)
|
||||
return c.json({ error: "Failed to update context item" }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.post("/api/v1/admin/context-items/:itemId/link", async (c) => {
|
||||
const itemId = parseInteger(c.req.param("itemId"))
|
||||
if (!itemId) {
|
||||
return c.json({ error: "itemId required" }, 400)
|
||||
}
|
||||
|
||||
const body = await parseBody(c)
|
||||
const threadId = parseInteger(body.threadId)
|
||||
if (!threadId) {
|
||||
return c.json({ error: "threadId required" }, 400)
|
||||
}
|
||||
|
||||
try {
|
||||
const database = getDb(c.env)
|
||||
await database.insert(thread_context_items).values({
|
||||
thread_id: threadId,
|
||||
context_item_id: itemId,
|
||||
})
|
||||
return c.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error("[worker] link context item failed", error)
|
||||
return c.json({ error: "Failed to link context item" }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.delete("/api/v1/admin/context-items/:itemId", async (c) => {
|
||||
const itemId = parseInteger(c.req.param("itemId"))
|
||||
if (!itemId) {
|
||||
return c.json({ error: "itemId required" }, 400)
|
||||
}
|
||||
|
||||
try {
|
||||
const database = getDb(c.env)
|
||||
const [item] = await database
|
||||
.delete(context_items)
|
||||
.where(eq(context_items.id, itemId))
|
||||
.returning()
|
||||
if (!item) {
|
||||
return c.json({ error: "Context item not found" }, 404)
|
||||
}
|
||||
return c.json({ id: itemId })
|
||||
} catch (error) {
|
||||
console.error("[worker] delete context item failed", error)
|
||||
return c.json({ error: "Failed to delete context item" }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
// Browser session endpoints
|
||||
app.post("/api/v1/admin/browser-sessions", async (c) => {
|
||||
const body = await parseBody(c)
|
||||
const userId = typeof body.userId === "string" ? body.userId.trim() : ""
|
||||
const name = typeof body.name === "string" ? body.name.trim() : ""
|
||||
const browser = typeof body.browser === "string" ? body.browser.trim() : "safari"
|
||||
const capturedAt = parseDate(body.capturedAt)
|
||||
const isFavorite = typeof body.isFavorite === "boolean" ? body.isFavorite : undefined
|
||||
const tabs = Array.isArray(body.tabs) ? body.tabs : []
|
||||
|
||||
if (!userId || !name) {
|
||||
return c.json({ error: "userId and name are required" }, 400)
|
||||
}
|
||||
|
||||
const tabValues = tabs
|
||||
.map((tab) => {
|
||||
if (!tab || typeof tab !== "object") {
|
||||
return null
|
||||
}
|
||||
const title = typeof (tab as { title?: unknown }).title === "string"
|
||||
? (tab as { title: string }).title
|
||||
: ""
|
||||
const url = typeof (tab as { url?: unknown }).url === "string"
|
||||
? (tab as { url: string }).url
|
||||
: ""
|
||||
if (!url) {
|
||||
return null
|
||||
}
|
||||
const faviconUrl =
|
||||
typeof (tab as { faviconUrl?: unknown }).faviconUrl === "string"
|
||||
? (tab as { faviconUrl: string }).faviconUrl
|
||||
: typeof (tab as { favicon_url?: unknown }).favicon_url === "string"
|
||||
? (tab as { favicon_url: string }).favicon_url
|
||||
: null
|
||||
return { title, url, favicon_url: faviconUrl }
|
||||
})
|
||||
.filter((tab): tab is { title: string; url: string; favicon_url: string | null } =>
|
||||
Boolean(tab),
|
||||
)
|
||||
|
||||
try {
|
||||
const database = getDb(c.env)
|
||||
const [session] = await database
|
||||
.insert(browser_sessions)
|
||||
.values({
|
||||
user_id: userId,
|
||||
name,
|
||||
browser,
|
||||
tab_count: tabValues.length,
|
||||
is_favorite: isFavorite ?? false,
|
||||
captured_at: capturedAt ?? new Date(),
|
||||
})
|
||||
.returning()
|
||||
|
||||
if (tabValues.length > 0) {
|
||||
await database.insert(browser_session_tabs).values(
|
||||
tabValues.map((tab, index) => ({
|
||||
session_id: session.id,
|
||||
title: tab.title,
|
||||
url: tab.url,
|
||||
position: index,
|
||||
favicon_url: tab.favicon_url ?? null,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
return c.json({ session }, 201)
|
||||
} catch (error) {
|
||||
console.error("[worker] create browser session failed", error)
|
||||
return c.json({ error: "Failed to create browser session" }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.patch("/api/v1/admin/browser-sessions/:sessionId", async (c) => {
|
||||
const sessionId = c.req.param("sessionId")
|
||||
if (!sessionId) {
|
||||
return c.json({ error: "sessionId required" }, 400)
|
||||
}
|
||||
|
||||
const body = await parseBody(c)
|
||||
const updates: Partial<typeof browser_sessions.$inferInsert> = {}
|
||||
|
||||
if (typeof body.name === "string") updates.name = body.name
|
||||
if (typeof body.isFavorite === "boolean") updates.is_favorite = body.isFavorite
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
return c.json({ error: "No updates provided" }, 400)
|
||||
}
|
||||
|
||||
try {
|
||||
const database = getDb(c.env)
|
||||
const [session] = await database
|
||||
.update(browser_sessions)
|
||||
.set(updates)
|
||||
.where(eq(browser_sessions.id, sessionId))
|
||||
.returning()
|
||||
if (!session) {
|
||||
return c.json({ error: "Session not found" }, 404)
|
||||
}
|
||||
return c.json({ session })
|
||||
} catch (error) {
|
||||
console.error("[worker] update browser session failed", error)
|
||||
return c.json({ error: "Failed to update browser session" }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.delete("/api/v1/admin/browser-sessions/:sessionId", async (c) => {
|
||||
const sessionId = c.req.param("sessionId")
|
||||
if (!sessionId) {
|
||||
return c.json({ error: "sessionId required" }, 400)
|
||||
}
|
||||
|
||||
try {
|
||||
const database = getDb(c.env)
|
||||
const [session] = await database
|
||||
.delete(browser_sessions)
|
||||
.where(eq(browser_sessions.id, sessionId))
|
||||
.returning()
|
||||
if (!session) {
|
||||
return c.json({ error: "Session not found" }, 404)
|
||||
}
|
||||
return c.json({ id: sessionId })
|
||||
} catch (error) {
|
||||
console.error("[worker] delete browser session failed", error)
|
||||
return c.json({ error: "Failed to delete browser session" }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
// Export the Hono app as default (handles HTTP requests)
|
||||
export default app
|
||||
|
||||
|
||||
Reference in New Issue
Block a user