feat(project): brand ProjectID through Drizzle and Zod schemas

Introduce Effect branded ProjectID type into Drizzle table columns
and Zod schemas so the brand flows through z.infer automatically.
Adds ProjectID.zod helper to bridge Effect brands into Zod,
eliminating manual Omit & intersect type overrides.
This commit is contained in:
Kit Langton
2026-03-10 21:27:02 -04:00
parent a1b06d63c9
commit e9f973ebc2
15 changed files with 82 additions and 46 deletions

View File

@@ -8,6 +8,7 @@ import { readFileSync, readdirSync } from "fs"
import { JsonMigration } from "../../src/storage/json-migration"
import { Global } from "../../src/global"
import { ProjectTable } from "../../src/project/project.sql"
import { ProjectID } from "../../src/project/schema"
import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../../src/session/session.sql"
import { SessionShareTable } from "../../src/share/share.sql"
@@ -123,7 +124,7 @@ describe("JSON to SQLite migration", () => {
const db = drizzle({ client: sqlite })
const projects = db.select().from(ProjectTable).all()
expect(projects.length).toBe(1)
expect(projects[0].id).toBe("proj_test123abc")
expect(projects[0].id).toBe(ProjectID.make("proj_test123abc"))
expect(projects[0].worktree).toBe("/test/path")
expect(projects[0].name).toBe("Test Project")
expect(projects[0].sandboxes).toEqual(["/test/sandbox"])
@@ -148,7 +149,7 @@ describe("JSON to SQLite migration", () => {
const db = drizzle({ client: sqlite })
const projects = db.select().from(ProjectTable).all()
expect(projects.length).toBe(1)
expect(projects[0].id).toBe("proj_filename") // Uses filename, not JSON id
expect(projects[0].id).toBe(ProjectID.make("proj_filename")) // Uses filename, not JSON id
})
test("migrates project with commands", async () => {
@@ -169,7 +170,7 @@ describe("JSON to SQLite migration", () => {
const db = drizzle({ client: sqlite })
const projects = db.select().from(ProjectTable).all()
expect(projects.length).toBe(1)
expect(projects[0].id).toBe("proj_with_commands")
expect(projects[0].id).toBe(ProjectID.make("proj_with_commands"))
expect(projects[0].commands).toEqual({ start: "npm run dev" })
})
@@ -190,7 +191,7 @@ describe("JSON to SQLite migration", () => {
const db = drizzle({ client: sqlite })
const projects = db.select().from(ProjectTable).all()
expect(projects.length).toBe(1)
expect(projects[0].id).toBe("proj_no_commands")
expect(projects[0].id).toBe(ProjectID.make("proj_no_commands"))
expect(projects[0].commands).toBeNull()
})
@@ -220,7 +221,7 @@ describe("JSON to SQLite migration", () => {
const sessions = db.select().from(SessionTable).all()
expect(sessions.length).toBe(1)
expect(sessions[0].id).toBe("ses_test456def")
expect(sessions[0].project_id).toBe("proj_test123abc")
expect(sessions[0].project_id).toBe(ProjectID.make("proj_test123abc"))
expect(sessions[0].slug).toBe("test-session")
expect(sessions[0].title).toBe("Test Session Title")
expect(sessions[0].summary_additions).toBe(10)
@@ -426,7 +427,7 @@ describe("JSON to SQLite migration", () => {
const sessions = db.select().from(SessionTable).all()
expect(sessions.length).toBe(1)
expect(sessions[0].id).toBe("ses_migrated")
expect(sessions[0].project_id).toBe(gitBasedProjectID) // Uses directory, not stale JSON
expect(sessions[0].project_id).toBe(ProjectID.make(gitBasedProjectID)) // Uses directory, not stale JSON
})
test("uses filename for session id when JSON has different value", async () => {
@@ -458,7 +459,7 @@ describe("JSON to SQLite migration", () => {
const sessions = db.select().from(SessionTable).all()
expect(sessions.length).toBe(1)
expect(sessions[0].id).toBe("ses_from_filename") // Uses filename, not JSON id
expect(sessions[0].project_id).toBe("proj_test123abc")
expect(sessions[0].project_id).toBe(ProjectID.make("proj_test123abc"))
})
test("is idempotent (running twice doesn't duplicate)", async () => {
@@ -643,7 +644,7 @@ describe("JSON to SQLite migration", () => {
const db = drizzle({ client: sqlite })
const projects = db.select().from(ProjectTable).all()
expect(projects.length).toBe(1)
expect(projects[0].id).toBe("proj_test123abc")
expect(projects[0].id).toBe(ProjectID.make("proj_test123abc"))
})
test("skips invalid todo entries while preserving source positions", async () => {