ocean-brain 0.2.2 → 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.
Files changed (113) hide show
  1. package/dist/index.js +73 -41
  2. package/dist/mcp.js +587 -26
  3. package/package.json +6 -1
  4. package/scripts/postinstall-prisma.mjs +41 -0
  5. package/server/client/dist/assets/Calendar-DRv2aEl8.js +28 -0
  6. package/server/client/dist/assets/Callout-B2kPcDrq.js +1 -0
  7. package/server/client/dist/assets/Graph-DotbTrZR.js +1 -0
  8. package/server/client/dist/assets/Image-B5R7gF2P.js +1 -0
  9. package/server/client/dist/assets/Image.es-Dld9r9MJ.js +1 -0
  10. package/server/client/dist/assets/ModalActionRow-CES9L_Gy.js +1 -0
  11. package/server/client/dist/assets/Note-SK4hDhEM.css +1 -0
  12. package/server/client/dist/assets/Note-cIbXJc1v.js +1 -0
  13. package/server/client/dist/assets/Reminders-4LunlZRb.js +1 -0
  14. package/server/client/dist/assets/{Search-BANPp2ZV.js → Search-Cp17DGR6.js} +1 -1
  15. package/server/client/dist/assets/SurfaceCard-yzVU4Qkx.js +1 -0
  16. package/server/client/dist/assets/Tag-CwmuMPmo.js +1 -0
  17. package/server/client/dist/assets/TagNotes-gGroWMtT.js +1 -0
  18. package/server/client/dist/assets/Trash.es-B3KWD8_C.js +1 -0
  19. package/server/client/dist/assets/color-wpu7wT_s.js +1 -0
  20. package/server/client/dist/assets/{image.api-rBH0OSWJ.js → image.api-DJgOBt8S.js} +1 -1
  21. package/server/client/dist/assets/index-40Y2UsVr.js +205 -0
  22. package/server/client/dist/assets/index-BnxpTwtL.css +1 -0
  23. package/server/client/dist/assets/index-DYUg2N-6.js +1 -0
  24. package/server/client/dist/assets/manage-image-ByY1mRZx.js +1 -0
  25. package/server/client/dist/assets/manage-image-detail-BVaDZHRu.js +1 -0
  26. package/server/client/dist/assets/mcp-Jj3061aI.js +1 -0
  27. package/server/client/dist/assets/placeholder-4P_GK7fV.js +34 -0
  28. package/server/client/dist/assets/trash-CPBwY07S.js +1 -0
  29. package/server/client/dist/assets/{useReminderMutate-CU-UinGn.js → useReminderMutate-1YVndM2T.js} +1 -1
  30. package/server/client/dist/index.html +2 -2
  31. package/server/dist/app.js +97 -9
  32. package/server/dist/app.js.map +1 -1
  33. package/server/dist/main.js +16 -2
  34. package/server/dist/main.js.map +1 -1
  35. package/server/dist/modules/auth-guard.js +73 -0
  36. package/server/dist/modules/auth-guard.js.map +1 -0
  37. package/server/dist/modules/auth-mode.js +51 -0
  38. package/server/dist/modules/auth-mode.js.map +1 -0
  39. package/server/dist/modules/auth.js +9 -0
  40. package/server/dist/modules/auth.js.map +1 -1
  41. package/server/dist/modules/blocknote.js +7 -1
  42. package/server/dist/modules/blocknote.js.map +1 -1
  43. package/server/dist/modules/error-handler.js +41 -0
  44. package/server/dist/modules/error-handler.js.map +1 -0
  45. package/server/dist/modules/image-upload.js +114 -0
  46. package/server/dist/modules/image-upload.js.map +1 -0
  47. package/server/dist/modules/mcp-admin.js +88 -0
  48. package/server/dist/modules/mcp-admin.js.map +1 -0
  49. package/server/dist/modules/mcp-auth.js +71 -0
  50. package/server/dist/modules/mcp-auth.js.map +1 -0
  51. package/server/dist/modules/mcp-token.js +28 -0
  52. package/server/dist/modules/mcp-token.js.map +1 -0
  53. package/server/dist/modules/note-authoring.js +140 -0
  54. package/server/dist/modules/note-authoring.js.map +1 -0
  55. package/server/dist/modules/note-cleanup.js +207 -0
  56. package/server/dist/modules/note-cleanup.js.map +1 -0
  57. package/server/dist/modules/note-snapshot.js +223 -0
  58. package/server/dist/modules/note-snapshot.js.map +1 -0
  59. package/server/dist/modules/note-trash.js +262 -0
  60. package/server/dist/modules/note-trash.js.map +1 -0
  61. package/server/dist/modules/prisma-runtime.js +46 -0
  62. package/server/dist/modules/prisma-runtime.js.map +1 -0
  63. package/server/dist/modules/recovery-retention.js +26 -0
  64. package/server/dist/modules/recovery-retention.js.map +1 -0
  65. package/server/dist/modules/remote-image.js +176 -0
  66. package/server/dist/modules/remote-image.js.map +1 -0
  67. package/server/dist/modules/tag-organization.js +66 -0
  68. package/server/dist/modules/tag-organization.js.map +1 -0
  69. package/server/dist/modules/use-async.js +2 -4
  70. package/server/dist/modules/use-async.js.map +1 -1
  71. package/server/dist/schema/note/index.js +150 -6
  72. package/server/dist/schema/note/index.js.map +1 -1
  73. package/server/dist/start.js +11 -0
  74. package/server/dist/start.js.map +1 -0
  75. package/server/dist/types/index.js.map +1 -1
  76. package/server/dist/urls.js +3 -2
  77. package/server/dist/urls.js.map +1 -1
  78. package/server/dist/views/auth.js +305 -0
  79. package/server/dist/views/auth.js.map +1 -0
  80. package/server/dist/views/image.js +24 -81
  81. package/server/dist/views/image.js.map +1 -1
  82. package/server/dist/views/index.js +4 -0
  83. package/server/dist/views/index.js.map +1 -1
  84. package/server/dist/views/mcp-admin.js +42 -0
  85. package/server/dist/views/mcp-admin.js.map +1 -0
  86. package/server/dist/views/note.js +124 -0
  87. package/server/dist/views/note.js.map +1 -0
  88. package/server/dist/views/tag.js +23 -0
  89. package/server/dist/views/tag.js.map +1 -0
  90. package/server/prisma/migrations/20260331183000_0010_note_snapshot/migration.sql +13 -0
  91. package/server/prisma/migrations/20260331195000_0011_note_trash_entities/migration.sql +35 -0
  92. package/server/prisma/migrations/20260331213000_0012_image_hash_unique/migration.sql +49 -0
  93. package/server/prisma/migrations/20260404000000_0013_mcp_admin_control/migration.sql +9 -0
  94. package/server/prisma/schema.prisma +66 -1
  95. package/server/client/dist/assets/Calendar-ST5q_8KT.js +0 -48
  96. package/server/client/dist/assets/Callout-CrWW3bg0.js +0 -1
  97. package/server/client/dist/assets/Graph-CzJjt6rP.js +0 -1
  98. package/server/client/dist/assets/Image-Dxg1qqZw.js +0 -1
  99. package/server/client/dist/assets/Image.es-DZ0H6wH1.js +0 -1
  100. package/server/client/dist/assets/Note-Bu7fN3B6.js +0 -6
  101. package/server/client/dist/assets/Note-CCvXqPzv.css +0 -1
  102. package/server/client/dist/assets/Plus.es-CS3WVMDq.js +0 -1
  103. package/server/client/dist/assets/PriorityLegend-D61iBwNW.js +0 -1
  104. package/server/client/dist/assets/Reminders-BjirJMOR.css +0 -1
  105. package/server/client/dist/assets/Reminders-DjWRombU.js +0 -1
  106. package/server/client/dist/assets/Tag-DbKSLTj0.js +0 -1
  107. package/server/client/dist/assets/TagNotes-B6s7jtL7.js +0 -1
  108. package/server/client/dist/assets/index-BzWwEDoL.js +0 -1
  109. package/server/client/dist/assets/index-CF5_ZCYN.js +0 -162
  110. package/server/client/dist/assets/index-DjZk2Gpg.css +0 -1
  111. package/server/client/dist/assets/manage-image-detail-DOx6QsCl.js +0 -1
  112. package/server/client/dist/assets/manage-image-g-MC6Fkc.js +0 -1
  113. package/server/client/dist/assets/placeholder-DV3u6MrA.js +0 -34
@@ -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":[]}