ocean-brain 0.4.2 → 0.5.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/package.json +1 -1
- package/server/client/dist/assets/{Calendar-DI0PhQTq.js → Calendar-BU9XgX2N.js} +1 -1
- package/server/client/dist/assets/Callout-C6tVCxOT.js +1 -0
- package/server/client/dist/assets/{Graph-Dhuog4tc.js → Graph-Dond2hGK.js} +1 -1
- package/server/client/dist/assets/{Image-DlJ4MDo3.js → Image-DxWhlIDG.js} +1 -1
- package/server/client/dist/assets/{Image.es-COwqkQuh.js → Image.es-BjGK7y6Y.js} +1 -1
- package/server/client/dist/assets/Note-Cr6GtXUM.js +21 -0
- package/server/client/dist/assets/{Plus.es-KeV96R_e.js → Plus.es-EcmtS_uw.js} +1 -1
- package/server/client/dist/assets/{Reminders-D7MCTNxb.js → Reminders-BlLLT_x0.js} +1 -1
- package/server/client/dist/assets/{Search-B-fwTr5y.js → Search-CrZ0ap8x.js} +1 -1
- package/server/client/dist/assets/{SurfaceCard-CHjcCuqZ.js → SurfaceCard-CO0oVxeL.js} +1 -1
- package/server/client/dist/assets/{Tag-BbV2z-Db.js → Tag-vws5AfNW.js} +1 -1
- package/server/client/dist/assets/{TagNotes-2HKF09Jm.js → TagNotes-Fmf1Kgim.js} +1 -1
- package/server/client/dist/assets/{Trash.es-BqRTZPrI.js → Trash.es-DmwfSy3l.js} +1 -1
- package/server/client/dist/assets/{ViewNotes-TfM3Uzbp.js → ViewNotes-BALN7IfH.js} +1 -1
- package/server/client/dist/assets/{Views-CUwtnfa8.js → Views-Ccc_Ml5U.js} +1 -1
- package/server/client/dist/assets/{image.api-CMdQbCyN.js → image.api-DeF9ck7g.js} +1 -1
- package/server/client/dist/assets/{index-zhxZjg7V.js → index-BSVE2cGd.js} +1 -1
- package/server/client/dist/assets/{index-D2gRB1ig.css → index-BZUGWdOU.css} +1 -1
- package/server/client/dist/assets/{index-B4l2UvuC.js → index-BvKBTjQq.js} +51 -37
- package/server/client/dist/assets/{manage-image-DdXb5Bkj.js → manage-image-IoTXaMBP.js} +1 -1
- package/server/client/dist/assets/{manage-image-detail-CrrXCQIX.js → manage-image-detail-BRTXwOsh.js} +1 -1
- package/server/client/dist/assets/{mcp-DqGLZv90.js → mcp-Bh743HBz.js} +1 -1
- package/server/client/dist/assets/note-core-Z6kqulGB.js +180 -0
- package/server/client/dist/assets/note-vendor-i1M2FSI2.js +54 -0
- package/server/client/dist/assets/{placeholder-BXLtvdtN.js → placeholder-XQqo1Fvr.js} +3 -3
- package/server/client/dist/assets/{trash-3eUAjFRh.js → trash-hiJIg6-c.js} +1 -1
- package/server/client/dist/assets/{useReminderMutate-v1EXqM90.js → useReminderMutate-D21PurIS.js} +1 -1
- package/server/client/dist/assets/{view-dashboard-T6gmohr-.js → view-dashboard-BuaZ4MvI.js} +1 -1
- package/server/client/dist/index.html +4 -4
- package/server/dist/features/note/graphql/note.field.resolver.js +14 -1
- package/server/dist/features/note/graphql/note.field.resolver.js.map +1 -1
- package/server/dist/features/note/graphql/note.mutation.resolver.js +73 -36
- package/server/dist/features/note/graphql/note.mutation.resolver.js.map +1 -1
- package/server/dist/features/note/graphql/note.query.resolver.js +74 -76
- package/server/dist/features/note/graphql/note.query.resolver.js.map +1 -1
- package/server/dist/features/note/graphql/note.resolver.js +3 -2
- package/server/dist/features/note/graphql/note.resolver.js.map +1 -1
- package/server/dist/features/note/graphql/note.type-defs.js +4 -1
- package/server/dist/features/note/graphql/note.type-defs.js.map +1 -1
- package/server/dist/features/note/services/cleanup.js +4 -3
- package/server/dist/features/note/services/cleanup.js.map +1 -1
- package/server/dist/features/note/services/content-blocks.js +113 -0
- package/server/dist/features/note/services/content-blocks.js.map +1 -0
- package/server/dist/features/note/services/search.js +10 -29
- package/server/dist/features/note/services/search.js.map +1 -1
- package/server/dist/features/note/services/snapshot.js +172 -12
- package/server/dist/features/note/services/snapshot.js.map +1 -1
- package/server/dist/features/note/services/write-conflict.js +87 -0
- package/server/dist/features/note/services/write-conflict.js.map +1 -0
- package/server/dist/features/note/services/write.js +107 -0
- package/server/dist/features/note/services/write.js.map +1 -0
- package/server/dist/modules/blocknote-tree.js +41 -0
- package/server/dist/modules/blocknote-tree.js.map +1 -0
- package/server/dist/modules/error-handler.js +7 -4
- package/server/dist/modules/error-handler.js.map +1 -1
- package/server/dist/modules/recovery-retention.js +2 -2
- package/server/dist/modules/recovery-retention.js.map +1 -1
- package/server/client/dist/assets/Callout-BAqbXI0L.js +0 -1
- package/server/client/dist/assets/Note-ChUWU3Su.js +0 -21
- package/server/client/dist/assets/note-core-GgFcksAF.js +0 -178
- package/server/client/dist/assets/note-vendor-B0t5F04o.js +0 -54
- package/server/dist/features/note/graphql/note.graphql.shared.js +0 -23
- package/server/dist/features/note/graphql/note.graphql.shared.js.map +0 -1
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { ensureTagByName } from "../../../features/tag/services/organization.js";
|
|
1
2
|
import models from "../../../models.js";
|
|
3
|
+
import { blocksToMarkdown, extractTagIdsFromContentJson } from "../../../modules/blocknote.js";
|
|
4
|
+
import { parseBlockNoteContent, walkBlockNoteTree } from "../../../modules/blocknote-tree.js";
|
|
2
5
|
import {
|
|
3
6
|
createRetentionCutoff,
|
|
4
7
|
RECOVERY_CLEANUP_BATCH_LIMIT,
|
|
5
8
|
SNAPSHOT_MAX_PER_NOTE,
|
|
6
9
|
SNAPSHOT_RETENTION_DAYS
|
|
7
10
|
} from "../../../modules/recovery-retention.js";
|
|
8
|
-
import { buildNoteSearchProjection } from "./search.js";
|
|
11
|
+
import { buildNoteSearchProjection, extractVisibleSearchTextFromContent } from "./search.js";
|
|
12
|
+
const SNAPSHOT_CONTENT_PREVIEW_MAX_LENGTH = 240;
|
|
9
13
|
const parseMeta = (value) => {
|
|
10
14
|
if (!value) {
|
|
11
15
|
return {};
|
|
@@ -20,11 +24,27 @@ const parseMeta = (value) => {
|
|
|
20
24
|
return {};
|
|
21
25
|
}
|
|
22
26
|
};
|
|
27
|
+
const readSnapshotContent = (payload) => {
|
|
28
|
+
try {
|
|
29
|
+
return parsePayload(payload).content;
|
|
30
|
+
} catch {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const buildSnapshotContentPreview = (content) => {
|
|
35
|
+
const preview = extractVisibleSearchTextFromContent(content);
|
|
36
|
+
if (preview.length <= SNAPSHOT_CONTENT_PREVIEW_MAX_LENGTH) {
|
|
37
|
+
return preview;
|
|
38
|
+
}
|
|
39
|
+
return `${preview.slice(0, SNAPSHOT_CONTENT_PREVIEW_MAX_LENGTH).trimEnd()}...`;
|
|
40
|
+
};
|
|
23
41
|
const serializeSnapshot = (snapshot) => ({
|
|
24
42
|
id: String(snapshot.id),
|
|
25
43
|
title: snapshot.title,
|
|
44
|
+
contentPreview: buildSnapshotContentPreview(readSnapshotContent(snapshot.payload)),
|
|
26
45
|
createdAt: snapshot.createdAt.toISOString(),
|
|
27
|
-
meta: parseMeta(snapshot.meta)
|
|
46
|
+
meta: parseMeta(snapshot.meta),
|
|
47
|
+
payload: snapshot.payload
|
|
28
48
|
});
|
|
29
49
|
const serializePayload = (note) => {
|
|
30
50
|
const payload = {
|
|
@@ -46,6 +66,100 @@ const parsePayload = (payload) => {
|
|
|
46
66
|
}
|
|
47
67
|
return parsed;
|
|
48
68
|
};
|
|
69
|
+
const extractRestoredTagIds = (content) => {
|
|
70
|
+
try {
|
|
71
|
+
return extractTagIdsFromContentJson(content).map(normalizeRestoredTagId).filter((id) => id !== null);
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const isRecord = (value) => {
|
|
77
|
+
return typeof value === "object" && value !== null;
|
|
78
|
+
};
|
|
79
|
+
const normalizeRestoredTagId = (value) => {
|
|
80
|
+
if (value === void 0 || value === null) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const normalized = typeof value === "string" ? value.trim() : value;
|
|
84
|
+
if (normalized === "") {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const numericId = Number(normalized);
|
|
88
|
+
return Number.isSafeInteger(numericId) && numericId > 0 ? numericId : null;
|
|
89
|
+
};
|
|
90
|
+
const normalizeRestoredTagName = (value) => {
|
|
91
|
+
if (typeof value !== "string") {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const normalized = value.trim();
|
|
95
|
+
return normalized ? normalized : null;
|
|
96
|
+
};
|
|
97
|
+
const resolveRestoredSnapshotTags = async (content, deps) => {
|
|
98
|
+
const parsed = parseBlockNoteContent(content);
|
|
99
|
+
if (!parsed) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const tagRefs = [];
|
|
103
|
+
walkBlockNoteTree(parsed, (node) => {
|
|
104
|
+
if (node.type !== "tag" || !isRecord(node.props)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
tagRefs.push({
|
|
108
|
+
id: normalizeRestoredTagId(node.props.id),
|
|
109
|
+
name: normalizeRestoredTagName(node.props.tag),
|
|
110
|
+
node,
|
|
111
|
+
props: node.props
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
if (tagRefs.length === 0) {
|
|
115
|
+
return { content, tagIds: [] };
|
|
116
|
+
}
|
|
117
|
+
const candidateIds = Array.from(new Set(tagRefs.map((tag) => tag.id).filter((id) => id !== null)));
|
|
118
|
+
const existingTags = candidateIds.length > 0 ? await deps.findTagsByIds(candidateIds) : [];
|
|
119
|
+
const existingTagIds = new Set(existingTags.map((tag) => tag.id));
|
|
120
|
+
const tagIds = /* @__PURE__ */ new Set();
|
|
121
|
+
let contentChanged = false;
|
|
122
|
+
for (const tagRef of tagRefs) {
|
|
123
|
+
if (tagRef.id !== null && existingTagIds.has(tagRef.id)) {
|
|
124
|
+
tagIds.add(tagRef.id);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!tagRef.name) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const resolvedTag = await deps.ensureTagByName(tagRef.name);
|
|
132
|
+
tagRef.node.props = {
|
|
133
|
+
...tagRef.props,
|
|
134
|
+
id: String(resolvedTag.id),
|
|
135
|
+
tag: resolvedTag.name
|
|
136
|
+
};
|
|
137
|
+
tagIds.add(resolvedTag.id);
|
|
138
|
+
contentChanged = true;
|
|
139
|
+
} catch {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
content: contentChanged ? JSON.stringify(parsed) : content,
|
|
145
|
+
tagIds: [...tagIds]
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
const renderNoteSnapshotContentAsMarkdown = async (payload) => {
|
|
149
|
+
try {
|
|
150
|
+
const snapshotPayload = parsePayload(payload);
|
|
151
|
+
return blocksToMarkdown(snapshotPayload.content);
|
|
152
|
+
} catch {
|
|
153
|
+
return "";
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const serializeSnapshotWithContent = async (snapshot) => {
|
|
157
|
+
const contentAsMarkdown = await renderNoteSnapshotContentAsMarkdown(snapshot.payload);
|
|
158
|
+
return {
|
|
159
|
+
...serializeSnapshot(snapshot),
|
|
160
|
+
contentAsMarkdown
|
|
161
|
+
};
|
|
162
|
+
};
|
|
49
163
|
const createSnapshotMetaFromUserAgent = (userAgent) => {
|
|
50
164
|
const normalized = userAgent?.toLowerCase() ?? "";
|
|
51
165
|
const isMobile = /iphone|ipad|android|mobile/.test(normalized);
|
|
@@ -92,11 +206,11 @@ const defaultTrimOverflowSnapshots = async (noteId, keep, limit) => {
|
|
|
92
206
|
const createNoteSnapshotService = (deps) => ({
|
|
93
207
|
captureBaseline: async (input) => {
|
|
94
208
|
await deps.purgeExpiredSnapshots(createRetentionCutoff(SNAPSHOT_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);
|
|
95
|
-
const existing = input.editSessionId ? await deps.findSnapshotByEditSessionId(input.noteId, input.editSessionId) : null;
|
|
209
|
+
const existing = input.editSessionId && !input.force ? await deps.findSnapshotByEditSessionId(input.noteId, input.editSessionId) : null;
|
|
96
210
|
if (existing) {
|
|
97
211
|
return serializeSnapshot(existing);
|
|
98
212
|
}
|
|
99
|
-
const note = await deps.findNoteById(input.noteId);
|
|
213
|
+
const note = input.baseline ?? await deps.findNoteById(input.noteId);
|
|
100
214
|
if (!note) {
|
|
101
215
|
return null;
|
|
102
216
|
}
|
|
@@ -109,18 +223,26 @@ const createNoteSnapshotService = (deps) => ({
|
|
|
109
223
|
noteId: note.id,
|
|
110
224
|
title: note.title,
|
|
111
225
|
payload,
|
|
112
|
-
...input.editSessionId ? { editSessionId: input.editSessionId } : {},
|
|
226
|
+
...input.editSessionId && !input.force ? { editSessionId: input.editSessionId } : {},
|
|
113
227
|
...input.meta ? { meta: input.meta } : {}
|
|
114
228
|
});
|
|
115
229
|
await deps.trimOverflowSnapshots(note.id, SNAPSHOT_MAX_PER_NOTE, RECOVERY_CLEANUP_BATCH_LIMIT);
|
|
116
230
|
return serializeSnapshot(snapshot);
|
|
117
231
|
},
|
|
118
|
-
listSnapshots: async (noteId, limit =
|
|
232
|
+
listSnapshots: async (noteId, limit = SNAPSHOT_MAX_PER_NOTE) => {
|
|
119
233
|
await deps.purgeExpiredSnapshots(createRetentionCutoff(SNAPSHOT_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);
|
|
120
234
|
await deps.trimOverflowSnapshots(noteId, SNAPSHOT_MAX_PER_NOTE, RECOVERY_CLEANUP_BATCH_LIMIT);
|
|
121
235
|
const snapshots = await deps.listSnapshots(noteId, limit);
|
|
122
236
|
return snapshots.map(serializeSnapshot);
|
|
123
237
|
},
|
|
238
|
+
getSnapshot: async (snapshotId) => {
|
|
239
|
+
await deps.purgeExpiredSnapshots(createRetentionCutoff(SNAPSHOT_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);
|
|
240
|
+
const snapshot = await deps.findSnapshotById(snapshotId);
|
|
241
|
+
if (!snapshot) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
return serializeSnapshotWithContent(snapshot);
|
|
245
|
+
},
|
|
124
246
|
restoreSnapshot: async (snapshotId, options) => {
|
|
125
247
|
await deps.purgeExpiredSnapshots(createRetentionCutoff(SNAPSHOT_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);
|
|
126
248
|
const snapshot = await deps.findSnapshotById(snapshotId);
|
|
@@ -131,6 +253,13 @@ const createNoteSnapshotService = (deps) => ({
|
|
|
131
253
|
if (!note) {
|
|
132
254
|
return null;
|
|
133
255
|
}
|
|
256
|
+
const payload = parsePayload(snapshot.payload);
|
|
257
|
+
const resolvedTags = deps.resolveRestoredTags ? await deps.resolveRestoredTags(payload.content) : extractRestoredTagIds(payload.content);
|
|
258
|
+
const restoredPayload = resolvedTags && !Array.isArray(resolvedTags) ? {
|
|
259
|
+
...payload,
|
|
260
|
+
content: resolvedTags.content
|
|
261
|
+
} : payload;
|
|
262
|
+
const restoredTagIds = Array.isArray(resolvedTags) ? resolvedTags : resolvedTags?.tagIds;
|
|
134
263
|
const currentPayload = serializePayload(note);
|
|
135
264
|
const latestSnapshot = await deps.findLatestSnapshot(note.id);
|
|
136
265
|
if (!hasSameSnapshotPayload(latestSnapshot, currentPayload)) {
|
|
@@ -141,9 +270,12 @@ const createNoteSnapshotService = (deps) => ({
|
|
|
141
270
|
...options?.meta ? { meta: options.meta } : {}
|
|
142
271
|
});
|
|
143
272
|
}
|
|
273
|
+
const restoredNote = await deps.updateNote(snapshot.noteId, {
|
|
274
|
+
...restoredPayload,
|
|
275
|
+
...restoredTagIds !== void 0 ? { tagIds: restoredTagIds } : {}
|
|
276
|
+
});
|
|
144
277
|
await deps.trimOverflowSnapshots(note.id, SNAPSHOT_MAX_PER_NOTE, RECOVERY_CLEANUP_BATCH_LIMIT);
|
|
145
|
-
|
|
146
|
-
return deps.updateNote(snapshot.noteId, payload);
|
|
278
|
+
return restoredNote;
|
|
147
279
|
}
|
|
148
280
|
});
|
|
149
281
|
const defaultNoteSnapshotService = createNoteSnapshotService({
|
|
@@ -177,6 +309,26 @@ const defaultNoteSnapshotService = createNoteSnapshotService({
|
|
|
177
309
|
},
|
|
178
310
|
purgeExpiredSnapshots: defaultPurgeExpiredSnapshots,
|
|
179
311
|
trimOverflowSnapshots: defaultTrimOverflowSnapshots,
|
|
312
|
+
resolveRestoredTags: async (content) => {
|
|
313
|
+
return resolveRestoredSnapshotTags(content, {
|
|
314
|
+
ensureTagByName: async (name) => {
|
|
315
|
+
const result = await ensureTagByName(name);
|
|
316
|
+
return {
|
|
317
|
+
id: Number(result.tag.id),
|
|
318
|
+
name: result.tag.name
|
|
319
|
+
};
|
|
320
|
+
},
|
|
321
|
+
findTagsByIds: async (ids) => {
|
|
322
|
+
return models.tag.findMany({
|
|
323
|
+
where: { id: { in: ids } },
|
|
324
|
+
select: {
|
|
325
|
+
id: true,
|
|
326
|
+
name: true
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
},
|
|
180
332
|
listSnapshots: async (noteId, limit) => {
|
|
181
333
|
return models.noteSnapshot.findMany({
|
|
182
334
|
where: { noteId },
|
|
@@ -188,14 +340,16 @@ const defaultNoteSnapshotService = createNoteSnapshotService({
|
|
|
188
340
|
return models.noteSnapshot.findUnique({ where: { id } });
|
|
189
341
|
},
|
|
190
342
|
updateNote: async (id, input) => {
|
|
343
|
+
const { tagIds, ...noteInput } = input;
|
|
191
344
|
return models.note.update({
|
|
192
345
|
where: { id },
|
|
193
346
|
data: {
|
|
194
|
-
...
|
|
347
|
+
...noteInput,
|
|
195
348
|
...buildNoteSearchProjection({
|
|
196
|
-
title:
|
|
197
|
-
content:
|
|
198
|
-
})
|
|
349
|
+
title: noteInput.title,
|
|
350
|
+
content: noteInput.content
|
|
351
|
+
}),
|
|
352
|
+
...tagIds !== void 0 ? { tags: { set: tagIds.map((tagId) => ({ id: tagId })) } } : {}
|
|
199
353
|
}
|
|
200
354
|
});
|
|
201
355
|
}
|
|
@@ -206,6 +360,9 @@ const captureNoteBaseline = async (input) => {
|
|
|
206
360
|
const listNoteSnapshots = async (noteId, limit) => {
|
|
207
361
|
return defaultNoteSnapshotService.listSnapshots(noteId, limit);
|
|
208
362
|
};
|
|
363
|
+
const getNoteSnapshot = async (snapshotId) => {
|
|
364
|
+
return defaultNoteSnapshotService.getSnapshot(snapshotId);
|
|
365
|
+
};
|
|
209
366
|
const restoreNoteSnapshot = async (snapshotId, options) => {
|
|
210
367
|
return defaultNoteSnapshotService.restoreSnapshot(snapshotId, options);
|
|
211
368
|
};
|
|
@@ -218,8 +375,11 @@ export {
|
|
|
218
375
|
createNoteSnapshotService,
|
|
219
376
|
createSnapshotMetaFromUserAgent,
|
|
220
377
|
defaultNoteSnapshotService,
|
|
378
|
+
getNoteSnapshot,
|
|
221
379
|
listNoteSnapshots,
|
|
222
380
|
purgeExpiredNoteSnapshots,
|
|
381
|
+
renderNoteSnapshotContentAsMarkdown,
|
|
382
|
+
resolveRestoredSnapshotTags,
|
|
223
383
|
restoreNoteSnapshot
|
|
224
384
|
};
|
|
225
385
|
//# sourceMappingURL=snapshot.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/features/note/services/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 '~/modules/recovery-retention.js';\nimport { buildNoteSearchProjection } from './search.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 findLatestSnapshot: (noteId: number) => 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: (\n id: number,\n input: {\n title: string;\n content: string;\n pinned: boolean;\n order: number;\n layout: NoteLayout;\n },\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 hasSameSnapshotPayload = (snapshot: NoteSnapshotRecord | null, payload: string) => {\n return snapshot?.payload === 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({\n where: { id: { in: expiredSnapshots.map((snapshot) => snapshot.id) } },\n });\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: [{ createdAt: 'desc' }, { id: 'desc' }],\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({\n where: { id: { in: overflowSnapshots.map((snapshot) => snapshot.id) } },\n });\n\n return deleted.count;\n};\n\nexport const createNoteSnapshotService = (deps: NoteSnapshotDeps) => ({\n captureBaseline: async (input: { noteId: number; editSessionId?: string; meta?: string }) => {\n await deps.purgeExpiredSnapshots(createRetentionCutoff(SNAPSHOT_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\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 payload = serializePayload(note);\n const latestSnapshot = await deps.findLatestSnapshot(note.id);\n\n if (latestSnapshot && hasSameSnapshotPayload(latestSnapshot, payload)) {\n return serializeSnapshot(latestSnapshot);\n }\n\n const snapshot = await deps.createSnapshot({\n noteId: note.id,\n title: note.title,\n payload,\n ...(input.editSessionId ? { editSessionId: input.editSessionId } : {}),\n ...(input.meta ? { meta: input.meta } : {}),\n });\n\n await deps.trimOverflowSnapshots(note.id, SNAPSHOT_MAX_PER_NOTE, RECOVERY_CLEANUP_BATCH_LIMIT);\n\n return serializeSnapshot(snapshot);\n },\n\n listSnapshots: async (noteId: number, limit = 5) => {\n await deps.purgeExpiredSnapshots(createRetentionCutoff(SNAPSHOT_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\n await deps.trimOverflowSnapshots(noteId, SNAPSHOT_MAX_PER_NOTE, RECOVERY_CLEANUP_BATCH_LIMIT);\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(createRetentionCutoff(SNAPSHOT_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\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 const currentPayload = serializePayload(note);\n const latestSnapshot = await deps.findLatestSnapshot(note.id);\n\n if (!hasSameSnapshotPayload(latestSnapshot, currentPayload)) {\n await deps.createSnapshot({\n noteId: note.id,\n title: note.title,\n payload: currentPayload,\n ...(options?.meta ? { meta: options.meta } : {}),\n });\n }\n\n await deps.trimOverflowSnapshots(note.id, SNAPSHOT_MAX_PER_NOTE, RECOVERY_CLEANUP_BATCH_LIMIT);\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 findLatestSnapshot: async (noteId) => {\n return models.noteSnapshot.findFirst({\n where: { noteId },\n orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],\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' }, { id: '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: {\n ...input,\n ...buildNoteSearchProjection({\n title: input.title,\n content: input.content,\n }),\n },\n });\n },\n});\n\nexport const captureNoteBaseline = async (input: { noteId: number; editSessionId?: string; meta?: string }) => {\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(createRetentionCutoff(SNAPSHOT_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\n};\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/features/note/services/snapshot.ts"],"sourcesContent":["import { ensureTagByName } from '~/features/tag/services/organization.js';\nimport models, { type NoteLayout } from '~/models.js';\nimport { blocksToMarkdown, extractTagIdsFromContentJson } from '~/modules/blocknote.js';\nimport { type BlockNoteTreeNode, parseBlockNoteContent, walkBlockNoteTree } from '~/modules/blocknote-tree.js';\nimport {\n createRetentionCutoff,\n RECOVERY_CLEANUP_BATCH_LIMIT,\n SNAPSHOT_MAX_PER_NOTE,\n SNAPSHOT_RETENTION_DAYS,\n} from '~/modules/recovery-retention.js';\nimport { buildNoteSearchProjection, extractVisibleSearchTextFromContent } from './search.js';\n\nconst SNAPSHOT_CONTENT_PREVIEW_MAX_LENGTH = 240;\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 findLatestSnapshot: (noteId: number) => 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 resolveRestoredTags?: (content: string) => Promise<ResolvedRestoredSnapshotTags | null>;\n updateNote: (\n id: number,\n input: {\n title: string;\n content: string;\n pinned: boolean;\n order: number;\n layout: NoteLayout;\n tagIds?: number[];\n },\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\ninterface TagRecord {\n id: number;\n name: string;\n}\n\ninterface ResolvedRestoredSnapshotTags {\n content: string;\n tagIds: number[];\n}\n\ninterface ResolveRestoredSnapshotTagsDeps {\n ensureTagByName: (name: string) => Promise<TagRecord>;\n findTagsByIds: (ids: number[]) => Promise<TagRecord[]>;\n}\n\nexport interface NoteSnapshotSummary {\n id: string;\n title: string;\n createdAt: string;\n meta: NoteSnapshotMeta;\n contentPreview: string;\n contentAsMarkdown?: string;\n payload?: string;\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 readSnapshotContent = (payload: string) => {\n try {\n return parsePayload(payload).content;\n } catch {\n return '';\n }\n};\n\nconst buildSnapshotContentPreview = (content: string) => {\n const preview = extractVisibleSearchTextFromContent(content);\n\n if (preview.length <= SNAPSHOT_CONTENT_PREVIEW_MAX_LENGTH) {\n return preview;\n }\n\n return `${preview.slice(0, SNAPSHOT_CONTENT_PREVIEW_MAX_LENGTH).trimEnd()}...`;\n};\n\nconst serializeSnapshot = (snapshot: NoteSnapshotRecord): NoteSnapshotSummary => ({\n id: String(snapshot.id),\n title: snapshot.title,\n contentPreview: buildSnapshotContentPreview(readSnapshotContent(snapshot.payload)),\n createdAt: snapshot.createdAt.toISOString(),\n meta: parseMeta(snapshot.meta),\n payload: snapshot.payload,\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 hasSameSnapshotPayload = (snapshot: NoteSnapshotRecord | null, payload: string) => {\n return snapshot?.payload === 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\nconst extractRestoredTagIds = (content: string): number[] | null => {\n try {\n return extractTagIdsFromContentJson(content)\n .map(normalizeRestoredTagId)\n .filter((id): id is number => id !== null);\n } catch {\n return null;\n }\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => {\n return typeof value === 'object' && value !== null;\n};\n\nconst normalizeRestoredTagId = (value: unknown) => {\n if (value === undefined || value === null) {\n return null;\n }\n\n const normalized = typeof value === 'string' ? value.trim() : value;\n\n if (normalized === '') {\n return null;\n }\n\n const numericId = Number(normalized);\n return Number.isSafeInteger(numericId) && numericId > 0 ? numericId : null;\n};\n\nconst normalizeRestoredTagName = (value: unknown) => {\n if (typeof value !== 'string') {\n return null;\n }\n\n const normalized = value.trim();\n return normalized ? normalized : null;\n};\n\nexport const resolveRestoredSnapshotTags = async (\n content: string,\n deps: ResolveRestoredSnapshotTagsDeps,\n): Promise<ResolvedRestoredSnapshotTags | null> => {\n const parsed = parseBlockNoteContent(content);\n\n if (!parsed) {\n return null;\n }\n\n const tagRefs: Array<{\n id: number | null;\n name: string | null;\n node: BlockNoteTreeNode;\n props: Record<string, unknown>;\n }> = [];\n\n walkBlockNoteTree(parsed, (node) => {\n if (node.type !== 'tag' || !isRecord(node.props)) {\n return;\n }\n\n tagRefs.push({\n id: normalizeRestoredTagId(node.props.id),\n name: normalizeRestoredTagName(node.props.tag),\n node,\n props: node.props,\n });\n });\n\n if (tagRefs.length === 0) {\n return { content, tagIds: [] };\n }\n\n const candidateIds = Array.from(new Set(tagRefs.map((tag) => tag.id).filter((id): id is number => id !== null)));\n const existingTags = candidateIds.length > 0 ? await deps.findTagsByIds(candidateIds) : [];\n const existingTagIds = new Set(existingTags.map((tag) => tag.id));\n const tagIds = new Set<number>();\n let contentChanged = false;\n\n for (const tagRef of tagRefs) {\n if (tagRef.id !== null && existingTagIds.has(tagRef.id)) {\n tagIds.add(tagRef.id);\n continue;\n }\n\n if (!tagRef.name) {\n continue;\n }\n\n try {\n const resolvedTag = await deps.ensureTagByName(tagRef.name);\n tagRef.node.props = {\n ...tagRef.props,\n id: String(resolvedTag.id),\n tag: resolvedTag.name,\n };\n tagIds.add(resolvedTag.id);\n contentChanged = true;\n } catch {\n continue;\n }\n }\n\n return {\n content: contentChanged ? JSON.stringify(parsed) : content,\n tagIds: [...tagIds],\n };\n};\n\nexport const renderNoteSnapshotContentAsMarkdown = async (payload: string) => {\n try {\n const snapshotPayload = parsePayload(payload);\n return blocksToMarkdown(snapshotPayload.content);\n } catch {\n return '';\n }\n};\n\nconst serializeSnapshotWithContent = async (snapshot: NoteSnapshotRecord): Promise<NoteSnapshotSummary> => {\n const contentAsMarkdown = await renderNoteSnapshotContentAsMarkdown(snapshot.payload);\n\n return {\n ...serializeSnapshot(snapshot),\n contentAsMarkdown,\n };\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({\n where: { id: { in: expiredSnapshots.map((snapshot) => snapshot.id) } },\n });\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: [{ createdAt: 'desc' }, { id: 'desc' }],\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({\n where: { id: { in: overflowSnapshots.map((snapshot) => snapshot.id) } },\n });\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 baseline?: NoteRecord;\n force?: boolean;\n }) => {\n await deps.purgeExpiredSnapshots(createRetentionCutoff(SNAPSHOT_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\n\n const existing =\n input.editSessionId && !input.force\n ? await deps.findSnapshotByEditSessionId(input.noteId, input.editSessionId)\n : null;\n\n if (existing) {\n return serializeSnapshot(existing);\n }\n\n const note = input.baseline ?? (await deps.findNoteById(input.noteId));\n\n if (!note) {\n return null;\n }\n\n const payload = serializePayload(note);\n const latestSnapshot = await deps.findLatestSnapshot(note.id);\n\n if (latestSnapshot && hasSameSnapshotPayload(latestSnapshot, payload)) {\n return serializeSnapshot(latestSnapshot);\n }\n\n const snapshot = await deps.createSnapshot({\n noteId: note.id,\n title: note.title,\n payload,\n ...(input.editSessionId && !input.force ? { editSessionId: input.editSessionId } : {}),\n ...(input.meta ? { meta: input.meta } : {}),\n });\n\n await deps.trimOverflowSnapshots(note.id, SNAPSHOT_MAX_PER_NOTE, RECOVERY_CLEANUP_BATCH_LIMIT);\n\n return serializeSnapshot(snapshot);\n },\n\n listSnapshots: async (noteId: number, limit = SNAPSHOT_MAX_PER_NOTE) => {\n await deps.purgeExpiredSnapshots(createRetentionCutoff(SNAPSHOT_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\n await deps.trimOverflowSnapshots(noteId, SNAPSHOT_MAX_PER_NOTE, RECOVERY_CLEANUP_BATCH_LIMIT);\n const snapshots = await deps.listSnapshots(noteId, limit);\n return snapshots.map(serializeSnapshot);\n },\n\n getSnapshot: async (snapshotId: number) => {\n await deps.purgeExpiredSnapshots(createRetentionCutoff(SNAPSHOT_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\n const snapshot = await deps.findSnapshotById(snapshotId);\n\n if (!snapshot) {\n return null;\n }\n\n return serializeSnapshotWithContent(snapshot);\n },\n\n restoreSnapshot: async (snapshotId: number, options?: { meta?: string }) => {\n await deps.purgeExpiredSnapshots(createRetentionCutoff(SNAPSHOT_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\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 const payload = parsePayload(snapshot.payload);\n const resolvedTags = deps.resolveRestoredTags\n ? await deps.resolveRestoredTags(payload.content)\n : extractRestoredTagIds(payload.content);\n const restoredPayload =\n resolvedTags && !Array.isArray(resolvedTags)\n ? {\n ...payload,\n content: resolvedTags.content,\n }\n : payload;\n const restoredTagIds = Array.isArray(resolvedTags) ? resolvedTags : resolvedTags?.tagIds;\n\n const currentPayload = serializePayload(note);\n const latestSnapshot = await deps.findLatestSnapshot(note.id);\n\n if (!hasSameSnapshotPayload(latestSnapshot, currentPayload)) {\n await deps.createSnapshot({\n noteId: note.id,\n title: note.title,\n payload: currentPayload,\n ...(options?.meta ? { meta: options.meta } : {}),\n });\n }\n\n const restoredNote = await deps.updateNote(snapshot.noteId, {\n ...restoredPayload,\n ...(restoredTagIds !== undefined ? { tagIds: restoredTagIds } : {}),\n });\n\n await deps.trimOverflowSnapshots(note.id, SNAPSHOT_MAX_PER_NOTE, RECOVERY_CLEANUP_BATCH_LIMIT);\n\n return restoredNote;\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 findLatestSnapshot: async (noteId) => {\n return models.noteSnapshot.findFirst({\n where: { noteId },\n orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],\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 resolveRestoredTags: async (content) => {\n return resolveRestoredSnapshotTags(content, {\n ensureTagByName: async (name) => {\n const result = await ensureTagByName(name);\n return {\n id: Number(result.tag.id),\n name: result.tag.name,\n };\n },\n findTagsByIds: async (ids) => {\n return models.tag.findMany({\n where: { id: { in: ids } },\n select: {\n id: true,\n name: true,\n },\n });\n },\n });\n },\n listSnapshots: async (noteId, limit) => {\n return models.noteSnapshot.findMany({\n where: { noteId },\n orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],\n take: limit,\n });\n },\n findSnapshotById: async (id) => {\n return models.noteSnapshot.findUnique({ where: { id } });\n },\n updateNote: async (id, input) => {\n const { tagIds, ...noteInput } = input;\n\n return models.note.update({\n where: { id },\n data: {\n ...noteInput,\n ...buildNoteSearchProjection({\n title: noteInput.title,\n content: noteInput.content,\n }),\n ...(tagIds !== undefined ? { tags: { set: tagIds.map((tagId) => ({ id: tagId })) } } : {}),\n },\n });\n },\n});\n\nexport const captureNoteBaseline = async (input: {\n noteId: number;\n editSessionId?: string;\n meta?: string;\n baseline?: NoteRecord;\n force?: boolean;\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 getNoteSnapshot = async (snapshotId: number) => {\n return defaultNoteSnapshotService.getSnapshot(snapshotId);\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(createRetentionCutoff(SNAPSHOT_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\n};\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const NOTE_UPDATE_CONFLICT_CODE = "NOTE_UPDATE_CONFLICT";
|
|
2
|
+
const INVALID_NOTE_VERSION_CODE = "INVALID_NOTE_VERSION";
|
|
3
|
+
const MISSING_NOTE_VERSION_CODE = "MISSING_NOTE_VERSION";
|
|
4
|
+
class InvalidNoteVersionError extends Error {
|
|
5
|
+
code = INVALID_NOTE_VERSION_CODE;
|
|
6
|
+
constructor() {
|
|
7
|
+
super("Invalid note version.");
|
|
8
|
+
this.name = "InvalidNoteVersionError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
class MissingNoteVersionError extends Error {
|
|
12
|
+
code = MISSING_NOTE_VERSION_CODE;
|
|
13
|
+
constructor() {
|
|
14
|
+
super("Expected note update time is required for this write.");
|
|
15
|
+
this.name = "MissingNoteVersionError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
class NoteVersionConflictError extends Error {
|
|
19
|
+
code = NOTE_UPDATE_CONFLICT_CODE;
|
|
20
|
+
currentUpdatedAt;
|
|
21
|
+
expectedUpdatedAt;
|
|
22
|
+
constructor(input) {
|
|
23
|
+
super("This note changed elsewhere. Reload the latest version before saving.");
|
|
24
|
+
this.name = "NoteVersionConflictError";
|
|
25
|
+
this.currentUpdatedAt = String(input.currentUpdatedAt);
|
|
26
|
+
this.expectedUpdatedAt = String(input.expectedUpdatedAt);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const parseNoteVersion = (value) => {
|
|
30
|
+
if (value === void 0 || value === null || value === "") {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const timestamp = /^\d+$/.test(value) ? Number(value) : Date.parse(value);
|
|
34
|
+
if (!Number.isFinite(timestamp)) {
|
|
35
|
+
throw new InvalidNoteVersionError();
|
|
36
|
+
}
|
|
37
|
+
return timestamp;
|
|
38
|
+
};
|
|
39
|
+
const createNoteVersionConflictError = ({
|
|
40
|
+
expectedUpdatedAt,
|
|
41
|
+
currentUpdatedAt
|
|
42
|
+
}) => {
|
|
43
|
+
return new NoteVersionConflictError({
|
|
44
|
+
expectedUpdatedAt,
|
|
45
|
+
currentUpdatedAt
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const assertExpectedNoteVersion = ({
|
|
49
|
+
expectedUpdatedAt,
|
|
50
|
+
currentUpdatedAt
|
|
51
|
+
}) => {
|
|
52
|
+
const expectedTimestamp = parseNoteVersion(expectedUpdatedAt);
|
|
53
|
+
if (expectedTimestamp === null) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const currentTimestamp = currentUpdatedAt.getTime();
|
|
57
|
+
if (expectedTimestamp !== currentTimestamp) {
|
|
58
|
+
throw createNoteVersionConflictError({
|
|
59
|
+
expectedUpdatedAt: expectedTimestamp,
|
|
60
|
+
currentUpdatedAt: currentTimestamp
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const isNoteVersionConflictError = (error) => {
|
|
65
|
+
return error instanceof NoteVersionConflictError;
|
|
66
|
+
};
|
|
67
|
+
const isInvalidNoteVersionError = (error) => {
|
|
68
|
+
return error instanceof InvalidNoteVersionError;
|
|
69
|
+
};
|
|
70
|
+
const isMissingNoteVersionError = (error) => {
|
|
71
|
+
return error instanceof MissingNoteVersionError;
|
|
72
|
+
};
|
|
73
|
+
export {
|
|
74
|
+
INVALID_NOTE_VERSION_CODE,
|
|
75
|
+
InvalidNoteVersionError,
|
|
76
|
+
MISSING_NOTE_VERSION_CODE,
|
|
77
|
+
MissingNoteVersionError,
|
|
78
|
+
NOTE_UPDATE_CONFLICT_CODE,
|
|
79
|
+
NoteVersionConflictError,
|
|
80
|
+
assertExpectedNoteVersion,
|
|
81
|
+
createNoteVersionConflictError,
|
|
82
|
+
isInvalidNoteVersionError,
|
|
83
|
+
isMissingNoteVersionError,
|
|
84
|
+
isNoteVersionConflictError,
|
|
85
|
+
parseNoteVersion
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=write-conflict.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/features/note/services/write-conflict.ts"],"sourcesContent":["export const NOTE_UPDATE_CONFLICT_CODE = 'NOTE_UPDATE_CONFLICT';\nexport const INVALID_NOTE_VERSION_CODE = 'INVALID_NOTE_VERSION';\nexport const MISSING_NOTE_VERSION_CODE = 'MISSING_NOTE_VERSION';\n\nexport class InvalidNoteVersionError extends Error {\n code = INVALID_NOTE_VERSION_CODE;\n\n constructor() {\n super('Invalid note version.');\n this.name = 'InvalidNoteVersionError';\n }\n}\n\nexport class MissingNoteVersionError extends Error {\n code = MISSING_NOTE_VERSION_CODE;\n\n constructor() {\n super('Expected note update time is required for this write.');\n this.name = 'MissingNoteVersionError';\n }\n}\n\nexport class NoteVersionConflictError extends Error {\n code = NOTE_UPDATE_CONFLICT_CODE;\n currentUpdatedAt: string;\n expectedUpdatedAt: string;\n\n constructor(input: { currentUpdatedAt: string | number; expectedUpdatedAt: string | number }) {\n super('This note changed elsewhere. Reload the latest version before saving.');\n this.name = 'NoteVersionConflictError';\n this.currentUpdatedAt = String(input.currentUpdatedAt);\n this.expectedUpdatedAt = String(input.expectedUpdatedAt);\n }\n}\n\nexport const parseNoteVersion = (value?: string | null) => {\n if (value === undefined || value === null || value === '') {\n return null;\n }\n\n const timestamp = /^\\d+$/.test(value) ? Number(value) : Date.parse(value);\n\n if (!Number.isFinite(timestamp)) {\n throw new InvalidNoteVersionError();\n }\n\n return timestamp;\n};\n\nexport const createNoteVersionConflictError = ({\n expectedUpdatedAt,\n currentUpdatedAt,\n}: {\n expectedUpdatedAt: string | number;\n currentUpdatedAt: string | number;\n}) => {\n return new NoteVersionConflictError({\n expectedUpdatedAt,\n currentUpdatedAt,\n });\n};\n\nexport const assertExpectedNoteVersion = ({\n expectedUpdatedAt,\n currentUpdatedAt,\n}: {\n expectedUpdatedAt?: string | null;\n currentUpdatedAt: Date;\n}) => {\n const expectedTimestamp = parseNoteVersion(expectedUpdatedAt);\n\n if (expectedTimestamp === null) {\n return;\n }\n\n const currentTimestamp = currentUpdatedAt.getTime();\n\n if (expectedTimestamp !== currentTimestamp) {\n throw createNoteVersionConflictError({\n expectedUpdatedAt: expectedTimestamp,\n currentUpdatedAt: currentTimestamp,\n });\n }\n};\n\nexport const isNoteVersionConflictError = (error: unknown): error is NoteVersionConflictError => {\n return error instanceof NoteVersionConflictError;\n};\n\nexport const isInvalidNoteVersionError = (error: unknown): error is InvalidNoteVersionError => {\n return error instanceof InvalidNoteVersionError;\n};\n\nexport const isMissingNoteVersionError = (error: unknown): error is MissingNoteVersionError => {\n return error instanceof MissingNoteVersionError;\n};\n"],"mappings":"AAAO,MAAM,4BAA4B;AAClC,MAAM,4BAA4B;AAClC,MAAM,4BAA4B;AAElC,MAAM,gCAAgC,MAAM;AAAA,EAC/C,OAAO;AAAA,EAEP,cAAc;AACV,UAAM,uBAAuB;AAC7B,SAAK,OAAO;AAAA,EAChB;AACJ;AAEO,MAAM,gCAAgC,MAAM;AAAA,EAC/C,OAAO;AAAA,EAEP,cAAc;AACV,UAAM,uDAAuD;AAC7D,SAAK,OAAO;AAAA,EAChB;AACJ;AAEO,MAAM,iCAAiC,MAAM;AAAA,EAChD,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EAEA,YAAY,OAAkF;AAC1F,UAAM,uEAAuE;AAC7E,SAAK,OAAO;AACZ,SAAK,mBAAmB,OAAO,MAAM,gBAAgB;AACrD,SAAK,oBAAoB,OAAO,MAAM,iBAAiB;AAAA,EAC3D;AACJ;AAEO,MAAM,mBAAmB,CAAC,UAA0B;AACvD,MAAI,UAAU,UAAa,UAAU,QAAQ,UAAU,IAAI;AACvD,WAAO;AAAA,EACX;AAEA,QAAM,YAAY,QAAQ,KAAK,KAAK,IAAI,OAAO,KAAK,IAAI,KAAK,MAAM,KAAK;AAExE,MAAI,CAAC,OAAO,SAAS,SAAS,GAAG;AAC7B,UAAM,IAAI,wBAAwB;AAAA,EACtC;AAEA,SAAO;AACX;AAEO,MAAM,iCAAiC,CAAC;AAAA,EAC3C;AAAA,EACA;AACJ,MAGM;AACF,SAAO,IAAI,yBAAyB;AAAA,IAChC;AAAA,IACA;AAAA,EACJ,CAAC;AACL;AAEO,MAAM,4BAA4B,CAAC;AAAA,EACtC;AAAA,EACA;AACJ,MAGM;AACF,QAAM,oBAAoB,iBAAiB,iBAAiB;AAE5D,MAAI,sBAAsB,MAAM;AAC5B;AAAA,EACJ;AAEA,QAAM,mBAAmB,iBAAiB,QAAQ;AAElD,MAAI,sBAAsB,kBAAkB;AACxC,UAAM,+BAA+B;AAAA,MACjC,mBAAmB;AAAA,MACnB,kBAAkB;AAAA,IACtB,CAAC;AAAA,EACL;AACJ;AAEO,MAAM,6BAA6B,CAAC,UAAsD;AAC7F,SAAO,iBAAiB;AAC5B;AAEO,MAAM,4BAA4B,CAAC,UAAqD;AAC3F,SAAO,iBAAiB;AAC5B;AAEO,MAAM,4BAA4B,CAAC,UAAqD;AAC3F,SAAO,iBAAiB;AAC5B;","names":[]}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import models, { Prisma } from "../../../models.js";
|
|
2
|
+
import { buildNoteSearchProjection } from "./search.js";
|
|
3
|
+
import { captureNoteBaseline } from "./snapshot.js";
|
|
4
|
+
import { createNoteVersionConflictError, MissingNoteVersionError, parseNoteVersion } from "./write-conflict.js";
|
|
5
|
+
const isRecordNotFoundError = (error) => {
|
|
6
|
+
return error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025";
|
|
7
|
+
};
|
|
8
|
+
const createNoteWriteService = (deps) => ({
|
|
9
|
+
updateNoteWithVersionGuard: async ({
|
|
10
|
+
id,
|
|
11
|
+
data,
|
|
12
|
+
editSessionId,
|
|
13
|
+
expectedUpdatedAt,
|
|
14
|
+
snapshotMeta,
|
|
15
|
+
force = false
|
|
16
|
+
}) => {
|
|
17
|
+
const existingNote = await deps.findNoteForWrite(id);
|
|
18
|
+
if (!existingNote) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const expectedTimestamp = parseNoteVersion(expectedUpdatedAt);
|
|
22
|
+
if (expectedTimestamp === null && !force) {
|
|
23
|
+
throw new MissingNoteVersionError();
|
|
24
|
+
}
|
|
25
|
+
const nextTitle = data.title ?? existingNote.title;
|
|
26
|
+
const nextContent = data.content ?? existingNote.content;
|
|
27
|
+
const where = force ? { id } : { id, updatedAt: new Date(expectedTimestamp ?? existingNote.updatedAt.getTime()) };
|
|
28
|
+
try {
|
|
29
|
+
const updatedNote = await deps.updateNote({
|
|
30
|
+
where,
|
|
31
|
+
data: {
|
|
32
|
+
...data.title !== void 0 ? { title: data.title } : {},
|
|
33
|
+
...data.content !== void 0 ? { content: data.content } : {},
|
|
34
|
+
...data.layout !== void 0 ? { layout: data.layout } : {},
|
|
35
|
+
...buildNoteSearchProjection({
|
|
36
|
+
title: nextTitle,
|
|
37
|
+
content: nextContent
|
|
38
|
+
}),
|
|
39
|
+
...data.tagIds ? { tags: { set: data.tagIds.map((tagId) => ({ id: tagId })) } } : {}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
await deps.captureBaseline({
|
|
43
|
+
noteId: id,
|
|
44
|
+
baseline: {
|
|
45
|
+
id,
|
|
46
|
+
title: existingNote.title,
|
|
47
|
+
content: existingNote.content,
|
|
48
|
+
pinned: existingNote.pinned,
|
|
49
|
+
order: existingNote.order,
|
|
50
|
+
layout: existingNote.layout
|
|
51
|
+
},
|
|
52
|
+
...editSessionId && !force ? { editSessionId } : {},
|
|
53
|
+
...snapshotMeta ? { meta: snapshotMeta } : {},
|
|
54
|
+
...force ? { force: true } : {}
|
|
55
|
+
});
|
|
56
|
+
return updatedNote;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (deps.isRecordNotFoundError(error)) {
|
|
59
|
+
const currentNote = await deps.findNoteVersion(id);
|
|
60
|
+
if (!currentNote) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
if (expectedTimestamp === null || currentNote.updatedAt.getTime() === expectedTimestamp) {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
throw createNoteVersionConflictError({
|
|
67
|
+
expectedUpdatedAt: expectedTimestamp,
|
|
68
|
+
currentUpdatedAt: currentNote.updatedAt.getTime()
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
const defaultNoteWriteService = createNoteWriteService({
|
|
76
|
+
findNoteForWrite: (id) => models.note.findUnique({
|
|
77
|
+
where: { id },
|
|
78
|
+
select: {
|
|
79
|
+
title: true,
|
|
80
|
+
content: true,
|
|
81
|
+
updatedAt: true,
|
|
82
|
+
pinned: true,
|
|
83
|
+
order: true,
|
|
84
|
+
layout: true
|
|
85
|
+
}
|
|
86
|
+
}),
|
|
87
|
+
findNoteVersion: (id) => models.note.findUnique({
|
|
88
|
+
where: { id },
|
|
89
|
+
select: {
|
|
90
|
+
updatedAt: true
|
|
91
|
+
}
|
|
92
|
+
}),
|
|
93
|
+
captureBaseline: captureNoteBaseline,
|
|
94
|
+
updateNote: ({ where, data }) => models.note.update({
|
|
95
|
+
where,
|
|
96
|
+
data
|
|
97
|
+
}),
|
|
98
|
+
isRecordNotFoundError
|
|
99
|
+
});
|
|
100
|
+
const updateNoteWithVersionGuard = async (input) => {
|
|
101
|
+
return defaultNoteWriteService.updateNoteWithVersionGuard(input);
|
|
102
|
+
};
|
|
103
|
+
export {
|
|
104
|
+
createNoteWriteService,
|
|
105
|
+
updateNoteWithVersionGuard
|
|
106
|
+
};
|
|
107
|
+
//# sourceMappingURL=write.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/features/note/services/write.ts"],"sourcesContent":["import models, { type NoteLayout, Prisma } from '~/models.js';\nimport { buildNoteSearchProjection } from './search.js';\nimport { captureNoteBaseline } from './snapshot.js';\nimport { createNoteVersionConflictError, MissingNoteVersionError, parseNoteVersion } from './write-conflict.js';\n\ninterface NoteWriteRecord {\n id: number;\n title: string;\n content: string;\n layout: NoteLayout;\n createdAt: Date;\n updatedAt: Date;\n pinned: boolean;\n order: number;\n}\n\ntype NoteWriteData = {\n title?: string;\n content?: string;\n layout?: NoteLayout;\n tagIds?: number[];\n};\n\ninterface UpdateNoteWithVersionGuardInput {\n id: number;\n data: NoteWriteData;\n editSessionId?: string;\n expectedUpdatedAt?: string;\n snapshotMeta?: string;\n force?: boolean;\n}\n\ninterface NoteWriteDeps {\n findNoteForWrite: (\n id: number,\n ) => Promise<Pick<NoteWriteRecord, 'title' | 'content' | 'updatedAt' | 'pinned' | 'order' | 'layout'> | null>;\n findNoteVersion: (id: number) => Promise<Pick<NoteWriteRecord, 'updatedAt'> | null>;\n captureBaseline: (input: {\n noteId: number;\n editSessionId?: string;\n meta?: string;\n baseline: Pick<NoteWriteRecord, 'id' | 'title' | 'content' | 'pinned' | 'order' | 'layout'>;\n force?: boolean;\n }) => Promise<unknown>;\n updateNote: (input: {\n where: {\n id: number;\n updatedAt?: Date;\n };\n data: {\n title?: string;\n content?: string;\n layout?: NoteLayout;\n searchableText: string;\n searchableTextVersion: number;\n tags?: {\n set: Array<{ id: number }>;\n };\n };\n }) => Promise<NoteWriteRecord>;\n isRecordNotFoundError: (error: unknown) => boolean;\n}\n\nconst isRecordNotFoundError = (error: unknown) => {\n return error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025';\n};\n\nexport const createNoteWriteService = (deps: NoteWriteDeps) => ({\n updateNoteWithVersionGuard: async ({\n id,\n data,\n editSessionId,\n expectedUpdatedAt,\n snapshotMeta,\n force = false,\n }: UpdateNoteWithVersionGuardInput): Promise<NoteWriteRecord | null> => {\n const existingNote = await deps.findNoteForWrite(id);\n\n if (!existingNote) {\n return null;\n }\n\n const expectedTimestamp = parseNoteVersion(expectedUpdatedAt);\n\n if (expectedTimestamp === null && !force) {\n throw new MissingNoteVersionError();\n }\n\n const nextTitle = data.title ?? existingNote.title;\n const nextContent = data.content ?? existingNote.content;\n const where = force\n ? { id }\n : { id, updatedAt: new Date(expectedTimestamp ?? existingNote.updatedAt.getTime()) };\n\n try {\n const updatedNote = await deps.updateNote({\n where,\n data: {\n ...(data.title !== undefined ? { title: data.title } : {}),\n ...(data.content !== undefined ? { content: data.content } : {}),\n ...(data.layout !== undefined ? { layout: data.layout } : {}),\n ...buildNoteSearchProjection({\n title: nextTitle,\n content: nextContent,\n }),\n ...(data.tagIds ? { tags: { set: data.tagIds.map((tagId) => ({ id: tagId })) } } : {}),\n },\n });\n\n await deps.captureBaseline({\n noteId: id,\n baseline: {\n id,\n title: existingNote.title,\n content: existingNote.content,\n pinned: existingNote.pinned,\n order: existingNote.order,\n layout: existingNote.layout,\n },\n ...(editSessionId && !force ? { editSessionId } : {}),\n ...(snapshotMeta ? { meta: snapshotMeta } : {}),\n ...(force ? { force: true } : {}),\n });\n\n return updatedNote;\n } catch (error) {\n if (deps.isRecordNotFoundError(error)) {\n const currentNote = await deps.findNoteVersion(id);\n\n if (!currentNote) {\n return null;\n }\n\n if (expectedTimestamp === null || currentNote.updatedAt.getTime() === expectedTimestamp) {\n throw error;\n }\n\n throw createNoteVersionConflictError({\n expectedUpdatedAt: expectedTimestamp,\n currentUpdatedAt: currentNote.updatedAt.getTime(),\n });\n }\n\n throw error;\n }\n },\n});\n\nconst defaultNoteWriteService = createNoteWriteService({\n findNoteForWrite: (id) =>\n models.note.findUnique({\n where: { id },\n select: {\n title: true,\n content: true,\n updatedAt: true,\n pinned: true,\n order: true,\n layout: true,\n },\n }),\n findNoteVersion: (id) =>\n models.note.findUnique({\n where: { id },\n select: {\n updatedAt: true,\n },\n }),\n captureBaseline: captureNoteBaseline,\n updateNote: ({ where, data }) =>\n models.note.update({\n where,\n data,\n }),\n isRecordNotFoundError,\n});\n\nexport const updateNoteWithVersionGuard = async (input: UpdateNoteWithVersionGuardInput) => {\n return defaultNoteWriteService.updateNoteWithVersionGuard(input);\n};\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const isBlockNoteTreeNode = (value) => {
|
|
2
|
+
return typeof value === "object" && value !== null;
|
|
3
|
+
};
|
|
4
|
+
const parseBlockNoteContent = (content) => {
|
|
5
|
+
try {
|
|
6
|
+
const parsed = JSON.parse(content);
|
|
7
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const walkBlockNoteTree = (value, visit) => {
|
|
13
|
+
if (Array.isArray(value)) {
|
|
14
|
+
for (const item of value) {
|
|
15
|
+
walkBlockNoteTree(item, visit);
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (!isBlockNoteTreeNode(value)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
visit(value);
|
|
23
|
+
if ("content" in value) {
|
|
24
|
+
walkBlockNoteTree(value.content, visit);
|
|
25
|
+
}
|
|
26
|
+
if ("children" in value) {
|
|
27
|
+
walkBlockNoteTree(value.children, visit);
|
|
28
|
+
}
|
|
29
|
+
if ("rows" in value) {
|
|
30
|
+
walkBlockNoteTree(value.rows, visit);
|
|
31
|
+
}
|
|
32
|
+
if ("cells" in value) {
|
|
33
|
+
walkBlockNoteTree(value.cells, visit);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
export {
|
|
37
|
+
isBlockNoteTreeNode,
|
|
38
|
+
parseBlockNoteContent,
|
|
39
|
+
walkBlockNoteTree
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=blocknote-tree.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/modules/blocknote-tree.ts"],"sourcesContent":["export type BlockNoteTreeNode = Record<string, unknown>;\n\nexport const isBlockNoteTreeNode = (value: unknown): value is BlockNoteTreeNode => {\n return typeof value === 'object' && value !== null;\n};\n\nexport const parseBlockNoteContent = (content: string): unknown[] | null => {\n try {\n const parsed = JSON.parse(content);\n return Array.isArray(parsed) ? parsed : null;\n } catch {\n return null;\n }\n};\n\nexport const walkBlockNoteTree = (value: unknown, visit: (node: BlockNoteTreeNode) => void) => {\n if (Array.isArray(value)) {\n for (const item of value) {\n walkBlockNoteTree(item, visit);\n }\n return;\n }\n\n if (!isBlockNoteTreeNode(value)) {\n return;\n }\n\n visit(value);\n\n if ('content' in value) {\n walkBlockNoteTree(value.content, visit);\n }\n\n if ('children' in value) {\n walkBlockNoteTree(value.children, visit);\n }\n\n if ('rows' in value) {\n walkBlockNoteTree(value.rows, visit);\n }\n\n if ('cells' in value) {\n walkBlockNoteTree(value.cells, visit);\n }\n};\n"],"mappings":"AAEO,MAAM,sBAAsB,CAAC,UAA+C;AAC/E,SAAO,OAAO,UAAU,YAAY,UAAU;AAClD;AAEO,MAAM,wBAAwB,CAAC,YAAsC;AACxE,MAAI;AACA,UAAM,SAAS,KAAK,MAAM,OAAO;AACjC,WAAO,MAAM,QAAQ,MAAM,IAAI,SAAS;AAAA,EAC5C,QAAQ;AACJ,WAAO;AAAA,EACX;AACJ;AAEO,MAAM,oBAAoB,CAAC,OAAgB,UAA6C;AAC3F,MAAI,MAAM,QAAQ,KAAK,GAAG;AACtB,eAAW,QAAQ,OAAO;AACtB,wBAAkB,MAAM,KAAK;AAAA,IACjC;AACA;AAAA,EACJ;AAEA,MAAI,CAAC,oBAAoB,KAAK,GAAG;AAC7B;AAAA,EACJ;AAEA,QAAM,KAAK;AAEX,MAAI,aAAa,OAAO;AACpB,sBAAkB,MAAM,SAAS,KAAK;AAAA,EAC1C;AAEA,MAAI,cAAc,OAAO;AACrB,sBAAkB,MAAM,UAAU,KAAK;AAAA,EAC3C;AAEA,MAAI,UAAU,OAAO;AACjB,sBAAkB,MAAM,MAAM,KAAK;AAAA,EACvC;AAEA,MAAI,WAAW,OAAO;AAClB,sBAAkB,MAAM,OAAO,KAAK;AAAA,EACxC;AACJ;","names":[]}
|