import type { AISession, AISessionRole, Board, Book, Chapter, Character, Entity, Note, Project, ProjectMeta, User, WorldItem, } from "@pubscript/core"; // ---- Row shapes (mirror the SQLite schema) ---- export interface UserRow { id: string; email: string; pass_hash: string; display_name: string; role: "admin" | "author"; disabled: number; created_at: string; } export interface ShareRow { project_id: string; user_id: string; perm: "read" | "edit"; } export interface ProjectRow { id: string; owner_id: string; title: string; settings: string; cover: string; created_at: string; updated_at: string; } export interface BookRow { id: string; project_id: string; title: string; ord: number; } export interface ChapterRow { id: string; book_id: string; project_id: string; title: string; doc: string; rev: number; ord: number; updated_at: string; } export interface EntityRow { id: string; project_id: string; kind: string; name: string; fields: string; created_at: string; updated_at: string; } export interface BoardRow { id: string; project_id: string; nodes: string; edges: string; updated_at: string; } export interface AISessionRow { id: string; project_id: string; role: string; model: string; settings: string; messages: string; suggestions: string; updated_at: string; } // ---- Row -> core types ---- export function toUser(r: UserRow): User { return { id: r.id, email: r.email, displayName: r.display_name, role: r.role, createdAt: r.created_at, }; } export function toProject(r: ProjectRow, shares: ShareRow[]): Project { return { id: r.id, projectId: r.id, title: r.title, ownerId: r.owner_id, sharedWith: shares.map((s) => ({ userId: s.user_id, perm: s.perm })), settings: JSON.parse(r.settings), cover: JSON.parse(r.cover), createdAt: r.created_at, updatedAt: r.updated_at, }; } export function toProjectMeta(r: ProjectRow, shares: ShareRow[], user: User): ProjectMeta { const owned = r.owner_id === user.id; const mine = shares.find((s) => s.user_id === user.id); return { id: r.id, projectId: r.id, title: r.title, ownerId: r.owner_id, cover: JSON.parse(r.cover), createdAt: r.created_at, updatedAt: r.updated_at, perm: owned ? "edit" : (mine?.perm ?? "read"), owned, }; } export function toBook(r: BookRow, chapters: ChapterRow[]): Book { return { id: r.id, projectId: r.project_id, title: r.title, order: r.ord, chapters: chapters .slice() .sort((a, b) => a.ord - b.ord) .map(toChapter), }; } export function toChapter(r: ChapterRow): Chapter { return { id: r.id, bookId: r.book_id, projectId: r.project_id, title: r.title, doc: JSON.parse(r.doc), rev: r.rev, order: r.ord, meta: { wordCount: wordCount(JSON.parse(r.doc)), scenes: [], updatedAt: r.updated_at, }, }; } export function toBoard(r: BoardRow): Board { return { id: r.id, projectId: r.project_id, nodes: JSON.parse(r.nodes), edges: JSON.parse(r.edges), }; } export function toAISession(r: AISessionRow): AISession { return { id: r.id, projectId: r.project_id, role: r.role as AISessionRole, model: r.model, settings: JSON.parse(r.settings), messages: JSON.parse(r.messages), suggestions: JSON.parse(r.suggestions), }; } export function toEntity(r: EntityRow): Entity { const f = JSON.parse(r.fields); if (r.kind === "character") { return { id: r.id, projectId: r.project_id, name: r.name, role: f.role ?? "", bio: f.bio ?? "", appearance: f.appearance ?? "", voice: f.voice ?? "", relationships: typeof f.relationships === "string" ? JSON.parse(f.relationships) : (f.relationships ?? []), arc: f.arc ?? "", status: f.status ?? "active", tags: f.tags ?? [], } satisfies Character; } if (r.kind === "world") { return { id: r.id, projectId: r.project_id, kind: f.kind ?? "location", name: r.name, fields: f.fields ?? {}, } satisfies WorldItem; } return { id: r.id, projectId: r.project_id, body: f.body ?? "", pinned: !!f.pinned, tags: f.tags ?? [], linkedTo: f.linkedTo ?? [], } satisfies Note; } // ---- core types -> row (for writes) ---- export function entityToRow(e: Entity): Omit { if ("fields" in e) { // WorldItem return { id: e.id, project_id: e.projectId, kind: "world", name: e.name, fields: JSON.stringify({ kind: e.kind, fields: e.fields }), }; } if ("body" in e) { // Note return { id: e.id, project_id: e.projectId, kind: "note", name: e.body.slice(0, 80), fields: JSON.stringify({ body: e.body, pinned: e.pinned, tags: e.tags, linkedTo: e.linkedTo }), }; } // Character const c = e as Character; return { id: c.id, project_id: c.projectId, kind: "character", name: c.name, fields: JSON.stringify({ role: c.role, bio: c.bio, appearance: c.appearance, voice: c.voice, relationships: c.relationships, arc: c.arc, status: c.status, tags: c.tags, }), }; } // ---- helpers ---- /** Concatenate every human-readable field of an entity into one searchable string. */ export function entitySearchText(e: Entity): string { if ("fields" in e) { const parts = [e.name, e.kind]; for (const v of Object.values(e.fields)) if (typeof v === "string") parts.push(v); return parts.join(" "); } if ("body" in e) { return [e.body, ...e.tags].join(" "); } const c = e as Character; const relText = (c.relationships ?? []).map((r) => `${r.targetId} ${r.kind} ${r.note}`).join(" "); return [c.name, c.role, c.bio, c.appearance, c.voice, relText, c.arc, c.status, ...c.tags].join(" "); } /** Count words in a ProseMirror doc (text nodes only). */ export function wordCount(doc: unknown): number { let count = 0; const walk = (node: any): void => { if (node && typeof node === "object") { if (node.type === "text") { const t = String(node.text ?? ""); count += t.split(/\s+/).filter(Boolean).length; } if (Array.isArray(node.content)) for (const c of node.content) walk(c); } }; walk(doc); return count; }