sync
This commit is contained in:
parent
2c234b8d62
commit
a4183c3b2c
11 changed files with 193 additions and 46 deletions
|
|
@ -4,4 +4,7 @@ export default defineConfig({
|
|||
dialect: "sqlite",
|
||||
schema: "./src/**/*.sql.ts",
|
||||
out: "./migration",
|
||||
dbCredentials: {
|
||||
url: "/home/thdxr/.local/share/opencode/opencode.db",
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
"@types/turndown": "5.0.5",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"better-sqlite3": "12.6.0",
|
||||
"drizzle-kit": "0.31.8",
|
||||
"typescript": "catalog:",
|
||||
"vscode-languageserver-types": "3.17.5",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
TodoTable,
|
||||
PermissionTable,
|
||||
} from "../../session/session.sql"
|
||||
import { Session } from "../../session"
|
||||
import { SessionShareTable, ShareTable } from "../../share/share.sql"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
|
|
@ -66,7 +67,7 @@ const ExportCommand = cmd({
|
|||
for (const row of db().select().from(SessionTable).all()) {
|
||||
const dir = path.join(sessionDir, row.projectID)
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
await Bun.write(path.join(dir, `${row.id}.json`), JSON.stringify(row.data, null, 2))
|
||||
await Bun.write(path.join(dir, `${row.id}.json`), JSON.stringify(Session.fromRow(row), null, 2))
|
||||
stats.sessions++
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -84,15 +84,11 @@ export const ImportCommand = cmd({
|
|||
|
||||
db()
|
||||
.insert(SessionTable)
|
||||
.values({
|
||||
id: exportData.info.id,
|
||||
projectID: Instance.project.id,
|
||||
parentID: exportData.info.parentID,
|
||||
createdAt: exportData.info.time.created,
|
||||
updatedAt: exportData.info.time.updated,
|
||||
data: exportData.info,
|
||||
.values(Session.toRow({ ...exportData.info, projectID: Instance.project.id }))
|
||||
.onConflictDoUpdate({
|
||||
target: SessionTable.id,
|
||||
set: Session.toRow({ ...exportData.info, projectID: Instance.project.id }),
|
||||
})
|
||||
.onConflictDoUpdate({ target: SessionTable.id, set: { data: exportData.info } })
|
||||
.run()
|
||||
|
||||
for (const msg of exportData.messages) {
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ async function getCurrentProject(): Promise<Project.Info> {
|
|||
|
||||
async function getAllSessions(): Promise<Session.Info[]> {
|
||||
const sessionRows = db().select().from(SessionTable).all()
|
||||
return sessionRows.map((row) => row.data)
|
||||
return sessionRows.map((row) => Session.fromRow(row))
|
||||
}
|
||||
|
||||
export async function aggregateSessionStats(days?: number, projectFilter?: string): Promise<SessionStats> {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { SessionTable } from "../session/session.sql"
|
|||
import { eq } from "drizzle-orm"
|
||||
import { Log } from "../util/log"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import type { Session } from "../session"
|
||||
import { Session } from "../session"
|
||||
import { work } from "../util/queue"
|
||||
import { fn } from "@opencode-ai/util/fn"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
|
|
@ -304,17 +304,13 @@ export namespace Project {
|
|||
log.info("migrating sessions from global", { newProjectID, worktree, count: globalSessions.length })
|
||||
|
||||
await work(10, globalSessions, async (row) => {
|
||||
const session = row.data as Session.Info
|
||||
const session = Session.fromRow(row)
|
||||
if (!session) return
|
||||
if (session.directory && session.directory !== worktree) return
|
||||
|
||||
session.projectID = newProjectID
|
||||
log.info("migrating session", { sessionID: session.id, from: "global", to: newProjectID })
|
||||
db()
|
||||
.update(SessionTable)
|
||||
.set({ projectID: newProjectID, data: session })
|
||||
.where(eq(SessionTable.id, session.id))
|
||||
.run()
|
||||
db().update(SessionTable).set(Session.toRow(session)).where(eq(SessionTable.id, session.id)).run()
|
||||
}).catch((error) => {
|
||||
log.error("failed to migrate sessions from global to project", { error, projectId: newProjectID })
|
||||
})
|
||||
|
|
|
|||
|
|
@ -42,6 +42,75 @@ export namespace Session {
|
|||
).test(title)
|
||||
}
|
||||
|
||||
type SessionRow = typeof SessionTable.$inferSelect
|
||||
|
||||
export function fromRow(row: SessionRow): Info {
|
||||
const summary =
|
||||
row.summary_additions !== null || row.summary_deletions !== null || row.summary_files !== null
|
||||
? {
|
||||
additions: row.summary_additions ?? 0,
|
||||
deletions: row.summary_deletions ?? 0,
|
||||
files: row.summary_files ?? 0,
|
||||
diffs: row.summary_diffs ?? undefined,
|
||||
}
|
||||
: undefined
|
||||
const share = row.share_url ? { url: row.share_url } : undefined
|
||||
const revert =
|
||||
row.revert_messageID !== null
|
||||
? {
|
||||
messageID: row.revert_messageID,
|
||||
partID: row.revert_partID ?? undefined,
|
||||
snapshot: row.revert_snapshot ?? undefined,
|
||||
diff: row.revert_diff ?? undefined,
|
||||
}
|
||||
: undefined
|
||||
return {
|
||||
id: row.id,
|
||||
slug: row.slug,
|
||||
projectID: row.projectID,
|
||||
directory: row.directory,
|
||||
parentID: row.parentID ?? undefined,
|
||||
title: row.title,
|
||||
version: row.version,
|
||||
summary,
|
||||
share,
|
||||
revert,
|
||||
permission: row.permission ?? undefined,
|
||||
time: {
|
||||
created: row.time_created,
|
||||
updated: row.time_updated,
|
||||
compacting: row.time_compacting ?? undefined,
|
||||
archived: row.time_archived ?? undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function toRow(info: Info) {
|
||||
return {
|
||||
id: info.id,
|
||||
projectID: info.projectID,
|
||||
parentID: info.parentID,
|
||||
slug: info.slug,
|
||||
directory: info.directory,
|
||||
title: info.title,
|
||||
version: info.version,
|
||||
share_url: info.share?.url,
|
||||
summary_additions: info.summary?.additions,
|
||||
summary_deletions: info.summary?.deletions,
|
||||
summary_files: info.summary?.files,
|
||||
summary_diffs: info.summary?.diffs,
|
||||
revert_messageID: info.revert?.messageID,
|
||||
revert_partID: info.revert?.partID,
|
||||
revert_snapshot: info.revert?.snapshot,
|
||||
revert_diff: info.revert?.diff,
|
||||
permission: info.permission,
|
||||
time_created: info.time.created,
|
||||
time_updated: info.time.updated,
|
||||
time_compacting: info.time.compacting,
|
||||
time_archived: info.time.archived,
|
||||
}
|
||||
}
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
id: Identifier.schema("session"),
|
||||
|
|
@ -214,17 +283,7 @@ export namespace Session {
|
|||
},
|
||||
}
|
||||
log.info("created", result)
|
||||
db()
|
||||
.insert(SessionTable)
|
||||
.values({
|
||||
id: result.id,
|
||||
projectID: result.projectID,
|
||||
parentID: result.parentID,
|
||||
createdAt: result.time.created,
|
||||
updatedAt: result.time.updated,
|
||||
data: result,
|
||||
})
|
||||
.run()
|
||||
db().insert(SessionTable).values(toRow(result)).run()
|
||||
Bus.publish(Event.Created, {
|
||||
info: result,
|
||||
})
|
||||
|
|
@ -255,7 +314,7 @@ export namespace Session {
|
|||
export const get = fn(Identifier.schema("session"), async (id) => {
|
||||
const row = db().select().from(SessionTable).where(eq(SessionTable.id, id)).get()
|
||||
if (!row) throw new NotFoundError({ message: `Session not found: ${id}` })
|
||||
return row.data
|
||||
return fromRow(row)
|
||||
})
|
||||
|
||||
export const getShare = fn(Identifier.schema("session"), async (id) => {
|
||||
|
|
@ -298,12 +357,12 @@ export namespace Session {
|
|||
export function update(id: string, editor: (session: Info) => void, options?: { touch?: boolean }) {
|
||||
const row = db().select().from(SessionTable).where(eq(SessionTable.id, id)).get()
|
||||
if (!row) throw new Error(`Session not found: ${id}`)
|
||||
const data = { ...row.data }
|
||||
const data = fromRow(row)
|
||||
editor(data)
|
||||
if (options?.touch !== false) {
|
||||
data.time.updated = Date.now()
|
||||
}
|
||||
db().update(SessionTable).set({ updatedAt: data.time.updated, data }).where(eq(SessionTable.id, id)).run()
|
||||
db().update(SessionTable).set(toRow(data)).where(eq(SessionTable.id, id)).run()
|
||||
Bus.publish(Event.Updated, {
|
||||
info: data,
|
||||
})
|
||||
|
|
@ -335,13 +394,13 @@ export namespace Session {
|
|||
const project = Instance.project
|
||||
const rows = db().select().from(SessionTable).where(eq(SessionTable.projectID, project.id)).all()
|
||||
for (const row of rows) {
|
||||
yield row.data
|
||||
yield fromRow(row)
|
||||
}
|
||||
}
|
||||
|
||||
export const children = fn(Identifier.schema("session"), async (parentID) => {
|
||||
const rows = db().select().from(SessionTable).where(eq(SessionTable.parentID, parentID)).all()
|
||||
return rows.map((row) => row.data)
|
||||
return rows.map((row) => fromRow(row))
|
||||
})
|
||||
|
||||
export const remove = fn(Identifier.schema("session"), async (sessionID) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core"
|
||||
import { ProjectTable } from "../project/project.sql"
|
||||
import type { Session } from "./index"
|
||||
import type { MessageV2 } from "./message-v2"
|
||||
import type { Snapshot } from "@/snapshot"
|
||||
import type { Todo } from "./todo"
|
||||
|
|
@ -14,9 +13,24 @@ export const SessionTable = sqliteTable(
|
|||
.notNull()
|
||||
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
||||
parentID: text("parent_id"),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
data: text("data", { mode: "json" }).notNull().$type<Session.Info>(),
|
||||
slug: text("slug").notNull(),
|
||||
directory: text("directory").notNull(),
|
||||
title: text("title").notNull(),
|
||||
version: text("version").notNull(),
|
||||
share_url: text("share_url"),
|
||||
summary_additions: integer("summary_additions"),
|
||||
summary_deletions: integer("summary_deletions"),
|
||||
summary_files: integer("summary_files"),
|
||||
summary_diffs: text("summary_diffs", { mode: "json" }).$type<Snapshot.FileDiff[]>(),
|
||||
revert_messageID: text("revert_message_id"),
|
||||
revert_partID: text("revert_part_id"),
|
||||
revert_snapshot: text("revert_snapshot"),
|
||||
revert_diff: text("revert_diff"),
|
||||
permission: text("permission", { mode: "json" }).$type<PermissionNext.Ruleset>(),
|
||||
time_created: integer("time_created").notNull(),
|
||||
time_updated: integer("time_updated").notNull(),
|
||||
time_compacting: integer("time_compacting"),
|
||||
time_archived: integer("time_archived"),
|
||||
},
|
||||
(table) => [index("session_project_idx").on(table.projectID), index("session_parent_idx").on(table.parentID)],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -97,10 +97,25 @@ export async function migrateFromJson(sqlite: Database, customStorageDir?: strin
|
|||
.values({
|
||||
id: data.id,
|
||||
projectID: data.projectID,
|
||||
parentID: data.parentID,
|
||||
createdAt: data.time?.created ?? Date.now(),
|
||||
updatedAt: data.time?.updated ?? Date.now(),
|
||||
data,
|
||||
parentID: data.parentID ?? null,
|
||||
slug: data.slug ?? "",
|
||||
directory: data.directory ?? "",
|
||||
title: data.title ?? "",
|
||||
version: data.version ?? "",
|
||||
share_url: data.share?.url ?? null,
|
||||
summary_additions: data.summary?.additions ?? null,
|
||||
summary_deletions: data.summary?.deletions ?? null,
|
||||
summary_files: data.summary?.files ?? null,
|
||||
summary_diffs: data.summary?.diffs ?? null,
|
||||
revert_messageID: data.revert?.messageID ?? null,
|
||||
revert_partID: data.revert?.partID ?? null,
|
||||
revert_snapshot: data.revert?.snapshot ?? null,
|
||||
revert_diff: data.revert?.diff ?? null,
|
||||
permission: data.permission ?? null,
|
||||
time_created: data.time?.created ?? Date.now(),
|
||||
time_updated: data.time?.updated ?? Date.now(),
|
||||
time_compacting: data.time?.compacting ?? null,
|
||||
time_archived: data.time?.archived ?? null,
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.run()
|
||||
|
|
|
|||
|
|
@ -239,10 +239,10 @@ describe("JSON to SQLite migration", () => {
|
|||
expect(stats?.sessions).toBe(1)
|
||||
const db = drizzle(sqlite)
|
||||
const row = db.select().from(SessionTable).where(eq(SessionTable.id, fixtures.session.id)).get()
|
||||
expect(row?.data.id).toBe(fixtures.session.id)
|
||||
expect(row?.id).toBe(fixtures.session.id)
|
||||
expect(row?.projectID).toBe(fixtures.project.id)
|
||||
expect(row?.createdAt).toBe(fixtures.session.time.created)
|
||||
expect(row?.updatedAt).toBe(fixtures.session.time.updated)
|
||||
expect(row?.time_created).toBe(fixtures.session.time.created)
|
||||
expect(row?.time_updated).toBe(fixtures.session.time.updated)
|
||||
})
|
||||
|
||||
test("migrates session with parentID", async () => {
|
||||
|
|
@ -296,8 +296,8 @@ describe("JSON to SQLite migration", () => {
|
|||
expect(stats?.sessions).toBe(1)
|
||||
const db = drizzle(sqlite)
|
||||
const row = db.select().from(SessionTable).where(eq(SessionTable.id, fixtures.session.id)).get()
|
||||
expect(row?.createdAt).toBeGreaterThanOrEqual(before)
|
||||
expect(row?.createdAt).toBeLessThanOrEqual(after)
|
||||
expect(row?.time_created).toBeGreaterThanOrEqual(before)
|
||||
expect(row?.time_created).toBeLessThanOrEqual(after)
|
||||
})
|
||||
|
||||
test("skips session with missing required fields", async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue