feature/workspace-domain
Dax Raad 2026-01-27 15:33:44 -05:00
parent f40685ab13
commit 63e38555c9
9 changed files with 83 additions and 66 deletions

View File

@ -14,7 +14,8 @@ CREATE TABLE `project` (
CREATE TABLE `message` (
`id` text PRIMARY KEY,
`session_id` text NOT NULL,
`created_at` integer NOT NULL,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
`data` text NOT NULL,
CONSTRAINT `fk_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
);
@ -23,12 +24,16 @@ CREATE TABLE `part` (
`id` text PRIMARY KEY,
`message_id` text NOT NULL,
`session_id` text NOT NULL,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
`data` text NOT NULL,
CONSTRAINT `fk_part_message_id_message_id_fk` FOREIGN KEY (`message_id`) REFERENCES `message`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `permission` (
`project_id` text PRIMARY KEY,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
`data` text NOT NULL,
CONSTRAINT `fk_permission_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
);
@ -46,10 +51,7 @@ CREATE TABLE `session` (
`summary_deletions` integer,
`summary_files` integer,
`summary_diffs` text,
`revert_message_id` text,
`revert_part_id` text,
`revert_snapshot` text,
`revert_diff` text,
`revert` text,
`permission` text,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
@ -65,6 +67,8 @@ CREATE TABLE `todo` (
`status` text NOT NULL,
`priority` text NOT NULL,
`position` integer NOT NULL,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
CONSTRAINT `todo_pk` PRIMARY KEY(`session_id`, `id`),
CONSTRAINT `fk_todo_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
);
@ -74,6 +78,8 @@ CREATE TABLE `session_share` (
`id` text NOT NULL,
`secret` text NOT NULL,
`url` text NOT NULL,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
CONSTRAINT `fk_session_share_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
@ -82,4 +88,4 @@ CREATE INDEX `part_message_idx` ON `part` (`message_id`);--> statement-breakpoin
CREATE INDEX `part_session_idx` ON `part` (`session_id`);--> statement-breakpoint
CREATE INDEX `session_project_idx` ON `session` (`project_id`);--> statement-breakpoint
CREATE INDEX `session_parent_idx` ON `session` (`parent_id`);--> statement-breakpoint
CREATE INDEX `todo_session_idx` ON `todo` (`session_id`);
CREATE INDEX `todo_session_idx` ON `todo` (`session_id`);

View File

@ -1,4 +1,5 @@
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
import { Database } from "@/storage/db"
export const ProjectTable = sqliteTable("project", {
id: text().primaryKey(),
@ -7,8 +8,7 @@ export const ProjectTable = sqliteTable("project", {
name: text(),
icon_url: text(),
icon_color: text(),
time_created: integer().notNull(),
time_updated: integer().notNull(),
...Database.Timestamps,
time_initialized: integer(),
sandboxes: text({ mode: "json" }).notNull().$type<string[]>(),
})

View File

@ -54,15 +54,7 @@ export namespace Session {
}
: undefined
const share = row.share_url ? { url: row.share_url } : undefined
const revert =
row.revert_message_id !== null
? {
messageID: row.revert_message_id,
partID: row.revert_part_id ?? undefined,
snapshot: row.revert_snapshot ?? undefined,
diff: row.revert_diff ?? undefined,
}
: undefined
const revert = row.revert ?? undefined
return {
id: row.id,
slug: row.slug,
@ -98,10 +90,7 @@ export namespace Session {
summary_deletions: info.summary?.deletions,
summary_files: info.summary?.files,
summary_diffs: info.summary?.diffs,
revert_message_id: info.revert?.messageID ?? null,
revert_part_id: info.revert?.partID ?? null,
revert_snapshot: info.revert?.snapshot ?? null,
revert_diff: info.revert?.diff ?? null,
revert: info.revert ?? null,
permission: info.permission,
time_created: info.time.created,
time_updated: info.time.updated,
@ -415,10 +404,7 @@ export namespace Session {
const row = db
.update(SessionTable)
.set({
revert_message_id: input.revert?.messageID ?? null,
revert_part_id: input.revert?.partID ?? null,
revert_snapshot: input.revert?.snapshot ?? null,
revert_diff: input.revert?.diff ?? null,
revert: input.revert ?? null,
summary_additions: input.summary?.additions,
summary_deletions: input.summary?.deletions,
summary_files: input.summary?.files,
@ -440,10 +426,7 @@ export namespace Session {
const row = db
.update(SessionTable)
.set({
revert_message_id: null,
revert_part_id: null,
revert_snapshot: null,
revert_diff: null,
revert: null,
time_updated: Date.now(),
})
.where(eq(SessionTable.id, sessionID))
@ -544,16 +527,17 @@ export namespace Session {
})
export const updateMessage = fn(MessageV2.Info, async (msg) => {
const created_at = msg.role === "user" ? msg.time.created : msg.time.created
const time_created = msg.role === "user" ? msg.time.created : msg.time.created
const { id, sessionID, ...data } = msg
Database.use((db) => {
db.insert(MessageTable)
.values({
id: msg.id,
session_id: msg.sessionID,
created_at,
data: msg,
id,
session_id: sessionID,
time_created,
data,
})
.onConflictDoUpdate({ target: MessageTable.id, set: { data: msg } })
.onConflictDoUpdate({ target: MessageTable.id, set: { data } })
.run()
Database.effect(() =>
Bus.publish(MessageV2.Event.Updated, {
@ -620,15 +604,18 @@ export namespace Session {
export const updatePart = fn(UpdatePartInput, async (input) => {
const part = "delta" in input ? input.part : input
const delta = "delta" in input ? input.delta : undefined
const { id, messageID, sessionID, ...data } = part
const time = Date.now()
Database.use((db) => {
db.insert(PartTable)
.values({
id: part.id,
message_id: part.messageID,
session_id: part.sessionID,
data: part,
id,
message_id: messageID,
session_id: sessionID,
time_created: time,
data,
})
.onConflictDoUpdate({ target: PartTable.id, set: { data: part } })
.onConflictDoUpdate({ target: PartTable.id, set: { data } })
.run()
Database.effect(() =>
Bus.publish(MessageV2.Event.PartUpdated, {

View File

@ -616,7 +616,7 @@ export namespace MessageV2 {
.select()
.from(MessageTable)
.where(eq(MessageTable.session_id, sessionID))
.orderBy(desc(MessageTable.created_at))
.orderBy(desc(MessageTable.time_created))
.limit(size)
.offset(offset)
.all(),
@ -635,15 +635,22 @@ export namespace MessageV2 {
.all(),
)
for (const row of partRows) {
const part = {
...row.data,
id: row.id,
sessionID: row.session_id,
messageID: row.message_id,
} as MessageV2.Part
const list = partsByMessage.get(row.message_id)
if (list) list.push(row.data)
else partsByMessage.set(row.message_id, [row.data])
if (list) list.push(part)
else partsByMessage.set(row.message_id, [part])
}
}
for (const row of rows) {
const info = { ...row.data, id: row.id, sessionID: row.session_id } as MessageV2.Info
yield {
info: row.data,
info,
parts: partsByMessage.get(row.id) ?? [],
}
}
@ -657,7 +664,9 @@ export namespace MessageV2 {
const rows = Database.use((db) =>
db.select().from(PartTable).where(eq(PartTable.message_id, message_id)).orderBy(PartTable.id).all(),
)
return rows.map((row) => row.data)
return rows.map(
(row) => ({ ...row.data, id: row.id, sessionID: row.session_id, messageID: row.message_id }) as MessageV2.Part,
)
})
export const get = fn(
@ -668,8 +677,9 @@ export namespace MessageV2 {
async (input) => {
const row = Database.use((db) => db.select().from(MessageTable).where(eq(MessageTable.id, input.messageID)).get())
if (!row) throw new Error(`Message not found: ${input.messageID}`)
const info = { ...row.data, id: row.id, sessionID: row.session_id } as MessageV2.Info
return {
info: row.data,
info,
parts: await parts(input.messageID),
}
},

View File

@ -3,6 +3,10 @@ import { ProjectTable } from "../project/project.sql"
import type { MessageV2 } from "./message-v2"
import type { Snapshot } from "@/snapshot"
import type { PermissionNext } from "@/permission/next"
import { Database } from "@/storage/db"
type PartData = Omit<MessageV2.Part, "id" | "sessionID" | "messageID">
type InfoData = Omit<MessageV2.Info, "id" | "sessionID">
export const SessionTable = sqliteTable(
"session",
@ -21,13 +25,9 @@ export const SessionTable = sqliteTable(
summary_deletions: integer(),
summary_files: integer(),
summary_diffs: text({ mode: "json" }).$type<Snapshot.FileDiff[]>(),
revert_message_id: text(),
revert_part_id: text(),
revert_snapshot: text(),
revert_diff: text(),
revert: text({ mode: "json" }).$type<{ messageID: string; partID?: string; snapshot?: string; diff?: string }>(),
permission: text({ mode: "json" }).$type<PermissionNext.Ruleset>(),
time_created: integer().notNull(),
time_updated: integer().notNull(),
...Database.Timestamps,
time_compacting: integer(),
time_archived: integer(),
},
@ -41,8 +41,8 @@ export const MessageTable = sqliteTable(
session_id: text()
.notNull()
.references(() => SessionTable.id, { onDelete: "cascade" }),
created_at: integer().notNull(),
data: text({ mode: "json" }).notNull().$type<MessageV2.Info>(),
...Database.Timestamps,
data: text({ mode: "json" }).notNull().$type<InfoData>(),
},
(table) => [index("message_session_idx").on(table.session_id)],
)
@ -55,7 +55,8 @@ export const PartTable = sqliteTable(
.notNull()
.references(() => MessageTable.id, { onDelete: "cascade" }),
session_id: text().notNull(),
data: text({ mode: "json" }).notNull().$type<MessageV2.Part>(),
...Database.Timestamps,
data: text({ mode: "json" }).notNull().$type<PartData>(),
},
(table) => [index("part_message_idx").on(table.message_id), index("part_session_idx").on(table.session_id)],
)
@ -71,6 +72,7 @@ export const TodoTable = sqliteTable(
status: text().notNull(),
priority: text().notNull(),
position: integer().notNull(),
...Database.Timestamps,
},
(table) => [primaryKey({ columns: [table.session_id, table.id] }), index("todo_session_idx").on(table.session_id)],
)
@ -79,5 +81,6 @@ export const PermissionTable = sqliteTable("permission", {
project_id: text()
.primaryKey()
.references(() => ProjectTable.id, { onDelete: "cascade" }),
...Database.Timestamps,
data: text({ mode: "json" }).notNull().$type<PermissionNext.Ruleset>(),
})

View File

@ -1,5 +1,6 @@
import { sqliteTable, text } from "drizzle-orm/sqlite-core"
import { SessionTable } from "../session/session.sql"
import { Database } from "@/storage/db"
export const SessionShareTable = sqliteTable("session_share", {
session_id: text()
@ -8,4 +9,5 @@ export const SessionShareTable = sqliteTable("session_share", {
id: text().notNull(),
secret: text().notNull(),
url: text().notNull(),
...Database.Timestamps,
})

View File

@ -1,7 +1,7 @@
import { Database as BunDatabase } from "bun:sqlite"
import { drizzle, type SQLiteBunDatabase } from "drizzle-orm/bun-sqlite"
import { migrate } from "drizzle-orm/bun-sqlite/migrator"
import type { SQLiteTransaction } from "drizzle-orm/sqlite-core"
import { integer, type SQLiteTransaction } from "drizzle-orm/sqlite-core"
export * from "drizzle-orm"
import { Context } from "../util/context"
import { lazy } from "../util/lazy"
@ -137,4 +137,11 @@ export namespace Database {
throw err
}
}
export const Timestamps = {
time_created: integer().notNull(),
time_updated: integer()
.notNull()
.$onUpdate(() => Date.now()),
}
}

View File

@ -118,10 +118,7 @@ export namespace JsonMigration {
summary_deletions: data.summary?.deletions ?? null,
summary_files: data.summary?.files ?? null,
summary_diffs: data.summary?.diffs ?? null,
revert_message_id: data.revert?.messageID ?? null,
revert_part_id: data.revert?.partID ?? null,
revert_snapshot: data.revert?.snapshot ?? null,
revert_diff: data.revert?.diff ?? null,
revert: data.revert ?? null,
permission: data.permission ?? null,
time_created: data.time?.created ?? Date.now(),
time_updated: data.time?.updated ?? Date.now(),
@ -159,11 +156,13 @@ export namespace JsonMigration {
stats.errors.push(`message missing id: ${item.file}`)
continue
}
const { id, sessionID: _, ...rest } = data
values.push({
id: data.id,
session_id: sessionID,
created_at: data.time?.created ?? Date.now(),
data,
time_created: data.time?.created ?? Date.now(),
time_updated: data.time?.updated ?? Date.now(),
data: rest,
})
messageIds.add(data.id)
}
@ -191,11 +190,14 @@ export namespace JsonMigration {
stats.errors.push(`part missing id or messageID: ${item.file}`)
continue
}
const { id, messageID, sessionID: _, ...rest } = data
values.push({
id: data.id,
message_id: data.messageID,
session_id: sessionID,
data,
time_created: data.time?.created ?? Date.now(),
time_updated: data.time?.updated ?? Date.now(),
data: rest,
})
}
if (values.length === 0) continue

View File

@ -196,11 +196,11 @@ describe("JSON to SQLite migration", () => {
const db = drizzle({ client: sqlite })
const messages = db.select().from(MessageTable).all()
expect(messages.length).toBe(1)
expect(messages[0].data.id).toBe("msg_test789ghi")
expect(messages[0].id).toBe("msg_test789ghi")
const parts = db.select().from(PartTable).all()
expect(parts.length).toBe(1)
expect(parts[0].data.id).toBe("prt_testabc123")
expect(parts[0].id).toBe("prt_testabc123")
})
test("skips orphaned sessions (no parent project)", async () => {