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.
Files changed (64) hide show
  1. package/package.json +1 -1
  2. package/server/client/dist/assets/{Calendar-DI0PhQTq.js → Calendar-BU9XgX2N.js} +1 -1
  3. package/server/client/dist/assets/Callout-C6tVCxOT.js +1 -0
  4. package/server/client/dist/assets/{Graph-Dhuog4tc.js → Graph-Dond2hGK.js} +1 -1
  5. package/server/client/dist/assets/{Image-DlJ4MDo3.js → Image-DxWhlIDG.js} +1 -1
  6. package/server/client/dist/assets/{Image.es-COwqkQuh.js → Image.es-BjGK7y6Y.js} +1 -1
  7. package/server/client/dist/assets/Note-Cr6GtXUM.js +21 -0
  8. package/server/client/dist/assets/{Plus.es-KeV96R_e.js → Plus.es-EcmtS_uw.js} +1 -1
  9. package/server/client/dist/assets/{Reminders-D7MCTNxb.js → Reminders-BlLLT_x0.js} +1 -1
  10. package/server/client/dist/assets/{Search-B-fwTr5y.js → Search-CrZ0ap8x.js} +1 -1
  11. package/server/client/dist/assets/{SurfaceCard-CHjcCuqZ.js → SurfaceCard-CO0oVxeL.js} +1 -1
  12. package/server/client/dist/assets/{Tag-BbV2z-Db.js → Tag-vws5AfNW.js} +1 -1
  13. package/server/client/dist/assets/{TagNotes-2HKF09Jm.js → TagNotes-Fmf1Kgim.js} +1 -1
  14. package/server/client/dist/assets/{Trash.es-BqRTZPrI.js → Trash.es-DmwfSy3l.js} +1 -1
  15. package/server/client/dist/assets/{ViewNotes-TfM3Uzbp.js → ViewNotes-BALN7IfH.js} +1 -1
  16. package/server/client/dist/assets/{Views-CUwtnfa8.js → Views-Ccc_Ml5U.js} +1 -1
  17. package/server/client/dist/assets/{image.api-CMdQbCyN.js → image.api-DeF9ck7g.js} +1 -1
  18. package/server/client/dist/assets/{index-zhxZjg7V.js → index-BSVE2cGd.js} +1 -1
  19. package/server/client/dist/assets/{index-D2gRB1ig.css → index-BZUGWdOU.css} +1 -1
  20. package/server/client/dist/assets/{index-B4l2UvuC.js → index-BvKBTjQq.js} +51 -37
  21. package/server/client/dist/assets/{manage-image-DdXb5Bkj.js → manage-image-IoTXaMBP.js} +1 -1
  22. package/server/client/dist/assets/{manage-image-detail-CrrXCQIX.js → manage-image-detail-BRTXwOsh.js} +1 -1
  23. package/server/client/dist/assets/{mcp-DqGLZv90.js → mcp-Bh743HBz.js} +1 -1
  24. package/server/client/dist/assets/note-core-Z6kqulGB.js +180 -0
  25. package/server/client/dist/assets/note-vendor-i1M2FSI2.js +54 -0
  26. package/server/client/dist/assets/{placeholder-BXLtvdtN.js → placeholder-XQqo1Fvr.js} +3 -3
  27. package/server/client/dist/assets/{trash-3eUAjFRh.js → trash-hiJIg6-c.js} +1 -1
  28. package/server/client/dist/assets/{useReminderMutate-v1EXqM90.js → useReminderMutate-D21PurIS.js} +1 -1
  29. package/server/client/dist/assets/{view-dashboard-T6gmohr-.js → view-dashboard-BuaZ4MvI.js} +1 -1
  30. package/server/client/dist/index.html +4 -4
  31. package/server/dist/features/note/graphql/note.field.resolver.js +14 -1
  32. package/server/dist/features/note/graphql/note.field.resolver.js.map +1 -1
  33. package/server/dist/features/note/graphql/note.mutation.resolver.js +73 -36
  34. package/server/dist/features/note/graphql/note.mutation.resolver.js.map +1 -1
  35. package/server/dist/features/note/graphql/note.query.resolver.js +74 -76
  36. package/server/dist/features/note/graphql/note.query.resolver.js.map +1 -1
  37. package/server/dist/features/note/graphql/note.resolver.js +3 -2
  38. package/server/dist/features/note/graphql/note.resolver.js.map +1 -1
  39. package/server/dist/features/note/graphql/note.type-defs.js +4 -1
  40. package/server/dist/features/note/graphql/note.type-defs.js.map +1 -1
  41. package/server/dist/features/note/services/cleanup.js +4 -3
  42. package/server/dist/features/note/services/cleanup.js.map +1 -1
  43. package/server/dist/features/note/services/content-blocks.js +113 -0
  44. package/server/dist/features/note/services/content-blocks.js.map +1 -0
  45. package/server/dist/features/note/services/search.js +10 -29
  46. package/server/dist/features/note/services/search.js.map +1 -1
  47. package/server/dist/features/note/services/snapshot.js +172 -12
  48. package/server/dist/features/note/services/snapshot.js.map +1 -1
  49. package/server/dist/features/note/services/write-conflict.js +87 -0
  50. package/server/dist/features/note/services/write-conflict.js.map +1 -0
  51. package/server/dist/features/note/services/write.js +107 -0
  52. package/server/dist/features/note/services/write.js.map +1 -0
  53. package/server/dist/modules/blocknote-tree.js +41 -0
  54. package/server/dist/modules/blocknote-tree.js.map +1 -0
  55. package/server/dist/modules/error-handler.js +7 -4
  56. package/server/dist/modules/error-handler.js.map +1 -1
  57. package/server/dist/modules/recovery-retention.js +2 -2
  58. package/server/dist/modules/recovery-retention.js.map +1 -1
  59. package/server/client/dist/assets/Callout-BAqbXI0L.js +0 -1
  60. package/server/client/dist/assets/Note-ChUWU3Su.js +0 -21
  61. package/server/client/dist/assets/note-core-GgFcksAF.js +0 -178
  62. package/server/client/dist/assets/note-vendor-B0t5F04o.js +0 -54
  63. package/server/dist/features/note/graphql/note.graphql.shared.js +0 -23
  64. 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 = 5) => {
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
- const payload = parsePayload(snapshot.payload);
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
- ...input,
347
+ ...noteInput,
195
348
  ...buildNoteSearchProjection({
196
- title: input.title,
197
- content: input.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":"AAAA,OAAO,UAA2B,cAAc;AAChD,SAAS,iCAAiC;AAC1C,SAAS,2BAA2B;AACpC,SAAS,gCAAgC,yBAAyB,wBAAwB;AA4D1F,MAAM,wBAAwB,CAAC,UAAmB;AAC9C,SAAO,iBAAiB,OAAO,iCAAiC,MAAM,SAAS;AACnF;AAEO,MAAM,yBAAyB,CAAC,UAAyB;AAAA,EAC5D,4BAA4B,OAAO;AAAA,IAC/B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,EACZ,MAAwE;AACpE,UAAM,eAAe,MAAM,KAAK,iBAAiB,EAAE;AAEnD,QAAI,CAAC,cAAc;AACf,aAAO;AAAA,IACX;AAEA,UAAM,oBAAoB,iBAAiB,iBAAiB;AAE5D,QAAI,sBAAsB,QAAQ,CAAC,OAAO;AACtC,YAAM,IAAI,wBAAwB;AAAA,IACtC;AAEA,UAAM,YAAY,KAAK,SAAS,aAAa;AAC7C,UAAM,cAAc,KAAK,WAAW,aAAa;AACjD,UAAM,QAAQ,QACR,EAAE,GAAG,IACL,EAAE,IAAI,WAAW,IAAI,KAAK,qBAAqB,aAAa,UAAU,QAAQ,CAAC,EAAE;AAEvF,QAAI;AACA,YAAM,cAAc,MAAM,KAAK,WAAW;AAAA,QACtC;AAAA,QACA,MAAM;AAAA,UACF,GAAI,KAAK,UAAU,SAAY,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;AAAA,UACxD,GAAI,KAAK,YAAY,SAAY,EAAE,SAAS,KAAK,QAAQ,IAAI,CAAC;AAAA,UAC9D,GAAI,KAAK,WAAW,SAAY,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;AAAA,UAC3D,GAAG,0BAA0B;AAAA,YACzB,OAAO;AAAA,YACP,SAAS;AAAA,UACb,CAAC;AAAA,UACD,GAAI,KAAK,SAAS,EAAE,MAAM,EAAE,KAAK,KAAK,OAAO,IAAI,CAAC,WAAW,EAAE,IAAI,MAAM,EAAE,EAAE,EAAE,IAAI,CAAC;AAAA,QACxF;AAAA,MACJ,CAAC;AAED,YAAM,KAAK,gBAAgB;AAAA,QACvB,QAAQ;AAAA,QACR,UAAU;AAAA,UACN;AAAA,UACA,OAAO,aAAa;AAAA,UACpB,SAAS,aAAa;AAAA,UACtB,QAAQ,aAAa;AAAA,UACrB,OAAO,aAAa;AAAA,UACpB,QAAQ,aAAa;AAAA,QACzB;AAAA,QACA,GAAI,iBAAiB,CAAC,QAAQ,EAAE,cAAc,IAAI,CAAC;AAAA,QACnD,GAAI,eAAe,EAAE,MAAM,aAAa,IAAI,CAAC;AAAA,QAC7C,GAAI,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;AAAA,MACnC,CAAC;AAED,aAAO;AAAA,IACX,SAAS,OAAO;AACZ,UAAI,KAAK,sBAAsB,KAAK,GAAG;AACnC,cAAM,cAAc,MAAM,KAAK,gBAAgB,EAAE;AAEjD,YAAI,CAAC,aAAa;AACd,iBAAO;AAAA,QACX;AAEA,YAAI,sBAAsB,QAAQ,YAAY,UAAU,QAAQ,MAAM,mBAAmB;AACrF,gBAAM;AAAA,QACV;AAEA,cAAM,+BAA+B;AAAA,UACjC,mBAAmB;AAAA,UACnB,kBAAkB,YAAY,UAAU,QAAQ;AAAA,QACpD,CAAC;AAAA,MACL;AAEA,YAAM;AAAA,IACV;AAAA,EACJ;AACJ;AAEA,MAAM,0BAA0B,uBAAuB;AAAA,EACnD,kBAAkB,CAAC,OACf,OAAO,KAAK,WAAW;AAAA,IACnB,OAAO,EAAE,GAAG;AAAA,IACZ,QAAQ;AAAA,MACJ,OAAO;AAAA,MACP,SAAS;AAAA,MACT,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,OAAO;AAAA,MACP,QAAQ;AAAA,IACZ;AAAA,EACJ,CAAC;AAAA,EACL,iBAAiB,CAAC,OACd,OAAO,KAAK,WAAW;AAAA,IACnB,OAAO,EAAE,GAAG;AAAA,IACZ,QAAQ;AAAA,MACJ,WAAW;AAAA,IACf;AAAA,EACJ,CAAC;AAAA,EACL,iBAAiB;AAAA,EACjB,YAAY,CAAC,EAAE,OAAO,KAAK,MACvB,OAAO,KAAK,OAAO;AAAA,IACf;AAAA,IACA;AAAA,EACJ,CAAC;AAAA,EACL;AACJ,CAAC;AAEM,MAAM,6BAA6B,OAAO,UAA2C;AACxF,SAAO,wBAAwB,2BAA2B,KAAK;AACnE;","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":[]}