forked from mirror/crumbs-original
feat: add shares service layer with batch fetch and hydration
Create shares-service.ts with fetchSharesForNotes, createShare, revokeShare, getShareByNoteId, and getSharedNote. Integrate share tokens into hydrateNotes pipeline alongside tags, attachments, and collaborators. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
Fred Cabassut
co-authored by
Claude Opus 4.6
parent
228a724c32
commit
90996cc72d
@@ -55,6 +55,7 @@
|
|||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"lucide-svelte": "^0.577.0",
|
"lucide-svelte": "^0.577.0",
|
||||||
"markdown-it": "^14.1.1",
|
"markdown-it": "^14.1.1",
|
||||||
|
"nanoid": "^5.1.7",
|
||||||
"node-diff3": "^3.2.0",
|
"node-diff3": "^3.2.0",
|
||||||
"nodemailer": "^8.0.2",
|
"nodemailer": "^8.0.2",
|
||||||
"svelte-dnd-action": "^0.9.69",
|
"svelte-dnd-action": "^0.9.69",
|
||||||
|
|||||||
Generated
+10
@@ -80,6 +80,9 @@ importers:
|
|||||||
markdown-it:
|
markdown-it:
|
||||||
specifier: ^14.1.1
|
specifier: ^14.1.1
|
||||||
version: 14.1.1
|
version: 14.1.1
|
||||||
|
nanoid:
|
||||||
|
specifier: ^5.1.7
|
||||||
|
version: 5.1.7
|
||||||
node-diff3:
|
node-diff3:
|
||||||
specifier: ^3.2.0
|
specifier: ^3.2.0
|
||||||
version: 3.2.0
|
version: 3.2.0
|
||||||
@@ -2998,6 +3001,11 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
nanoid@5.1.7:
|
||||||
|
resolution: {integrity: sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==}
|
||||||
|
engines: {node: ^18 || >=20}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
napi-build-utils@2.0.0:
|
napi-build-utils@2.0.0:
|
||||||
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
|
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
|
||||||
|
|
||||||
@@ -6783,6 +6791,8 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
|
nanoid@5.1.7: {}
|
||||||
|
|
||||||
napi-build-utils@2.0.0: {}
|
napi-build-utils@2.0.0: {}
|
||||||
|
|
||||||
negotiator@1.0.0: {}
|
negotiator@1.0.0: {}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { extractTags } from '$lib/utils/tags.js';
|
|||||||
import { fetchTagsForNotes, syncNoteTags } from './tags.js';
|
import { fetchTagsForNotes, syncNoteTags } from './tags.js';
|
||||||
import { fetchAttachmentsForNotes } from './attachments.js';
|
import { fetchAttachmentsForNotes } from './attachments.js';
|
||||||
import { fetchCollaboratorsForNotes } from './collaborators.js';
|
import { fetchCollaboratorsForNotes } from './collaborators.js';
|
||||||
|
import { fetchSharesForNotes } from './shares-service.js';
|
||||||
import { canAccessNote } from './api-utils.js';
|
import { canAccessNote } from './api-utils.js';
|
||||||
import { createSnapshot } from './versions-service.js';
|
import { createSnapshot } from './versions-service.js';
|
||||||
import { mergeContent } from '$lib/utils/content-merge.js';
|
import { mergeContent } from '$lib/utils/content-merge.js';
|
||||||
@@ -33,6 +34,7 @@ function hydrateNotes(db: Db, noteRows: NoteRow[], userId: number) {
|
|||||||
const tagMap = fetchTagsForNotes(db, noteIds);
|
const tagMap = fetchTagsForNotes(db, noteIds);
|
||||||
const attachmentMap = fetchAttachmentsForNotes(db, noteIds);
|
const attachmentMap = fetchAttachmentsForNotes(db, noteIds);
|
||||||
const collabMap = fetchCollaboratorsForNotes(db, noteIds);
|
const collabMap = fetchCollaboratorsForNotes(db, noteIds);
|
||||||
|
const shareMap = fetchSharesForNotes(db, noteIds);
|
||||||
|
|
||||||
return noteRows.map((note) => {
|
return noteRows.map((note) => {
|
||||||
const collaborators = collabMap.get(note.id) ?? [];
|
const collaborators = collabMap.get(note.id) ?? [];
|
||||||
@@ -44,7 +46,8 @@ function hydrateNotes(db: Db, noteRows: NoteRow[], userId: number) {
|
|||||||
attachments: attachmentMap.get(note.id) ?? [],
|
attachments: attachmentMap.get(note.id) ?? [],
|
||||||
collaborators,
|
collaborators,
|
||||||
isOwner,
|
isOwner,
|
||||||
isShared
|
isShared,
|
||||||
|
shareToken: shareMap.get(note.id) ?? undefined
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import type { Db } from './db/index.js';
|
||||||
|
import { sharedNotes, notes, attachments } from './db/schema.js';
|
||||||
|
import { eq, and, inArray } from 'drizzle-orm';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { requireNoteOwnership } from './api-utils.js';
|
||||||
|
import type { SharedNoteData, Attachment } from '$lib/types/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch fetch share tokens for multiple notes.
|
||||||
|
* Returns a Map of noteId → token.
|
||||||
|
*/
|
||||||
|
export function fetchSharesForNotes(db: Db, noteIds: string[]): Map<string, string> {
|
||||||
|
if (noteIds.length === 0) return new Map();
|
||||||
|
|
||||||
|
const rows = db
|
||||||
|
.select({ noteId: sharedNotes.noteId, token: sharedNotes.token })
|
||||||
|
.from(sharedNotes)
|
||||||
|
.where(inArray(sharedNotes.noteId, noteIds))
|
||||||
|
.all();
|
||||||
|
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
for (const row of rows) {
|
||||||
|
map.set(row.noteId, row.token);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a public share link for a note. Idempotent — returns existing token if already shared.
|
||||||
|
*/
|
||||||
|
export function createShare(db: Db, noteId: string, userId: number): { token: string } {
|
||||||
|
requireNoteOwnership(db, noteId, userId);
|
||||||
|
|
||||||
|
const existing = db
|
||||||
|
.select({ token: sharedNotes.token })
|
||||||
|
.from(sharedNotes)
|
||||||
|
.where(eq(sharedNotes.noteId, noteId))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (existing) return { token: existing.token };
|
||||||
|
|
||||||
|
const token = nanoid(21);
|
||||||
|
db.insert(sharedNotes)
|
||||||
|
.values({ noteId, token, createdAt: new Date() })
|
||||||
|
.run();
|
||||||
|
|
||||||
|
return { token };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke a public share link for a note.
|
||||||
|
*/
|
||||||
|
export function revokeShare(db: Db, noteId: string, userId: number): void {
|
||||||
|
requireNoteOwnership(db, noteId, userId);
|
||||||
|
|
||||||
|
db.delete(sharedNotes)
|
||||||
|
.where(eq(sharedNotes.noteId, noteId))
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get share info for a specific note.
|
||||||
|
*/
|
||||||
|
export function getShareByNoteId(db: Db, noteId: string) {
|
||||||
|
return db
|
||||||
|
.select()
|
||||||
|
.from(sharedNotes)
|
||||||
|
.where(eq(sharedNotes.noteId, noteId))
|
||||||
|
.get() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a shared note by its public token. Returns null if note is trashed or token is invalid.
|
||||||
|
*/
|
||||||
|
export function getSharedNote(db: Db, token: string): (SharedNoteData & { noteId: string }) | null {
|
||||||
|
const share = db
|
||||||
|
.select()
|
||||||
|
.from(sharedNotes)
|
||||||
|
.where(eq(sharedNotes.token, token))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (!share) return null;
|
||||||
|
|
||||||
|
const note = db
|
||||||
|
.select()
|
||||||
|
.from(notes)
|
||||||
|
.where(and(eq(notes.id, share.noteId), eq(notes.trashed, false)))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (!note) return null;
|
||||||
|
|
||||||
|
const noteAttachments = db
|
||||||
|
.select()
|
||||||
|
.from(attachments)
|
||||||
|
.where(eq(attachments.noteId, share.noteId))
|
||||||
|
.all() as Attachment[];
|
||||||
|
|
||||||
|
return {
|
||||||
|
noteId: note.id,
|
||||||
|
title: note.title,
|
||||||
|
content: note.content,
|
||||||
|
checklistMode: note.checklistMode,
|
||||||
|
color: note.color as SharedNoteData['color'],
|
||||||
|
attachments: noteAttachments,
|
||||||
|
createdAt: note.createdAt,
|
||||||
|
updatedAt: note.updatedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user