ocean-brain 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +73 -41
- package/dist/mcp.js +587 -26
- package/package.json +6 -1
- package/scripts/postinstall-prisma.mjs +41 -0
- package/server/client/dist/assets/Calendar-DRv2aEl8.js +28 -0
- package/server/client/dist/assets/Callout-B2kPcDrq.js +1 -0
- package/server/client/dist/assets/Graph-DotbTrZR.js +1 -0
- package/server/client/dist/assets/Image-B5R7gF2P.js +1 -0
- package/server/client/dist/assets/Image.es-Dld9r9MJ.js +1 -0
- package/server/client/dist/assets/ModalActionRow-CES9L_Gy.js +1 -0
- package/server/client/dist/assets/Note-SK4hDhEM.css +1 -0
- package/server/client/dist/assets/Note-cIbXJc1v.js +1 -0
- package/server/client/dist/assets/Reminders-4LunlZRb.js +1 -0
- package/server/client/dist/assets/Search-Cp17DGR6.js +1 -0
- package/server/client/dist/assets/SurfaceCard-yzVU4Qkx.js +1 -0
- package/server/client/dist/assets/Tag-CwmuMPmo.js +1 -0
- package/server/client/dist/assets/TagNotes-gGroWMtT.js +1 -0
- package/server/client/dist/assets/Trash.es-B3KWD8_C.js +1 -0
- package/server/client/dist/assets/color-wpu7wT_s.js +1 -0
- package/server/client/dist/assets/graph-vendor-CUxe67Lr.js +46 -0
- package/server/client/dist/assets/image.api-DJgOBt8S.js +17 -0
- package/server/client/dist/assets/index-40Y2UsVr.js +205 -0
- package/server/client/dist/assets/index-BnxpTwtL.css +1 -0
- package/server/client/dist/assets/index-DYUg2N-6.js +1 -0
- package/server/client/dist/assets/manage-image-ByY1mRZx.js +1 -0
- package/server/client/dist/assets/manage-image-detail-BVaDZHRu.js +1 -0
- package/server/client/dist/assets/mcp-Jj3061aI.js +1 -0
- package/server/client/dist/assets/{note-core-DTIu0anW.js → note-core-BCgMq5QA.js} +7 -7
- package/server/client/dist/assets/note-vendor-BofYbzmZ.js +54 -0
- package/server/client/dist/assets/placeholder-4P_GK7fV.js +34 -0
- package/server/client/dist/assets/trash-CPBwY07S.js +1 -0
- package/server/client/dist/assets/useReminderMutate-1YVndM2T.js +82 -0
- package/server/client/dist/index.html +5 -4
- package/server/dist/app.js +97 -9
- package/server/dist/app.js.map +1 -1
- package/server/dist/main.js +16 -2
- package/server/dist/main.js.map +1 -1
- package/server/dist/modules/auth-guard.js +73 -0
- package/server/dist/modules/auth-guard.js.map +1 -0
- package/server/dist/modules/auth-mode.js +51 -0
- package/server/dist/modules/auth-mode.js.map +1 -0
- package/server/dist/modules/auth.js +9 -0
- package/server/dist/modules/auth.js.map +1 -1
- package/server/dist/modules/blocknote.js +7 -1
- package/server/dist/modules/blocknote.js.map +1 -1
- package/server/dist/modules/error-handler.js +41 -0
- package/server/dist/modules/error-handler.js.map +1 -0
- package/server/dist/modules/image-upload.js +114 -0
- package/server/dist/modules/image-upload.js.map +1 -0
- package/server/dist/modules/mcp-admin.js +88 -0
- package/server/dist/modules/mcp-admin.js.map +1 -0
- package/server/dist/modules/mcp-auth.js +71 -0
- package/server/dist/modules/mcp-auth.js.map +1 -0
- package/server/dist/modules/mcp-token.js +28 -0
- package/server/dist/modules/mcp-token.js.map +1 -0
- package/server/dist/modules/note-authoring.js +140 -0
- package/server/dist/modules/note-authoring.js.map +1 -0
- package/server/dist/modules/note-cleanup.js +207 -0
- package/server/dist/modules/note-cleanup.js.map +1 -0
- package/server/dist/modules/note-snapshot.js +223 -0
- package/server/dist/modules/note-snapshot.js.map +1 -0
- package/server/dist/modules/note-trash.js +262 -0
- package/server/dist/modules/note-trash.js.map +1 -0
- package/server/dist/modules/prisma-runtime.js +46 -0
- package/server/dist/modules/prisma-runtime.js.map +1 -0
- package/server/dist/modules/recovery-retention.js +26 -0
- package/server/dist/modules/recovery-retention.js.map +1 -0
- package/server/dist/modules/remote-image.js +176 -0
- package/server/dist/modules/remote-image.js.map +1 -0
- package/server/dist/modules/tag-organization.js +66 -0
- package/server/dist/modules/tag-organization.js.map +1 -0
- package/server/dist/modules/use-async.js +2 -4
- package/server/dist/modules/use-async.js.map +1 -1
- package/server/dist/schema/note/index.js +150 -6
- package/server/dist/schema/note/index.js.map +1 -1
- package/server/dist/start.js +11 -0
- package/server/dist/start.js.map +1 -0
- package/server/dist/types/index.js.map +1 -1
- package/server/dist/urls.js +3 -2
- package/server/dist/urls.js.map +1 -1
- package/server/dist/views/auth.js +305 -0
- package/server/dist/views/auth.js.map +1 -0
- package/server/dist/views/image.js +24 -81
- package/server/dist/views/image.js.map +1 -1
- package/server/dist/views/index.js +4 -0
- package/server/dist/views/index.js.map +1 -1
- package/server/dist/views/mcp-admin.js +42 -0
- package/server/dist/views/mcp-admin.js.map +1 -0
- package/server/dist/views/note.js +124 -0
- package/server/dist/views/note.js.map +1 -0
- package/server/dist/views/tag.js +23 -0
- package/server/dist/views/tag.js.map +1 -0
- package/server/prisma/migrations/20260331183000_0010_note_snapshot/migration.sql +13 -0
- package/server/prisma/migrations/20260331195000_0011_note_trash_entities/migration.sql +35 -0
- package/server/prisma/migrations/20260331213000_0012_image_hash_unique/migration.sql +49 -0
- package/server/prisma/migrations/20260404000000_0013_mcp_admin_control/migration.sql +9 -0
- package/server/prisma/schema.prisma +66 -1
- package/server/client/dist/assets/index-BJgBFWwL.css +0 -1
- package/server/client/dist/assets/index-DvNRv9YC.js +0 -391
- package/server/client/dist/assets/note-vendor-CUVHa9LU.js +0 -54
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import models from "../models.js";
|
|
2
|
+
import { trashNoteById as moveNoteToTrashById } from "./note-trash.js";
|
|
3
|
+
const DEFAULT_CLEANUP_KEYWORDS = ["temp", "temporary", "tmp", "draft", "test", "wip"];
|
|
4
|
+
const normalizeCleanupKeywords = (keywords, query) => {
|
|
5
|
+
const queryKeywords = typeof query === "string" ? query.split(/[,\s]+/).map((keyword) => keyword.trim()) : [];
|
|
6
|
+
const source = keywords && keywords.length > 0 ? keywords : queryKeywords.length > 0 ? queryKeywords : DEFAULT_CLEANUP_KEYWORDS;
|
|
7
|
+
return Array.from(new Set(
|
|
8
|
+
source.map((keyword) => keyword.trim().toLowerCase()).filter(Boolean)
|
|
9
|
+
));
|
|
10
|
+
};
|
|
11
|
+
const buildForceReasons = (input) => {
|
|
12
|
+
const forceReasons = [];
|
|
13
|
+
if (input.pinned) {
|
|
14
|
+
forceReasons.push("note_is_pinned");
|
|
15
|
+
}
|
|
16
|
+
if (input.reminderCount > 0) {
|
|
17
|
+
forceReasons.push("has_reminders");
|
|
18
|
+
}
|
|
19
|
+
if (input.backReferenceCount > 0) {
|
|
20
|
+
forceReasons.push("has_back_references");
|
|
21
|
+
}
|
|
22
|
+
return forceReasons;
|
|
23
|
+
};
|
|
24
|
+
const createNoteCleanupService = (deps) => {
|
|
25
|
+
const buildCandidate = async (note, matchedTerms) => {
|
|
26
|
+
const [reminderCount, backReferences] = await Promise.all([
|
|
27
|
+
deps.countReminders(note.id),
|
|
28
|
+
deps.findBackReferences(note.id)
|
|
29
|
+
]);
|
|
30
|
+
const normalizedMatchedTerms = matchedTerms ?? [];
|
|
31
|
+
const forceReasons = buildForceReasons({
|
|
32
|
+
pinned: note.pinned,
|
|
33
|
+
reminderCount,
|
|
34
|
+
backReferenceCount: backReferences.length
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
id: String(note.id),
|
|
38
|
+
title: note.title,
|
|
39
|
+
updatedAt: note.updatedAt.toISOString(),
|
|
40
|
+
pinned: note.pinned,
|
|
41
|
+
tagNames: note.tags.map((tag) => tag.name),
|
|
42
|
+
reminderCount,
|
|
43
|
+
backReferenceCount: backReferences.length,
|
|
44
|
+
matchedTerms: normalizedMatchedTerms,
|
|
45
|
+
reasons: [
|
|
46
|
+
...normalizedMatchedTerms.length > 0 ? [`matched_terms:${normalizedMatchedTerms.join(",")}`] : [],
|
|
47
|
+
...note.pinned ? [] : ["not_pinned"],
|
|
48
|
+
...note.tags.length === 0 ? ["tagless"] : [],
|
|
49
|
+
...reminderCount === 0 ? ["no_reminders"] : [],
|
|
50
|
+
...backReferences.length === 0 ? ["no_back_references"] : []
|
|
51
|
+
],
|
|
52
|
+
requiresForce: forceReasons.length > 0,
|
|
53
|
+
forceReasons
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
const getDeletePreviewInternal = async (id) => {
|
|
57
|
+
const note = await deps.findNote(id);
|
|
58
|
+
if (!note) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const [candidate, backReferences, tagNoteCounts] = await Promise.all([
|
|
62
|
+
buildCandidate(note),
|
|
63
|
+
deps.findBackReferences(note.id),
|
|
64
|
+
deps.getTagNoteCounts(note.tags.map((tag) => tag.id))
|
|
65
|
+
]);
|
|
66
|
+
const orphanedTags = note.tags.filter((tag) => (tagNoteCounts.get(tag.id) ?? 0) <= 1);
|
|
67
|
+
return {
|
|
68
|
+
id: candidate.id,
|
|
69
|
+
title: candidate.title,
|
|
70
|
+
updatedAt: candidate.updatedAt,
|
|
71
|
+
pinned: candidate.pinned,
|
|
72
|
+
tagNames: candidate.tagNames,
|
|
73
|
+
reminderCount: candidate.reminderCount,
|
|
74
|
+
backReferences: backReferences.map((backReference) => ({
|
|
75
|
+
id: String(backReference.id),
|
|
76
|
+
title: backReference.title
|
|
77
|
+
})),
|
|
78
|
+
orphanedTagNames: orphanedTags.map((tag) => tag.name),
|
|
79
|
+
orphanedTagIds: orphanedTags.map((tag) => tag.id),
|
|
80
|
+
requiresForce: candidate.requiresForce || orphanedTags.length > 0,
|
|
81
|
+
forceReasons: [
|
|
82
|
+
...candidate.forceReasons,
|
|
83
|
+
...orphanedTags.length > 0 ? ["orphan_tags"] : []
|
|
84
|
+
]
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
const getDeletePreview = async (id) => {
|
|
88
|
+
const preview = await getDeletePreviewInternal(id);
|
|
89
|
+
if (!preview) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const { orphanedTagIds, ...publicPreview } = preview;
|
|
93
|
+
void orphanedTagIds;
|
|
94
|
+
return publicPreview;
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
listCleanupCandidates: async (input = {}) => {
|
|
98
|
+
const limit = Math.max(1, Number(input.limit ?? 20));
|
|
99
|
+
const offset = Math.max(0, Number(input.offset ?? 0));
|
|
100
|
+
const olderThanDays = Math.max(0, Number(input.olderThanDays ?? 0));
|
|
101
|
+
const keywords = normalizeCleanupKeywords(input.keywords, input.query);
|
|
102
|
+
const [totalCount, notes] = await Promise.all([
|
|
103
|
+
deps.countCandidateNotes(keywords, olderThanDays),
|
|
104
|
+
deps.findCandidateNotes(keywords, offset, limit, olderThanDays)
|
|
105
|
+
]);
|
|
106
|
+
const candidates = await Promise.all(notes.map((note) => {
|
|
107
|
+
const haystack = `${note.title}
|
|
108
|
+
${note.content}`.toLowerCase();
|
|
109
|
+
const matchedTerms = keywords.filter((keyword) => haystack.includes(keyword));
|
|
110
|
+
return buildCandidate(note, matchedTerms);
|
|
111
|
+
}));
|
|
112
|
+
return {
|
|
113
|
+
keywords,
|
|
114
|
+
limit,
|
|
115
|
+
offset,
|
|
116
|
+
olderThanDays,
|
|
117
|
+
notes: candidates,
|
|
118
|
+
totalCount
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
getDeletePreview,
|
|
122
|
+
deleteNoteById: async (id) => {
|
|
123
|
+
const preview = await getDeletePreviewInternal(id);
|
|
124
|
+
if (!preview) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
await deps.deleteNoteAndPruneTags(id, preview.orphanedTagIds);
|
|
128
|
+
const { orphanedTagIds, ...publicPreview } = preview;
|
|
129
|
+
void orphanedTagIds;
|
|
130
|
+
return publicPreview;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
const buildKeywordClauses = (keywords) => {
|
|
135
|
+
return keywords.flatMap((keyword) => [
|
|
136
|
+
{ title: { contains: keyword } },
|
|
137
|
+
{ content: { contains: keyword } }
|
|
138
|
+
]);
|
|
139
|
+
};
|
|
140
|
+
const buildStaleWhere = (olderThanDays) => {
|
|
141
|
+
if (olderThanDays <= 0) {
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
return { updatedAt: { lte: new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1e3) } };
|
|
145
|
+
};
|
|
146
|
+
const noteCleanupService = createNoteCleanupService({
|
|
147
|
+
countCandidateNotes: (keywords, olderThanDays) => models.note.count({
|
|
148
|
+
where: {
|
|
149
|
+
...buildStaleWhere(olderThanDays),
|
|
150
|
+
OR: buildKeywordClauses(keywords)
|
|
151
|
+
}
|
|
152
|
+
}),
|
|
153
|
+
countReminders: (noteId) => models.reminder.count({ where: { noteId } }),
|
|
154
|
+
deleteNoteAndPruneTags: async (noteId, orphanTagIds) => {
|
|
155
|
+
void orphanTagIds;
|
|
156
|
+
await moveNoteToTrashById(noteId);
|
|
157
|
+
},
|
|
158
|
+
findBackReferences: (noteId) => models.note.findMany({
|
|
159
|
+
select: {
|
|
160
|
+
id: true,
|
|
161
|
+
title: true
|
|
162
|
+
},
|
|
163
|
+
where: {
|
|
164
|
+
content: { contains: `reference","props":{"id":"${noteId}"` },
|
|
165
|
+
NOT: { id: noteId }
|
|
166
|
+
},
|
|
167
|
+
orderBy: [
|
|
168
|
+
{ pinned: "desc" },
|
|
169
|
+
{ updatedAt: "desc" }
|
|
170
|
+
]
|
|
171
|
+
}),
|
|
172
|
+
findCandidateNotes: (keywords, skip, take, olderThanDays) => models.note.findMany({
|
|
173
|
+
include: { tags: true },
|
|
174
|
+
orderBy: { updatedAt: "asc" },
|
|
175
|
+
skip,
|
|
176
|
+
take,
|
|
177
|
+
where: {
|
|
178
|
+
...buildStaleWhere(olderThanDays),
|
|
179
|
+
OR: buildKeywordClauses(keywords)
|
|
180
|
+
}
|
|
181
|
+
}),
|
|
182
|
+
findNote: (id) => models.note.findUnique({
|
|
183
|
+
where: { id },
|
|
184
|
+
include: { tags: true }
|
|
185
|
+
}),
|
|
186
|
+
getTagNoteCounts: async (tagIds) => {
|
|
187
|
+
if (tagIds.length === 0) {
|
|
188
|
+
return /* @__PURE__ */ new Map();
|
|
189
|
+
}
|
|
190
|
+
const tags = await models.tag.findMany({
|
|
191
|
+
where: { id: { in: tagIds } },
|
|
192
|
+
include: { _count: { select: { notes: true } } }
|
|
193
|
+
});
|
|
194
|
+
return new Map(tags.map((tag) => [tag.id, tag._count.notes]));
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
const listNoteCleanupCandidates = noteCleanupService.listCleanupCandidates;
|
|
198
|
+
const getNoteCleanupPreview = noteCleanupService.getDeletePreview;
|
|
199
|
+
const deleteNoteById = noteCleanupService.deleteNoteById;
|
|
200
|
+
export {
|
|
201
|
+
createNoteCleanupService,
|
|
202
|
+
deleteNoteById,
|
|
203
|
+
getNoteCleanupPreview,
|
|
204
|
+
listNoteCleanupCandidates,
|
|
205
|
+
normalizeCleanupKeywords
|
|
206
|
+
};
|
|
207
|
+
//# sourceMappingURL=note-cleanup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/modules/note-cleanup.ts"],"sourcesContent":["import models from '~/models.js';\nimport { trashNoteById as moveNoteToTrashById } from './note-trash.js';\n\nexport interface NoteCleanupBackReference {\n id: string;\n title: string;\n}\n\nexport interface NoteCleanupPreview {\n id: string;\n title: string;\n updatedAt: string;\n pinned: boolean;\n tagNames: string[];\n reminderCount: number;\n backReferences: NoteCleanupBackReference[];\n orphanedTagNames: string[];\n requiresForce: boolean;\n forceReasons: string[];\n}\n\nexport interface NoteCleanupCandidate {\n id: string;\n title: string;\n updatedAt: string;\n pinned: boolean;\n tagNames: string[];\n reminderCount: number;\n backReferenceCount: number;\n matchedTerms: string[];\n reasons: string[];\n requiresForce: boolean;\n forceReasons: string[];\n}\n\nexport interface NoteCleanupCandidatesResult {\n keywords: string[];\n limit: number;\n offset: number;\n olderThanDays: number;\n notes: NoteCleanupCandidate[];\n totalCount: number;\n}\n\nexport interface NoteCleanupService {\n deleteNoteById: (id: number) => Promise<NoteCleanupPreview | null>;\n getDeletePreview: (id: number) => Promise<NoteCleanupPreview | null>;\n listCleanupCandidates: (input?: {\n keywords?: string[];\n limit?: number;\n offset?: number;\n olderThanDays?: number;\n query?: string;\n }) => Promise<NoteCleanupCandidatesResult>;\n}\n\ninterface NoteRecord {\n id: number;\n title: string;\n content: string;\n updatedAt: Date;\n pinned: boolean;\n tags: Array<{\n id: number;\n name: string;\n }>;\n}\n\ninterface NoteCleanupPreviewInternal extends NoteCleanupPreview {\n orphanedTagIds: number[];\n}\n\nconst DEFAULT_CLEANUP_KEYWORDS = ['temp', 'temporary', 'tmp', 'draft', 'test', 'wip'];\n\nexport const normalizeCleanupKeywords = (keywords?: string[], query?: string) => {\n const queryKeywords = typeof query === 'string'\n ? query.split(/[,\\s]+/).map((keyword) => keyword.trim())\n : [];\n const source = (keywords && keywords.length > 0)\n ? keywords\n : (queryKeywords.length > 0 ? queryKeywords : DEFAULT_CLEANUP_KEYWORDS);\n\n return Array.from(new Set(\n source\n .map((keyword) => keyword.trim().toLowerCase())\n .filter(Boolean)\n ));\n};\n\nconst buildForceReasons = (input: {\n pinned: boolean;\n reminderCount: number;\n backReferenceCount: number;\n}) => {\n const forceReasons: string[] = [];\n\n if (input.pinned) {\n forceReasons.push('note_is_pinned');\n }\n\n if (input.reminderCount > 0) {\n forceReasons.push('has_reminders');\n }\n\n if (input.backReferenceCount > 0) {\n forceReasons.push('has_back_references');\n }\n\n return forceReasons;\n};\n\nexport const createNoteCleanupService = (deps: {\n countCandidateNotes: (keywords: string[], olderThanDays: number) => Promise<number>;\n countReminders: (noteId: number) => Promise<number>;\n deleteNoteAndPruneTags: (noteId: number, orphanTagIds: number[]) => Promise<void>;\n findBackReferences: (noteId: number) => Promise<Array<{ id: number; title: string }>>;\n findCandidateNotes: (keywords: string[], skip: number, take: number, olderThanDays: number) => Promise<NoteRecord[]>;\n findNote: (id: number) => Promise<NoteRecord | null>;\n getTagNoteCounts: (tagIds: number[]) => Promise<Map<number, number>>;\n}): NoteCleanupService => {\n const buildCandidate = async (\n note: NoteRecord,\n matchedTerms?: string[]\n ): Promise<NoteCleanupCandidate> => {\n const [reminderCount, backReferences] = await Promise.all([\n deps.countReminders(note.id),\n deps.findBackReferences(note.id)\n ]);\n const normalizedMatchedTerms = matchedTerms ?? [];\n const forceReasons = buildForceReasons({\n pinned: note.pinned,\n reminderCount,\n backReferenceCount: backReferences.length\n });\n\n return {\n id: String(note.id),\n title: note.title,\n updatedAt: note.updatedAt.toISOString(),\n pinned: note.pinned,\n tagNames: note.tags.map((tag) => tag.name),\n reminderCount,\n backReferenceCount: backReferences.length,\n matchedTerms: normalizedMatchedTerms,\n reasons: [\n ...(normalizedMatchedTerms.length > 0 ? [`matched_terms:${normalizedMatchedTerms.join(',')}`] : []),\n ...(note.pinned ? [] : ['not_pinned']),\n ...(note.tags.length === 0 ? ['tagless'] : []),\n ...(reminderCount === 0 ? ['no_reminders'] : []),\n ...(backReferences.length === 0 ? ['no_back_references'] : [])\n ],\n requiresForce: forceReasons.length > 0,\n forceReasons\n };\n };\n\n const getDeletePreviewInternal = async (id: number): Promise<NoteCleanupPreviewInternal | null> => {\n const note = await deps.findNote(id);\n\n if (!note) {\n return null;\n }\n\n const [candidate, backReferences, tagNoteCounts] = await Promise.all([\n buildCandidate(note),\n deps.findBackReferences(note.id),\n deps.getTagNoteCounts(note.tags.map((tag) => tag.id))\n ]);\n\n const orphanedTags = note.tags.filter((tag) => (tagNoteCounts.get(tag.id) ?? 0) <= 1);\n\n return {\n id: candidate.id,\n title: candidate.title,\n updatedAt: candidate.updatedAt,\n pinned: candidate.pinned,\n tagNames: candidate.tagNames,\n reminderCount: candidate.reminderCount,\n backReferences: backReferences.map((backReference) => ({\n id: String(backReference.id),\n title: backReference.title\n })),\n orphanedTagNames: orphanedTags.map((tag) => tag.name),\n orphanedTagIds: orphanedTags.map((tag) => tag.id),\n requiresForce: candidate.requiresForce || orphanedTags.length > 0,\n forceReasons: [\n ...candidate.forceReasons,\n ...(orphanedTags.length > 0 ? ['orphan_tags'] : [])\n ]\n };\n };\n\n const getDeletePreview = async (id: number): Promise<NoteCleanupPreview | null> => {\n const preview = await getDeletePreviewInternal(id);\n\n if (!preview) {\n return null;\n }\n\n const { orphanedTagIds, ...publicPreview } = preview;\n void orphanedTagIds;\n return publicPreview;\n };\n\n return {\n listCleanupCandidates: async (input = {}) => {\n const limit = Math.max(1, Number(input.limit ?? 20));\n const offset = Math.max(0, Number(input.offset ?? 0));\n const olderThanDays = Math.max(0, Number(input.olderThanDays ?? 0));\n const keywords = normalizeCleanupKeywords(input.keywords, input.query);\n\n const [totalCount, notes] = await Promise.all([\n deps.countCandidateNotes(keywords, olderThanDays),\n deps.findCandidateNotes(keywords, offset, limit, olderThanDays)\n ]);\n\n const candidates = await Promise.all(notes.map((note) => {\n const haystack = `${note.title}\\n${note.content}`.toLowerCase();\n const matchedTerms = keywords.filter((keyword) => haystack.includes(keyword));\n return buildCandidate(note, matchedTerms);\n }));\n\n return {\n keywords,\n limit,\n offset,\n olderThanDays,\n notes: candidates,\n totalCount\n };\n },\n getDeletePreview,\n deleteNoteById: async (id: number) => {\n const preview = await getDeletePreviewInternal(id);\n\n if (!preview) {\n return null;\n }\n\n await deps.deleteNoteAndPruneTags(id, preview.orphanedTagIds);\n\n const { orphanedTagIds, ...publicPreview } = preview;\n void orphanedTagIds;\n return publicPreview;\n }\n };\n};\n\nconst buildKeywordClauses = (keywords: string[]) => {\n return keywords.flatMap((keyword) => ([\n { title: { contains: keyword } },\n { content: { contains: keyword } }\n ]));\n};\n\nconst buildStaleWhere = (olderThanDays: number) => {\n if (olderThanDays <= 0) {\n return {};\n }\n\n return { updatedAt: { lte: new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000) } };\n};\n\nconst noteCleanupService = createNoteCleanupService({\n countCandidateNotes: (keywords: string[], olderThanDays: number) => models.note.count({\n where: {\n ...buildStaleWhere(olderThanDays),\n OR: buildKeywordClauses(keywords)\n }\n }),\n countReminders: (noteId: number) => models.reminder.count({ where: { noteId } }),\n deleteNoteAndPruneTags: async (noteId: number, orphanTagIds: number[]) => {\n void orphanTagIds;\n await moveNoteToTrashById(noteId);\n },\n findBackReferences: (noteId: number) => models.note.findMany({\n select: {\n id: true,\n title: true\n },\n where: {\n content: { contains: `reference\",\"props\":{\"id\":\"${noteId}\"` },\n NOT: { id: noteId }\n },\n orderBy: [\n { pinned: 'desc' },\n { updatedAt: 'desc' }\n ]\n }),\n findCandidateNotes: (keywords: string[], skip: number, take: number, olderThanDays: number) => models.note.findMany({\n include: { tags: true },\n orderBy: { updatedAt: 'asc' },\n skip,\n take,\n where: {\n ...buildStaleWhere(olderThanDays),\n OR: buildKeywordClauses(keywords)\n }\n }),\n findNote: (id: number) => models.note.findUnique({\n where: { id },\n include: { tags: true }\n }),\n getTagNoteCounts: async (tagIds: number[]) => {\n if (tagIds.length === 0) {\n return new Map<number, number>();\n }\n\n const tags = await models.tag.findMany({\n where: { id: { in: tagIds } },\n include: { _count: { select: { notes: true } } }\n });\n\n return new Map(tags.map((tag) => [tag.id, tag._count.notes]));\n }\n});\n\nexport const listNoteCleanupCandidates = noteCleanupService.listCleanupCandidates;\nexport const getNoteCleanupPreview = noteCleanupService.getDeletePreview;\nexport const deleteNoteById = noteCleanupService.deleteNoteById;\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import models from "../models.js";
|
|
2
|
+
import {
|
|
3
|
+
createRetentionCutoff,
|
|
4
|
+
RECOVERY_CLEANUP_BATCH_LIMIT,
|
|
5
|
+
SNAPSHOT_MAX_PER_NOTE,
|
|
6
|
+
SNAPSHOT_RETENTION_DAYS
|
|
7
|
+
} from "./recovery-retention.js";
|
|
8
|
+
const parseMeta = (value) => {
|
|
9
|
+
if (!value) {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(value);
|
|
14
|
+
return {
|
|
15
|
+
...parsed.entrypoint ? { entrypoint: parsed.entrypoint } : {},
|
|
16
|
+
...parsed.label ? { label: parsed.label } : {}
|
|
17
|
+
};
|
|
18
|
+
} catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const serializeSnapshot = (snapshot) => ({
|
|
23
|
+
id: String(snapshot.id),
|
|
24
|
+
title: snapshot.title,
|
|
25
|
+
createdAt: snapshot.createdAt.toISOString(),
|
|
26
|
+
meta: parseMeta(snapshot.meta)
|
|
27
|
+
});
|
|
28
|
+
const serializePayload = (note) => {
|
|
29
|
+
const payload = {
|
|
30
|
+
title: note.title,
|
|
31
|
+
content: note.content,
|
|
32
|
+
pinned: note.pinned,
|
|
33
|
+
order: note.order,
|
|
34
|
+
layout: note.layout
|
|
35
|
+
};
|
|
36
|
+
return JSON.stringify(payload);
|
|
37
|
+
};
|
|
38
|
+
const parsePayload = (payload) => {
|
|
39
|
+
const parsed = JSON.parse(payload);
|
|
40
|
+
if (typeof parsed.title !== "string" || typeof parsed.content !== "string" || typeof parsed.pinned !== "boolean" || typeof parsed.order !== "number" || parsed.layout !== "narrow" && parsed.layout !== "wide" && parsed.layout !== "full") {
|
|
41
|
+
throw new Error("INVALID_NOTE_SNAPSHOT_PAYLOAD");
|
|
42
|
+
}
|
|
43
|
+
return parsed;
|
|
44
|
+
};
|
|
45
|
+
const createSnapshotMetaFromUserAgent = (userAgent) => {
|
|
46
|
+
const normalized = userAgent?.toLowerCase() ?? "";
|
|
47
|
+
const isMobile = /iphone|ipad|android|mobile/.test(normalized);
|
|
48
|
+
return JSON.stringify({
|
|
49
|
+
entrypoint: isMobile ? "mobile" : "web",
|
|
50
|
+
label: isMobile ? "Mobile browser" : "Web browser"
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
const MCP_SNAPSHOT_META = JSON.stringify({
|
|
54
|
+
entrypoint: "mcp",
|
|
55
|
+
label: "MCP"
|
|
56
|
+
});
|
|
57
|
+
const defaultPurgeExpiredSnapshots = async (before, limit) => {
|
|
58
|
+
const expiredSnapshots = await models.noteSnapshot.findMany({
|
|
59
|
+
where: { createdAt: { lt: before } },
|
|
60
|
+
orderBy: { createdAt: "asc" },
|
|
61
|
+
take: limit,
|
|
62
|
+
select: { id: true }
|
|
63
|
+
});
|
|
64
|
+
if (expiredSnapshots.length === 0) {
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
const deleted = await models.noteSnapshot.deleteMany({ where: { id: { in: expiredSnapshots.map((snapshot) => snapshot.id) } } });
|
|
68
|
+
return deleted.count;
|
|
69
|
+
};
|
|
70
|
+
const defaultTrimOverflowSnapshots = async (noteId, keep, limit) => {
|
|
71
|
+
const overflowSnapshots = await models.noteSnapshot.findMany({
|
|
72
|
+
where: { noteId },
|
|
73
|
+
orderBy: [
|
|
74
|
+
{ createdAt: "desc" },
|
|
75
|
+
{ id: "desc" }
|
|
76
|
+
],
|
|
77
|
+
skip: keep,
|
|
78
|
+
take: limit,
|
|
79
|
+
select: { id: true }
|
|
80
|
+
});
|
|
81
|
+
if (overflowSnapshots.length === 0) {
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
const deleted = await models.noteSnapshot.deleteMany({ where: { id: { in: overflowSnapshots.map((snapshot) => snapshot.id) } } });
|
|
85
|
+
return deleted.count;
|
|
86
|
+
};
|
|
87
|
+
const createNoteSnapshotService = (deps) => ({
|
|
88
|
+
captureBaseline: async (input) => {
|
|
89
|
+
await deps.purgeExpiredSnapshots(
|
|
90
|
+
createRetentionCutoff(SNAPSHOT_RETENTION_DAYS),
|
|
91
|
+
RECOVERY_CLEANUP_BATCH_LIMIT
|
|
92
|
+
);
|
|
93
|
+
const existing = input.editSessionId ? await deps.findSnapshotByEditSessionId(input.noteId, input.editSessionId) : null;
|
|
94
|
+
if (existing) {
|
|
95
|
+
return serializeSnapshot(existing);
|
|
96
|
+
}
|
|
97
|
+
const note = await deps.findNoteById(input.noteId);
|
|
98
|
+
if (!note) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const snapshot = await deps.createSnapshot({
|
|
102
|
+
noteId: note.id,
|
|
103
|
+
title: note.title,
|
|
104
|
+
payload: serializePayload(note),
|
|
105
|
+
...input.editSessionId ? { editSessionId: input.editSessionId } : {},
|
|
106
|
+
...input.meta ? { meta: input.meta } : {}
|
|
107
|
+
});
|
|
108
|
+
await deps.trimOverflowSnapshots(
|
|
109
|
+
note.id,
|
|
110
|
+
SNAPSHOT_MAX_PER_NOTE,
|
|
111
|
+
RECOVERY_CLEANUP_BATCH_LIMIT
|
|
112
|
+
);
|
|
113
|
+
return serializeSnapshot(snapshot);
|
|
114
|
+
},
|
|
115
|
+
listSnapshots: async (noteId, limit = 5) => {
|
|
116
|
+
await deps.purgeExpiredSnapshots(
|
|
117
|
+
createRetentionCutoff(SNAPSHOT_RETENTION_DAYS),
|
|
118
|
+
RECOVERY_CLEANUP_BATCH_LIMIT
|
|
119
|
+
);
|
|
120
|
+
await deps.trimOverflowSnapshots(
|
|
121
|
+
noteId,
|
|
122
|
+
SNAPSHOT_MAX_PER_NOTE,
|
|
123
|
+
RECOVERY_CLEANUP_BATCH_LIMIT
|
|
124
|
+
);
|
|
125
|
+
const snapshots = await deps.listSnapshots(noteId, limit);
|
|
126
|
+
return snapshots.map(serializeSnapshot);
|
|
127
|
+
},
|
|
128
|
+
restoreSnapshot: async (snapshotId, options) => {
|
|
129
|
+
await deps.purgeExpiredSnapshots(
|
|
130
|
+
createRetentionCutoff(SNAPSHOT_RETENTION_DAYS),
|
|
131
|
+
RECOVERY_CLEANUP_BATCH_LIMIT
|
|
132
|
+
);
|
|
133
|
+
const snapshot = await deps.findSnapshotById(snapshotId);
|
|
134
|
+
if (!snapshot) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const note = await deps.findNoteById(snapshot.noteId);
|
|
138
|
+
if (!note) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
await deps.createSnapshot({
|
|
142
|
+
noteId: note.id,
|
|
143
|
+
title: note.title,
|
|
144
|
+
payload: serializePayload(note),
|
|
145
|
+
...options?.meta ? { meta: options.meta } : {}
|
|
146
|
+
});
|
|
147
|
+
await deps.trimOverflowSnapshots(
|
|
148
|
+
note.id,
|
|
149
|
+
SNAPSHOT_MAX_PER_NOTE,
|
|
150
|
+
RECOVERY_CLEANUP_BATCH_LIMIT
|
|
151
|
+
);
|
|
152
|
+
const payload = parsePayload(snapshot.payload);
|
|
153
|
+
return deps.updateNote(snapshot.noteId, payload);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
const defaultNoteSnapshotService = createNoteSnapshotService({
|
|
157
|
+
findNoteById: async (id) => {
|
|
158
|
+
return models.note.findUnique({ where: { id } });
|
|
159
|
+
},
|
|
160
|
+
findSnapshotByEditSessionId: async (noteId, editSessionId) => {
|
|
161
|
+
return models.noteSnapshot.findFirst({
|
|
162
|
+
where: {
|
|
163
|
+
noteId,
|
|
164
|
+
editSessionId
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
createSnapshot: async (input) => {
|
|
169
|
+
return models.noteSnapshot.create({
|
|
170
|
+
data: {
|
|
171
|
+
noteId: input.noteId,
|
|
172
|
+
title: input.title,
|
|
173
|
+
payload: input.payload,
|
|
174
|
+
...input.editSessionId ? { editSessionId: input.editSessionId } : {},
|
|
175
|
+
...input.meta ? { meta: input.meta } : {}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
purgeExpiredSnapshots: defaultPurgeExpiredSnapshots,
|
|
180
|
+
trimOverflowSnapshots: defaultTrimOverflowSnapshots,
|
|
181
|
+
listSnapshots: async (noteId, limit) => {
|
|
182
|
+
return models.noteSnapshot.findMany({
|
|
183
|
+
where: { noteId },
|
|
184
|
+
orderBy: { createdAt: "desc" },
|
|
185
|
+
take: limit
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
findSnapshotById: async (id) => {
|
|
189
|
+
return models.noteSnapshot.findUnique({ where: { id } });
|
|
190
|
+
},
|
|
191
|
+
updateNote: async (id, input) => {
|
|
192
|
+
return models.note.update({
|
|
193
|
+
where: { id },
|
|
194
|
+
data: input
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
const captureNoteBaseline = async (input) => {
|
|
199
|
+
return defaultNoteSnapshotService.captureBaseline(input);
|
|
200
|
+
};
|
|
201
|
+
const listNoteSnapshots = async (noteId, limit) => {
|
|
202
|
+
return defaultNoteSnapshotService.listSnapshots(noteId, limit);
|
|
203
|
+
};
|
|
204
|
+
const restoreNoteSnapshot = async (snapshotId, options) => {
|
|
205
|
+
return defaultNoteSnapshotService.restoreSnapshot(snapshotId, options);
|
|
206
|
+
};
|
|
207
|
+
const purgeExpiredNoteSnapshots = async () => {
|
|
208
|
+
return defaultPurgeExpiredSnapshots(
|
|
209
|
+
createRetentionCutoff(SNAPSHOT_RETENTION_DAYS),
|
|
210
|
+
RECOVERY_CLEANUP_BATCH_LIMIT
|
|
211
|
+
);
|
|
212
|
+
};
|
|
213
|
+
export {
|
|
214
|
+
MCP_SNAPSHOT_META,
|
|
215
|
+
captureNoteBaseline,
|
|
216
|
+
createNoteSnapshotService,
|
|
217
|
+
createSnapshotMetaFromUserAgent,
|
|
218
|
+
defaultNoteSnapshotService,
|
|
219
|
+
listNoteSnapshots,
|
|
220
|
+
purgeExpiredNoteSnapshots,
|
|
221
|
+
restoreNoteSnapshot
|
|
222
|
+
};
|
|
223
|
+
//# sourceMappingURL=note-snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/modules/note-snapshot.ts"],"sourcesContent":["import models, { type NoteLayout } from '~/models.js';\nimport {\n createRetentionCutoff,\n RECOVERY_CLEANUP_BATCH_LIMIT,\n SNAPSHOT_MAX_PER_NOTE,\n SNAPSHOT_RETENTION_DAYS\n} from './recovery-retention.js';\n\ninterface NoteRecord {\n id: number;\n title: string;\n content: string;\n pinned: boolean;\n order: number;\n layout: NoteLayout;\n updatedAt?: Date;\n}\n\ninterface NoteSnapshotRecord {\n id: number;\n noteId: number;\n title: string;\n payload: string;\n editSessionId: string | null;\n meta: string | null;\n createdAt: Date;\n}\n\ninterface NoteSnapshotDeps {\n findNoteById: (id: number) => Promise<NoteRecord | null>;\n findSnapshotByEditSessionId: (noteId: number, editSessionId: string) => Promise<NoteSnapshotRecord | null>;\n createSnapshot: (input: {\n noteId: number;\n title: string;\n payload: string;\n editSessionId?: string;\n meta?: string;\n }) => Promise<NoteSnapshotRecord>;\n listSnapshots: (noteId: number, limit: number) => Promise<NoteSnapshotRecord[]>;\n findSnapshotById: (id: number) => Promise<NoteSnapshotRecord | null>;\n purgeExpiredSnapshots: (before: Date, limit: number) => Promise<number>;\n trimOverflowSnapshots: (noteId: number, keep: number, limit: number) => Promise<number>;\n updateNote: (id: number, input: {\n title: string;\n content: string;\n pinned: boolean;\n order: number;\n layout: NoteLayout;\n }) => Promise<NoteRecord>;\n}\n\nexport interface NoteSnapshotMeta {\n entrypoint?: 'web' | 'mobile' | 'mcp';\n label?: string;\n}\n\ninterface NoteSnapshotPayload {\n title: string;\n content: string;\n pinned: boolean;\n order: number;\n layout: NoteLayout;\n}\n\nexport interface NoteSnapshotSummary {\n id: string;\n title: string;\n createdAt: string;\n meta: NoteSnapshotMeta;\n}\n\nconst parseMeta = (value?: string | null): NoteSnapshotMeta => {\n if (!value) {\n return {};\n }\n\n try {\n const parsed = JSON.parse(value) as NoteSnapshotMeta;\n return {\n ...(parsed.entrypoint ? { entrypoint: parsed.entrypoint } : {}),\n ...(parsed.label ? { label: parsed.label } : {})\n };\n } catch {\n return {};\n }\n};\n\nconst serializeSnapshot = (snapshot: NoteSnapshotRecord): NoteSnapshotSummary => ({\n id: String(snapshot.id),\n title: snapshot.title,\n createdAt: snapshot.createdAt.toISOString(),\n meta: parseMeta(snapshot.meta)\n});\n\nconst serializePayload = (note: NoteRecord): string => {\n const payload: NoteSnapshotPayload = {\n title: note.title,\n content: note.content,\n pinned: note.pinned,\n order: note.order,\n layout: note.layout\n };\n\n return JSON.stringify(payload);\n};\n\nconst parsePayload = (payload: string): NoteSnapshotPayload => {\n const parsed = JSON.parse(payload) as Partial<NoteSnapshotPayload>;\n\n if (\n typeof parsed.title !== 'string' ||\n typeof parsed.content !== 'string' ||\n typeof parsed.pinned !== 'boolean' ||\n typeof parsed.order !== 'number' ||\n (parsed.layout !== 'narrow' && parsed.layout !== 'wide' && parsed.layout !== 'full')\n ) {\n throw new Error('INVALID_NOTE_SNAPSHOT_PAYLOAD');\n }\n\n return parsed as NoteSnapshotPayload;\n};\n\nexport const createSnapshotMetaFromUserAgent = (userAgent?: string | null): string | undefined => {\n const normalized = userAgent?.toLowerCase() ?? '';\n const isMobile = /iphone|ipad|android|mobile/.test(normalized);\n\n return JSON.stringify({\n entrypoint: isMobile ? 'mobile' : 'web',\n label: isMobile ? 'Mobile browser' : 'Web browser'\n } satisfies NoteSnapshotMeta);\n};\n\nexport const MCP_SNAPSHOT_META = JSON.stringify({\n entrypoint: 'mcp',\n label: 'MCP'\n} satisfies NoteSnapshotMeta);\n\nconst defaultPurgeExpiredSnapshots = async (before: Date, limit: number) => {\n const expiredSnapshots = await models.noteSnapshot.findMany({\n where: { createdAt: { lt: before } },\n orderBy: { createdAt: 'asc' },\n take: limit,\n select: { id: true }\n });\n\n if (expiredSnapshots.length === 0) {\n return 0;\n }\n\n const deleted = await models.noteSnapshot.deleteMany({ where: { id: { in: expiredSnapshots.map((snapshot) => snapshot.id) } } });\n\n return deleted.count;\n};\n\nconst defaultTrimOverflowSnapshots = async (noteId: number, keep: number, limit: number) => {\n const overflowSnapshots = await models.noteSnapshot.findMany({\n where: { noteId },\n orderBy: [\n { createdAt: 'desc' },\n { id: 'desc' }\n ],\n skip: keep,\n take: limit,\n select: { id: true }\n });\n\n if (overflowSnapshots.length === 0) {\n return 0;\n }\n\n const deleted = await models.noteSnapshot.deleteMany({ where: { id: { in: overflowSnapshots.map((snapshot) => snapshot.id) } } });\n\n return deleted.count;\n};\n\nexport const createNoteSnapshotService = (deps: NoteSnapshotDeps) => ({\n captureBaseline: async (input: {\n noteId: number;\n editSessionId?: string;\n meta?: string;\n }) => {\n await deps.purgeExpiredSnapshots(\n createRetentionCutoff(SNAPSHOT_RETENTION_DAYS),\n RECOVERY_CLEANUP_BATCH_LIMIT\n );\n\n const existing = input.editSessionId\n ? await deps.findSnapshotByEditSessionId(input.noteId, input.editSessionId)\n : null;\n\n if (existing) {\n return serializeSnapshot(existing);\n }\n\n const note = await deps.findNoteById(input.noteId);\n\n if (!note) {\n return null;\n }\n\n const snapshot = await deps.createSnapshot({\n noteId: note.id,\n title: note.title,\n payload: serializePayload(note),\n ...(input.editSessionId ? { editSessionId: input.editSessionId } : {}),\n ...(input.meta ? { meta: input.meta } : {})\n });\n\n await deps.trimOverflowSnapshots(\n note.id,\n SNAPSHOT_MAX_PER_NOTE,\n RECOVERY_CLEANUP_BATCH_LIMIT\n );\n\n return serializeSnapshot(snapshot);\n },\n\n listSnapshots: async (noteId: number, limit = 5) => {\n await deps.purgeExpiredSnapshots(\n createRetentionCutoff(SNAPSHOT_RETENTION_DAYS),\n RECOVERY_CLEANUP_BATCH_LIMIT\n );\n await deps.trimOverflowSnapshots(\n noteId,\n SNAPSHOT_MAX_PER_NOTE,\n RECOVERY_CLEANUP_BATCH_LIMIT\n );\n const snapshots = await deps.listSnapshots(noteId, limit);\n return snapshots.map(serializeSnapshot);\n },\n\n restoreSnapshot: async (snapshotId: number, options?: { meta?: string }) => {\n await deps.purgeExpiredSnapshots(\n createRetentionCutoff(SNAPSHOT_RETENTION_DAYS),\n RECOVERY_CLEANUP_BATCH_LIMIT\n );\n\n const snapshot = await deps.findSnapshotById(snapshotId);\n\n if (!snapshot) {\n return null;\n }\n\n const note = await deps.findNoteById(snapshot.noteId);\n\n if (!note) {\n return null;\n }\n\n await deps.createSnapshot({\n noteId: note.id,\n title: note.title,\n payload: serializePayload(note),\n ...(options?.meta ? { meta: options.meta } : {})\n });\n\n await deps.trimOverflowSnapshots(\n note.id,\n SNAPSHOT_MAX_PER_NOTE,\n RECOVERY_CLEANUP_BATCH_LIMIT\n );\n\n const payload = parsePayload(snapshot.payload);\n\n return deps.updateNote(snapshot.noteId, payload);\n }\n});\n\nexport const defaultNoteSnapshotService = createNoteSnapshotService({\n findNoteById: async (id) => {\n return models.note.findUnique({ where: { id } });\n },\n findSnapshotByEditSessionId: async (noteId, editSessionId) => {\n return models.noteSnapshot.findFirst({\n where: {\n noteId,\n editSessionId\n }\n });\n },\n createSnapshot: async (input) => {\n return models.noteSnapshot.create({\n data: {\n noteId: input.noteId,\n title: input.title,\n payload: input.payload,\n ...(input.editSessionId ? { editSessionId: input.editSessionId } : {}),\n ...(input.meta ? { meta: input.meta } : {})\n }\n });\n },\n purgeExpiredSnapshots: defaultPurgeExpiredSnapshots,\n trimOverflowSnapshots: defaultTrimOverflowSnapshots,\n listSnapshots: async (noteId, limit) => {\n return models.noteSnapshot.findMany({\n where: { noteId },\n orderBy: { createdAt: 'desc' },\n take: limit\n });\n },\n findSnapshotById: async (id) => {\n return models.noteSnapshot.findUnique({ where: { id } });\n },\n updateNote: async (id, input) => {\n return models.note.update({\n where: { id },\n data: input\n });\n }\n});\n\nexport const captureNoteBaseline = async (input: {\n noteId: number;\n editSessionId?: string;\n meta?: string;\n}) => {\n return defaultNoteSnapshotService.captureBaseline(input);\n};\n\nexport const listNoteSnapshots = async (noteId: number, limit?: number) => {\n return defaultNoteSnapshotService.listSnapshots(noteId, limit);\n};\n\nexport const restoreNoteSnapshot = async (snapshotId: number, options?: { meta?: string }) => {\n return defaultNoteSnapshotService.restoreSnapshot(snapshotId, options);\n};\n\nexport const purgeExpiredNoteSnapshots = async () => {\n return defaultPurgeExpiredSnapshots(\n createRetentionCutoff(SNAPSHOT_RETENTION_DAYS),\n RECOVERY_CLEANUP_BATCH_LIMIT\n );\n};\n"],"mappings":"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","names":[]}
|