/** * rNotes Automerge document schemas. * * Granularity: one Automerge document per notebook. * DocId format: {space}:notes:notebooks:{notebookId} * * The shape matches the PG→Automerge migration adapter * (server/local-first/migration/pg-to-automerge.ts:notesMigration) * and the client-side NotebookDoc type in folk-notes-app.ts. */ import type { DocSchema } from '../../shared/local-first/document'; // ── Document types ── export interface SourceRef { source: 'logseq' | 'obsidian' | 'notion' | 'google-docs' | 'manual'; externalId: string; // Notion page ID, Google Doc ID, file path, etc. lastSyncedAt: number; contentHash?: string; // For conflict detection on re-import } export interface NoteItem { id: string; notebookId: string; authorId: string | null; title: string; content: string; contentPlain: string; contentFormat?: 'html' | 'tiptap-json'; type: 'NOTE' | 'CLIP' | 'BOOKMARK' | 'CODE' | 'IMAGE' | 'FILE' | 'AUDIO'; url: string | null; language: string | null; fileUrl: string | null; mimeType: string | null; fileSize: number | null; duration: number | null; isPinned: boolean; sortOrder: number; tags: string[]; sourceRef?: SourceRef; createdAt: number; updatedAt: number; } export interface NotebookMeta { id: string; title: string; slug: string; description: string; coverColor: string; isPublic: boolean; createdAt: number; updatedAt: number; } export interface NotebookDoc { meta: { module: string; collection: string; version: number; spaceSlug: string; createdAt: number; }; notebook: NotebookMeta; items: Record; } // ── Schema registration ── export interface ConnectionsDoc { meta: { module: string; collection: string; version: number; spaceSlug: string; createdAt: number; }; notion?: { accessToken: string; workspaceId: string; workspaceName: string; connectedAt: number; }; google?: { refreshToken: string; accessToken: string; expiresAt: number; email: string; connectedAt: number; }; } /** Generate a docId for a space's integration connections. */ export function connectionsDocId(space: string) { return `${space}:notes:connections` as const; } export const notebookSchema: DocSchema = { module: 'notes', collection: 'notebooks', version: 3, init: (): NotebookDoc => ({ meta: { module: 'notes', collection: 'notebooks', version: 3, spaceSlug: '', createdAt: Date.now(), }, notebook: { id: '', title: 'Untitled Notebook', slug: '', description: '', coverColor: '#3b82f6', isPublic: false, createdAt: Date.now(), updatedAt: Date.now(), }, items: {}, }), migrate: (doc: NotebookDoc, fromVersion: number): NotebookDoc => { if (fromVersion < 2) { for (const item of Object.values(doc.items)) { if (!(item as any).contentFormat) (item as any).contentFormat = 'html'; } } // v2→v3: sourceRef field is optional, no migration needed return doc; }, }; // ── Helpers ── /** Generate a docId for a notebook. */ export function notebookDocId(space: string, notebookId: string) { return `${space}:notes:notebooks:${notebookId}` as const; } /** Create a fresh NoteItem with defaults. */ export function createNoteItem( id: string, notebookId: string, title: string, opts: Partial = {}, ): NoteItem { const now = Date.now(); return { id, notebookId, authorId: null, title, content: '', contentPlain: '', contentFormat: 'tiptap-json', type: 'NOTE', url: null, language: null, fileUrl: null, mimeType: null, fileSize: null, duration: null, isPinned: false, sortOrder: 0, tags: [], createdAt: now, updatedAt: now, ...opts, }; }