694 lines
22 KiB
TypeScript
694 lines
22 KiB
TypeScript
import { get } from 'svelte/store';
|
|
import { db } from './schema';
|
|
import { newId, now, today } from './helpers';
|
|
import { getAccessToken } from '$lib/auth/authStore';
|
|
import { normalizeTitleAndBody } from '$lib/utils/titleUtils';
|
|
import { scopeSettings } from '$lib/stores/scopeContext';
|
|
import type { AgendaContext, Topic, HistoryEntry, Rating, ContextType, TopicStatus, ProjectMeta, PersonMeta, CompanyMeta, Page, Notebook, PageNotebook } from '@ka-note/shared';
|
|
|
|
// --- Contexts ---
|
|
|
|
export async function getAllContexts(): Promise<AgendaContext[]> {
|
|
return db.contexts.where('deletedAt').equals('').or('deletedAt').equals(null as any).sortBy('sortOrder');
|
|
}
|
|
|
|
export async function getContextsByType(type: ContextType): Promise<AgendaContext[]> {
|
|
const all = await getAllContexts();
|
|
return all.filter(c => c.type === type);
|
|
}
|
|
|
|
export async function getContext(id: string): Promise<AgendaContext | undefined> {
|
|
return db.contexts.get(id);
|
|
}
|
|
|
|
export async function upsertContext(ctx: Partial<AgendaContext> & { id: string }): Promise<void> {
|
|
const existing = await db.contexts.get(ctx.id);
|
|
if (existing) {
|
|
await db.contexts.update(ctx.id, { ...ctx, updatedAt: now(), version: existing.version + 1 });
|
|
} else {
|
|
await db.contexts.put({
|
|
name: '',
|
|
type: 'meeting',
|
|
sortOrder: 0,
|
|
meta: null,
|
|
archivedAt: null,
|
|
isFavorite: false,
|
|
deletedAt: null,
|
|
updatedAt: now(),
|
|
version: 1,
|
|
...ctx
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function softDeleteContext(id: string): Promise<void> {
|
|
const ctx = await db.contexts.get(id);
|
|
if (ctx) {
|
|
await db.contexts.update(id, { deletedAt: now(), updatedAt: now(), version: ctx.version + 1 });
|
|
}
|
|
}
|
|
|
|
export async function archiveContext(id: string): Promise<void> {
|
|
const ctx = await db.contexts.get(id);
|
|
if (ctx) {
|
|
await db.contexts.update(id, { archivedAt: now(), updatedAt: now(), version: ctx.version + 1 });
|
|
}
|
|
}
|
|
|
|
export async function unarchiveContext(id: string): Promise<void> {
|
|
const ctx = await db.contexts.get(id);
|
|
if (ctx) {
|
|
await db.contexts.update(id, { archivedAt: null, updatedAt: now(), version: ctx.version + 1 });
|
|
}
|
|
}
|
|
|
|
export async function toggleFavorite(id: string): Promise<void> {
|
|
const ctx = await db.contexts.get(id);
|
|
if (ctx) {
|
|
await db.contexts.update(id, { isFavorite: !ctx.isFavorite, updatedAt: now(), version: ctx.version + 1 });
|
|
}
|
|
}
|
|
|
|
export async function reorderContext(id: string, direction: 'up' | 'down'): Promise<void> {
|
|
const ctx = await db.contexts.get(id);
|
|
if (!ctx) return;
|
|
const siblings = await db.contexts
|
|
.filter(c => !c.deletedAt && c.type === ctx.type)
|
|
.sortBy('sortOrder');
|
|
const idx = siblings.findIndex(c => c.id === id);
|
|
const swapIdx = direction === 'up' ? idx - 1 : idx + 1;
|
|
if (swapIdx < 0 || swapIdx >= siblings.length) return;
|
|
// Swap positions in the array, then write clean sequential sortOrder values
|
|
[siblings[idx], siblings[swapIdx]] = [siblings[swapIdx], siblings[idx]];
|
|
const ts = now();
|
|
for (let i = 0; i < siblings.length; i++) {
|
|
const s = siblings[i];
|
|
await db.contexts.update(s.id, { sortOrder: i, updatedAt: ts, version: s.version + 1 });
|
|
}
|
|
}
|
|
|
|
export async function contextNameExists(name: string, type: ContextType): Promise<boolean> {
|
|
const q = name.toLowerCase();
|
|
const count = await db.contexts
|
|
.filter(c => !c.deletedAt && c.type === type && c.name.toLowerCase() === q)
|
|
.count();
|
|
return count > 0;
|
|
}
|
|
|
|
export async function pageNameExists(title: string): Promise<boolean> {
|
|
const q = title.toLowerCase();
|
|
const count = await db.pages.filter(p => !p.deletedAt && p.title.toLowerCase() === q).count();
|
|
return count > 0;
|
|
}
|
|
|
|
export async function notebookNameExists(name: string): Promise<boolean> {
|
|
const q = name.toLowerCase();
|
|
const count = await db.notebooks.filter(n => !n.deletedAt && n.name.toLowerCase() === q).count();
|
|
return count > 0;
|
|
}
|
|
|
|
export async function findContextByMentionName(name: string, type: 'person' | 'project' | 'company'): Promise<AgendaContext | undefined> {
|
|
const q = name.toLowerCase();
|
|
return db.contexts
|
|
.filter(c => !c.deletedAt && c.type === type && c.name.replace(/^(Person|Project|Firma)\s+/, '').toLowerCase() === q)
|
|
.first();
|
|
}
|
|
|
|
// --- Topics ---
|
|
|
|
export async function getTopicsByContext(contextId: string): Promise<Topic[]> {
|
|
return db.topics
|
|
.where('contextId').equals(contextId)
|
|
.filter(t => !t.deletedAt)
|
|
.sortBy('sortOrder');
|
|
}
|
|
|
|
export async function getTopic(id: string): Promise<Topic | undefined> {
|
|
return db.topics.get(id);
|
|
}
|
|
|
|
export async function createTopic(contextId: string, rawTitle: string, isPrivate = false): Promise<Topic> {
|
|
const existing = await getTopicsByContext(contextId);
|
|
const { maxTitleLength } = get(scopeSettings);
|
|
const { title, body } = normalizeTitleAndBody(rawTitle, '', maxTitleLength);
|
|
const topic: Topic = {
|
|
id: newId(),
|
|
contextId,
|
|
title,
|
|
status: 'active',
|
|
snoozeUntil: null,
|
|
sortOrder: 0,
|
|
isNew: true,
|
|
updatedAt: now(),
|
|
deletedAt: null,
|
|
version: 1
|
|
};
|
|
// Push existing topics down
|
|
for (const t of existing) {
|
|
await db.topics.update(t.id, { sortOrder: t.sortOrder + 1 });
|
|
}
|
|
await db.topics.put(topic);
|
|
if (body) {
|
|
await createHistoryEntry(topic.id, today(), body, null, false, isPrivate);
|
|
}
|
|
return topic;
|
|
}
|
|
|
|
export async function updateTopic(id: string, changes: Partial<Topic>): Promise<void> {
|
|
const topic = await db.topics.get(id);
|
|
if (topic) {
|
|
await db.topics.update(id, { ...changes, updatedAt: now(), version: topic.version + 1 });
|
|
}
|
|
}
|
|
|
|
export async function softDeleteTopic(id: string): Promise<void> {
|
|
const topic = await db.topics.get(id);
|
|
if (topic) {
|
|
await db.topics.update(id, { deletedAt: now(), status: 'done' as TopicStatus, updatedAt: now(), version: topic.version + 1 });
|
|
}
|
|
}
|
|
|
|
export async function archiveTopic(id: string): Promise<void> {
|
|
const topic = await db.topics.get(id);
|
|
if (topic) {
|
|
await db.topics.update(id, { status: 'archived' as TopicStatus, updatedAt: now(), version: topic.version + 1 });
|
|
}
|
|
}
|
|
|
|
export async function updateTopicSortOrder(contextId: string, orderedIds: string[]): Promise<void> {
|
|
await db.transaction('rw', db.topics, async () => {
|
|
for (let i = 0; i < orderedIds.length; i++) {
|
|
await db.topics.update(orderedIds[i], { sortOrder: i, updatedAt: now() });
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- Journal (daily-log) ---
|
|
|
|
const JOURNAL_TOPIC_ID = 'daily-log-journal';
|
|
|
|
export async function getOrCreateJournalTopic(): Promise<Topic> {
|
|
const existing = await db.topics.get(JOURNAL_TOPIC_ID);
|
|
if (existing && !existing.deletedAt) return existing;
|
|
const topic: Topic = {
|
|
id: JOURNAL_TOPIC_ID,
|
|
contextId: 'daily-log',
|
|
title: '__journal__',
|
|
status: 'active',
|
|
snoozeUntil: null,
|
|
sortOrder: -1,
|
|
isNew: false,
|
|
updatedAt: now(),
|
|
deletedAt: null,
|
|
version: 1
|
|
};
|
|
await db.topics.put(topic);
|
|
return topic;
|
|
}
|
|
|
|
export { JOURNAL_TOPIC_ID };
|
|
|
|
// --- Notes (project/person) ---
|
|
|
|
export function notesTopicId(contextId: string): string {
|
|
return `${contextId}-notes`;
|
|
}
|
|
|
|
export async function getOrCreateNotesTopic(contextId: string): Promise<Topic> {
|
|
const id = notesTopicId(contextId);
|
|
const existing = await db.topics.get(id);
|
|
if (existing && !existing.deletedAt) return existing;
|
|
const topic: Topic = {
|
|
id,
|
|
contextId,
|
|
title: '__notes__',
|
|
status: 'active',
|
|
snoozeUntil: null,
|
|
sortOrder: -1,
|
|
isNew: false,
|
|
updatedAt: now(),
|
|
deletedAt: null,
|
|
version: 1
|
|
};
|
|
await db.topics.put(topic);
|
|
return topic;
|
|
}
|
|
|
|
|
|
// --- History Entries ---
|
|
|
|
export async function getHistoryByTopic(topicId: string): Promise<HistoryEntry[]> {
|
|
return db.historyEntries
|
|
.where('topicId').equals(topicId)
|
|
.filter(h => !h.deletedAt)
|
|
.sortBy('sortOrder');
|
|
}
|
|
|
|
export async function getAllHistoryByContext(contextId: string): Promise<(HistoryEntry & { topicTitle: string })[]> {
|
|
const topics = await getTopicsByContext(contextId);
|
|
const topicMap = new Map(topics.map(t => [t.id, t]));
|
|
const allHistory: (HistoryEntry & { topicTitle: string })[] = [];
|
|
for (const topic of topics) {
|
|
const entries = await getHistoryByTopic(topic.id);
|
|
for (const entry of entries) {
|
|
allHistory.push({ ...entry, topicTitle: topic.title });
|
|
}
|
|
}
|
|
return allHistory;
|
|
}
|
|
|
|
export async function createHistoryEntry(topicId: string, date: string, text: string, linkedContextId: string | null = null, wiedervorlage = false, isPrivate = false): Promise<HistoryEntry> {
|
|
const existing = await getHistoryByTopic(topicId);
|
|
const autoWiedervorlage = date > today() || wiedervorlage;
|
|
const entry: HistoryEntry = {
|
|
id: newId(),
|
|
topicId,
|
|
date,
|
|
text,
|
|
sortOrder: Date.now(),
|
|
linkedContextId,
|
|
doneAt: null,
|
|
wiedervorlageDate: autoWiedervorlage ? date : null,
|
|
wiedervorlageResolvedAt: null,
|
|
updatedAt: now(),
|
|
deletedAt: null,
|
|
version: 1,
|
|
...(isPrivate ? { isPrivate: true } : {})
|
|
};
|
|
await db.historyEntries.put(entry);
|
|
return entry;
|
|
}
|
|
|
|
export async function setWiedervorlage(id: string, date: string): Promise<void> {
|
|
const entry = await db.historyEntries.get(id);
|
|
if (entry) {
|
|
await db.historyEntries.update(id, { wiedervorlageDate: date, wiedervorlageResolvedAt: null, updatedAt: now(), version: entry.version + 1 });
|
|
}
|
|
}
|
|
|
|
export async function resolveWiedervorlage(id: string): Promise<void> {
|
|
const entry = await db.historyEntries.get(id);
|
|
if (entry) {
|
|
await db.historyEntries.update(id, { wiedervorlageResolvedAt: now(), updatedAt: now(), version: entry.version + 1 });
|
|
}
|
|
}
|
|
|
|
export async function convertToTopic(entryId: string, contextId: string): Promise<Topic> {
|
|
const entry = await db.historyEntries.get(entryId);
|
|
if (!entry) throw new Error('Entry not found');
|
|
const lines = entry.text.split('\n');
|
|
const title = lines[0].trim();
|
|
const topic = await createTopic(contextId, title);
|
|
await resolveWiedervorlage(entryId);
|
|
return topic;
|
|
}
|
|
|
|
export async function updateHistoryEntry(id: string, text: string, isPrivate?: boolean, date?: string): Promise<void> {
|
|
const entry = await db.historyEntries.get(id);
|
|
if (entry) {
|
|
const patch: Record<string, unknown> = { text, updatedAt: now(), version: entry.version + 1 };
|
|
if (isPrivate !== undefined) patch.isPrivate = isPrivate;
|
|
if (date !== undefined) patch.date = date;
|
|
await db.historyEntries.update(id, patch);
|
|
}
|
|
}
|
|
|
|
export async function softDeleteHistoryEntry(id: string): Promise<void> {
|
|
const entry = await db.historyEntries.get(id);
|
|
if (entry) {
|
|
await db.historyEntries.update(id, { deletedAt: now(), updatedAt: now(), version: entry.version + 1 });
|
|
}
|
|
}
|
|
|
|
export async function toggleHistoryEntryDone(id: string): Promise<void> {
|
|
const entry = await db.historyEntries.get(id);
|
|
if (entry) {
|
|
const doneAt = entry.doneAt ? null : now();
|
|
await db.historyEntries.update(id, { doneAt, updatedAt: now(), version: entry.version + 1 });
|
|
}
|
|
}
|
|
|
|
// --- Ratings ---
|
|
|
|
export async function getRating(topicId: string, historyEntryId: string, personName: string): Promise<Rating | undefined> {
|
|
return db.ratings
|
|
.where('historyEntryId').equals(historyEntryId)
|
|
.filter(r => r.topicId === topicId && r.personName === personName && !r.deletedAt)
|
|
.first();
|
|
}
|
|
|
|
export async function getRatingsByHistoryEntry(historyEntryId: string): Promise<Rating[]> {
|
|
return db.ratings
|
|
.where('historyEntryId').equals(historyEntryId)
|
|
.filter(r => !r.deletedAt)
|
|
.toArray();
|
|
}
|
|
|
|
export async function upsertRating(topicId: string, historyEntryId: string, personName: string, value: 1 | 2 | 3 | 4, comment: string | null = null): Promise<void> {
|
|
const existing = await db.ratings
|
|
.where('historyEntryId').equals(historyEntryId)
|
|
.filter(r => r.topicId === topicId && r.personName === personName && !r.deletedAt)
|
|
.first();
|
|
if (existing) {
|
|
await db.ratings.update(existing.id, { value, comment, updatedAt: now(), version: existing.version + 1 });
|
|
} else {
|
|
await db.ratings.put({
|
|
id: newId(),
|
|
topicId,
|
|
historyEntryId,
|
|
personName,
|
|
value,
|
|
comment,
|
|
updatedAt: now(),
|
|
deletedAt: null,
|
|
version: 1
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function getRatingsByPerson(personName: string): Promise<Rating[]> {
|
|
return db.ratings
|
|
.where('personName').equals(personName)
|
|
.filter(r => !r.deletedAt)
|
|
.toArray();
|
|
}
|
|
|
|
export async function softDeleteRating(id: string): Promise<void> {
|
|
const rating = await db.ratings.get(id);
|
|
if (rating) {
|
|
await db.ratings.update(id, { deletedAt: now(), updatedAt: now(), version: rating.version + 1 });
|
|
}
|
|
}
|
|
|
|
// --- Wiki Pages ---
|
|
|
|
export async function getPage(id: string): Promise<Page | undefined> {
|
|
return db.pages.get(id);
|
|
}
|
|
|
|
export async function getAllPages(): Promise<Page[]> {
|
|
return db.pages.filter(p => !p.deletedAt).sortBy('sortOrder');
|
|
}
|
|
|
|
export async function createPage(title: string, isPrivate = false): Promise<Page> {
|
|
const all = await db.pages.toArray();
|
|
const page: Page = {
|
|
id: newId(),
|
|
title,
|
|
body: '',
|
|
isPrivate,
|
|
sortOrder: all.length,
|
|
updatedAt: now(),
|
|
deletedAt: null,
|
|
purgedAt: null,
|
|
version: 1,
|
|
};
|
|
await db.pages.put(page);
|
|
return page;
|
|
}
|
|
|
|
export async function upsertPage(page: Partial<Page> & { id: string }): Promise<void> {
|
|
const existing = await db.pages.get(page.id);
|
|
if (existing) {
|
|
const oldTitle = existing.title;
|
|
await db.pages.update(page.id, { ...page, updatedAt: now(), version: existing.version + 1 });
|
|
if (page.title && page.title !== oldTitle) {
|
|
await renameTitleInLinks(oldTitle, page.title);
|
|
}
|
|
} else {
|
|
await db.pages.put({
|
|
title: '',
|
|
body: '',
|
|
isPrivate: false,
|
|
sortOrder: 0,
|
|
deletedAt: null,
|
|
purgedAt: null,
|
|
updatedAt: now(),
|
|
version: 1,
|
|
...page,
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function togglePageFavorite(id: string): Promise<void> {
|
|
const p = await db.pages.get(id);
|
|
if (p) {
|
|
await db.pages.update(id, { isFavorite: !p.isFavorite, updatedAt: now(), version: p.version + 1 });
|
|
}
|
|
}
|
|
|
|
export async function softDeletePage(id: string): Promise<void> {
|
|
const page = await db.pages.get(id);
|
|
if (page) {
|
|
await db.pages.update(id, { deletedAt: now(), updatedAt: now(), version: page.version + 1 });
|
|
// soft-delete all join entries
|
|
const joins = await db.pageNotebooks.where('pageId').equals(id).filter(pn => !pn.deletedAt).toArray();
|
|
for (const pn of joins) {
|
|
await db.pageNotebooks.update(pn.id, { deletedAt: now(), updatedAt: now(), version: pn.version + 1 });
|
|
}
|
|
}
|
|
}
|
|
|
|
async function renameTitleInLinks(oldTitle: string, newTitle: string): Promise<void> {
|
|
const pattern = `[[${oldTitle}]]`;
|
|
const replacement = `[[${newTitle}]]`;
|
|
const pages = await db.pages.filter(p => !p.deletedAt && p.body.includes(pattern)).toArray();
|
|
const ts = now();
|
|
for (const p of pages) {
|
|
await db.pages.update(p.id, {
|
|
body: p.body.split(pattern).join(replacement),
|
|
updatedAt: ts,
|
|
version: p.version + 1,
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function getBacklinksForPage(pageId: string): Promise<Page[]> {
|
|
const page = await db.pages.get(pageId);
|
|
if (!page) return [];
|
|
const pattern = `[[${page.title}]]`;
|
|
return db.pages.filter(p => !p.deletedAt && p.id !== pageId && p.body.includes(pattern)).toArray();
|
|
}
|
|
|
|
// --- Notebooks ---
|
|
|
|
export async function getNotebook(id: string): Promise<Notebook | undefined> {
|
|
return db.notebooks.get(id);
|
|
}
|
|
|
|
export async function getAllNotebooks(): Promise<Notebook[]> {
|
|
return db.notebooks.filter(n => !n.deletedAt).sortBy('sortOrder');
|
|
}
|
|
|
|
export async function upsertNotebook(notebook: Partial<Notebook> & { id: string }): Promise<void> {
|
|
const existing = await db.notebooks.get(notebook.id);
|
|
if (existing) {
|
|
await db.notebooks.update(notebook.id, { ...notebook, updatedAt: now(), version: existing.version + 1 });
|
|
} else {
|
|
await db.notebooks.put({
|
|
name: '',
|
|
contextId: null,
|
|
isPrivate: false,
|
|
isFavorite: false,
|
|
sortOrder: 0,
|
|
deletedAt: null,
|
|
purgedAt: null,
|
|
updatedAt: now(),
|
|
version: 1,
|
|
...notebook,
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function createNotebook(name: string, contextId: string | null = null, isPrivate = false): Promise<Notebook> {
|
|
const all = await db.notebooks.toArray();
|
|
const notebook: Notebook = {
|
|
id: newId(),
|
|
name,
|
|
contextId,
|
|
isPrivate,
|
|
isFavorite: false,
|
|
sortOrder: all.length,
|
|
updatedAt: now(),
|
|
deletedAt: null,
|
|
purgedAt: null,
|
|
version: 1,
|
|
};
|
|
await db.notebooks.put(notebook);
|
|
return notebook;
|
|
}
|
|
|
|
export async function toggleNotebookFavorite(id: string): Promise<void> {
|
|
const nb = await db.notebooks.get(id);
|
|
if (nb) {
|
|
await db.notebooks.update(id, { isFavorite: !nb.isFavorite, updatedAt: now(), version: nb.version + 1 });
|
|
}
|
|
}
|
|
|
|
export async function reorderNotebook(id: string, direction: 'up' | 'down'): Promise<void> {
|
|
const nb = await db.notebooks.get(id);
|
|
if (!nb) return;
|
|
const siblings = await db.notebooks
|
|
.filter(n => !n.deletedAt)
|
|
.sortBy('sortOrder');
|
|
const idx = siblings.findIndex(n => n.id === id);
|
|
const swapIdx = direction === 'up' ? idx - 1 : idx + 1;
|
|
if (swapIdx < 0 || swapIdx >= siblings.length) return;
|
|
[siblings[idx], siblings[swapIdx]] = [siblings[swapIdx], siblings[idx]];
|
|
const ts = now();
|
|
for (let i = 0; i < siblings.length; i++) {
|
|
const s = siblings[i];
|
|
await db.notebooks.update(s.id, { sortOrder: i, updatedAt: ts, version: s.version + 1 });
|
|
}
|
|
}
|
|
|
|
export async function softDeleteNotebook(id: string): Promise<void> {
|
|
const nb = await db.notebooks.get(id);
|
|
if (nb) {
|
|
await db.notebooks.update(id, { deletedAt: now(), updatedAt: now(), version: nb.version + 1 });
|
|
}
|
|
}
|
|
|
|
// --- PageNotebook join ---
|
|
|
|
export async function assignPageToNotebook(pageId: string, notebookId: string): Promise<void> {
|
|
const existing = await db.pageNotebooks
|
|
.filter(pn => pn.pageId === pageId && pn.notebookId === notebookId && !pn.deletedAt)
|
|
.first();
|
|
if (existing) return;
|
|
const joins = await db.pageNotebooks.where('notebookId').equals(notebookId).filter(pn => !pn.deletedAt).toArray();
|
|
const pn: PageNotebook = {
|
|
id: newId(),
|
|
pageId,
|
|
notebookId,
|
|
sortOrder: joins.length,
|
|
updatedAt: now(),
|
|
deletedAt: null,
|
|
purgedAt: null,
|
|
version: 1,
|
|
};
|
|
await db.pageNotebooks.put(pn);
|
|
}
|
|
|
|
export async function removePageFromNotebook(pageId: string, notebookId: string): Promise<void> {
|
|
const existing = await db.pageNotebooks
|
|
.filter(pn => pn.pageId === pageId && pn.notebookId === notebookId && !pn.deletedAt)
|
|
.first();
|
|
if (existing) {
|
|
await db.pageNotebooks.update(existing.id, { deletedAt: now(), updatedAt: now(), version: existing.version + 1 });
|
|
}
|
|
}
|
|
|
|
export async function getNotebooksForPage(pageId: string): Promise<Notebook[]> {
|
|
const joins = await db.pageNotebooks.where('pageId').equals(pageId).filter(pn => !pn.deletedAt).toArray();
|
|
const ids = joins.map(pn => pn.notebookId);
|
|
if (ids.length === 0) return [];
|
|
const nbs = await db.notebooks.bulkGet(ids);
|
|
return nbs.filter((n): n is Notebook => !!n && !n.deletedAt);
|
|
}
|
|
|
|
export async function getPagesForNotebook(notebookId: string): Promise<Page[]> {
|
|
const joins = await db.pageNotebooks
|
|
.where('notebookId').equals(notebookId)
|
|
.filter(pn => !pn.deletedAt)
|
|
.sortBy('sortOrder');
|
|
const ids = joins.map(pn => pn.pageId);
|
|
if (ids.length === 0) return [];
|
|
const ps = await db.pages.bulkGet(ids);
|
|
return ps.filter((p): p is Page => !!p && !p.deletedAt);
|
|
}
|
|
|
|
export async function getUnassignedPages(): Promise<Page[]> {
|
|
const allPages = await getAllPages();
|
|
const result: Page[] = [];
|
|
for (const p of allPages) {
|
|
const joined = await db.pageNotebooks
|
|
.where('pageId').equals(p.id)
|
|
.filter(pn => !pn.deletedAt)
|
|
.count();
|
|
if (joined === 0) result.push(p);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export async function updatePageNotebookSortOrder(notebookId: string, orderedPageIds: string[]): Promise<void> {
|
|
await db.transaction('rw', db.pageNotebooks, async () => {
|
|
for (let i = 0; i < orderedPageIds.length; i++) {
|
|
const join = await db.pageNotebooks
|
|
.filter(pn => pn.pageId === orderedPageIds[i] && pn.notebookId === notebookId && !pn.deletedAt)
|
|
.first();
|
|
if (join) {
|
|
await db.pageNotebooks.update(join.id, { sortOrder: i, updatedAt: now(), version: join.version + 1 });
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- Trash ---
|
|
|
|
export type TrashEntityType = 'context' | 'topic' | 'history' | 'rating';
|
|
|
|
export interface TrashEntityRef {
|
|
type: TrashEntityType;
|
|
id: string;
|
|
}
|
|
|
|
export async function restoreEntities(entities: TrashEntityRef[]): Promise<void> {
|
|
const timestamp = now();
|
|
for (const { type, id } of entities) {
|
|
if (type === 'context') {
|
|
const item = await db.contexts.get(id);
|
|
if (item) await db.contexts.update(id, { deletedAt: null, updatedAt: timestamp, version: item.version + 1 });
|
|
} else if (type === 'topic') {
|
|
const item = await db.topics.get(id);
|
|
if (item) await db.topics.update(id, { deletedAt: null, updatedAt: timestamp, version: item.version + 1 });
|
|
} else if (type === 'history') {
|
|
const item = await db.historyEntries.get(id);
|
|
if (item) await db.historyEntries.update(id, { deletedAt: null, updatedAt: timestamp, version: item.version + 1 });
|
|
} else if (type === 'rating') {
|
|
const item = await db.ratings.get(id);
|
|
if (item) await db.ratings.update(id, { deletedAt: null, updatedAt: timestamp, version: item.version + 1 });
|
|
}
|
|
}
|
|
}
|
|
|
|
async function resolveLocalCascade(entities: TrashEntityRef[]) {
|
|
const contextIds = new Set(entities.filter(e => e.type === 'context').map(e => e.id));
|
|
const topicIds = new Set(entities.filter(e => e.type === 'topic').map(e => e.id));
|
|
const historyIds = new Set(entities.filter(e => e.type === 'history').map(e => e.id));
|
|
const ratingIds = new Set(entities.filter(e => e.type === 'rating').map(e => e.id));
|
|
|
|
if (contextIds.size > 0) {
|
|
const childTopics = await db.topics.where('contextId').anyOf([...contextIds]).toArray();
|
|
for (const t of childTopics) topicIds.add(t.id);
|
|
}
|
|
if (topicIds.size > 0) {
|
|
const childHistory = await db.historyEntries.where('topicId').anyOf([...topicIds]).toArray();
|
|
for (const h of childHistory) historyIds.add(h.id);
|
|
}
|
|
if (historyIds.size > 0) {
|
|
const childRatings = await db.ratings.where('historyEntryId').anyOf([...historyIds]).toArray();
|
|
for (const r of childRatings) ratingIds.add(r.id);
|
|
}
|
|
|
|
return { contextIds, topicIds, historyIds, ratingIds };
|
|
}
|
|
|
|
export async function hardDeleteEntities(entities: TrashEntityRef[]): Promise<void> {
|
|
const token = await getAccessToken();
|
|
const res = await fetch('/api/trash', {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
|
body: JSON.stringify({ entities }),
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text().catch(() => '');
|
|
throw new Error(`Server error ${res.status}: ${body}`);
|
|
}
|
|
const { contextIds, topicIds, historyIds, ratingIds } = await resolveLocalCascade(entities);
|
|
await db.ratings.bulkDelete([...ratingIds]);
|
|
await db.historyEntries.bulkDelete([...historyIds]);
|
|
await db.topics.bulkDelete([...topicIds]);
|
|
await db.contexts.bulkDelete([...contextIds]);
|
|
}
|