rspace-online/modules/rnotes/schemas.ts

172 lines
3.7 KiB
TypeScript

/**
* 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<string, NoteItem>;
}
// ── 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<NotebookDoc> = {
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> = {},
): 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,
};
}