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:
Frederic Cabassut
2026-03-20 22:06:41 +01:00
committed by Fred Cabassut
co-authored by Claude Opus 4.6
parent 228a724c32
commit 90996cc72d
4 changed files with 123 additions and 1 deletions
+1
View File
@@ -55,6 +55,7 @@
"idb": "^8.0.3",
"lucide-svelte": "^0.577.0",
"markdown-it": "^14.1.1",
"nanoid": "^5.1.7",
"node-diff3": "^3.2.0",
"nodemailer": "^8.0.2",
"svelte-dnd-action": "^0.9.69",
+10
View File
@@ -80,6 +80,9 @@ importers:
markdown-it:
specifier: ^14.1.1
version: 14.1.1
nanoid:
specifier: ^5.1.7
version: 5.1.7
node-diff3:
specifier: ^3.2.0
version: 3.2.0
@@ -2998,6 +3001,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@5.1.7:
resolution: {integrity: sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==}
engines: {node: ^18 || >=20}
hasBin: true
napi-build-utils@2.0.0:
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
@@ -6783,6 +6791,8 @@ snapshots:
nanoid@3.3.11: {}
nanoid@5.1.7: {}
napi-build-utils@2.0.0: {}
negotiator@1.0.0: {}
+4 -1
View File
@@ -6,6 +6,7 @@ import { extractTags } from '$lib/utils/tags.js';
import { fetchTagsForNotes, syncNoteTags } from './tags.js';
import { fetchAttachmentsForNotes } from './attachments.js';
import { fetchCollaboratorsForNotes } from './collaborators.js';
import { fetchSharesForNotes } from './shares-service.js';
import { canAccessNote } from './api-utils.js';
import { createSnapshot } from './versions-service.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 attachmentMap = fetchAttachmentsForNotes(db, noteIds);
const collabMap = fetchCollaboratorsForNotes(db, noteIds);
const shareMap = fetchSharesForNotes(db, noteIds);
return noteRows.map((note) => {
const collaborators = collabMap.get(note.id) ?? [];
@@ -44,7 +46,8 @@ function hydrateNotes(db: Db, noteRows: NoteRow[], userId: number) {
attachments: attachmentMap.get(note.id) ?? [],
collaborators,
isOwner,
isShared
isShared,
shareToken: shareMap.get(note.id) ?? undefined
};
});
}
+108
View File
@@ -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
};
}