ocean-brain 0.4.1 → 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 (79) hide show
  1. package/package.json +3 -1
  2. package/server/client/dist/assets/{Calendar-D4KkvcU8.js → Calendar-BU9XgX2N.js} +1 -1
  3. package/server/client/dist/assets/Callout-C6tVCxOT.js +1 -0
  4. package/server/client/dist/assets/{Graph-C8pBdJzV.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-C8g5NwNt.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-Bkl1ZXE0.js → Plus.es-EcmtS_uw.js} +1 -1
  9. package/server/client/dist/assets/{Reminders-FrTR9ny8.js → Reminders-BlLLT_x0.js} +1 -1
  10. package/server/client/dist/assets/{Search-BPf2LO1Y.js → Search-CrZ0ap8x.js} +1 -1
  11. package/server/client/dist/assets/{SurfaceCard-WSk7zWeM.js → SurfaceCard-CO0oVxeL.js} +1 -1
  12. package/server/client/dist/assets/{Tag-uArdLjo1.js → Tag-vws5AfNW.js} +1 -1
  13. package/server/client/dist/assets/{TagNotes-DDvXHB9I.js → TagNotes-Fmf1Kgim.js} +1 -1
  14. package/server/client/dist/assets/{Trash.es-DumPfiUz.js → Trash.es-DmwfSy3l.js} +1 -1
  15. package/server/client/dist/assets/{ViewNotes-BVu9lEs-.js → ViewNotes-BALN7IfH.js} +1 -1
  16. package/server/client/dist/assets/{Views-D4LGh30P.js → Views-Ccc_Ml5U.js} +1 -1
  17. package/server/client/dist/assets/{image.api-D3Ei3fbf.js → image.api-DeF9ck7g.js} +1 -1
  18. package/server/client/dist/assets/{index-C7YGbIuA.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-BvKBTjQq.js +246 -0
  21. package/server/client/dist/assets/{manage-image-DiuREdeE.js → manage-image-IoTXaMBP.js} +1 -1
  22. package/server/client/dist/assets/{manage-image-detail-D8Eorq3u.js → manage-image-detail-BRTXwOsh.js} +1 -1
  23. package/server/client/dist/assets/{mcp-CvCGaYSz.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-znZDfNAE.js → placeholder-XQqo1Fvr.js} +3 -3
  27. package/server/client/dist/assets/{trash-R0sww0ur.js → trash-hiJIg6-c.js} +1 -1
  28. package/server/client/dist/assets/{useReminderMutate-Bkj-oIm4.js → useReminderMutate-D21PurIS.js} +1 -1
  29. package/server/client/dist/assets/{view-dashboard-ClpF_qLM.js → view-dashboard-BuaZ4MvI.js} +1 -1
  30. package/server/client/dist/index.html +4 -4
  31. package/server/dist/app.js +2 -2
  32. package/server/dist/app.js.map +1 -1
  33. package/server/dist/features/auth/http/login-page.js +11 -4
  34. package/server/dist/features/auth/http/login-page.js.map +1 -1
  35. package/server/dist/features/auth/http/pages.js +3 -2
  36. package/server/dist/features/auth/http/pages.js.map +1 -1
  37. package/server/dist/features/note/graphql/note.field.resolver.js +14 -1
  38. package/server/dist/features/note/graphql/note.field.resolver.js.map +1 -1
  39. package/server/dist/features/note/graphql/note.mutation.resolver.js +73 -36
  40. package/server/dist/features/note/graphql/note.mutation.resolver.js.map +1 -1
  41. package/server/dist/features/note/graphql/note.query.resolver.js +74 -76
  42. package/server/dist/features/note/graphql/note.query.resolver.js.map +1 -1
  43. package/server/dist/features/note/graphql/note.resolver.js +3 -2
  44. package/server/dist/features/note/graphql/note.resolver.js.map +1 -1
  45. package/server/dist/features/note/graphql/note.type-defs.js +4 -1
  46. package/server/dist/features/note/graphql/note.type-defs.js.map +1 -1
  47. package/server/dist/features/note/services/cleanup.js +4 -3
  48. package/server/dist/features/note/services/cleanup.js.map +1 -1
  49. package/server/dist/features/note/services/content-blocks.js +113 -0
  50. package/server/dist/features/note/services/content-blocks.js.map +1 -0
  51. package/server/dist/features/note/services/search.js +10 -29
  52. package/server/dist/features/note/services/search.js.map +1 -1
  53. package/server/dist/features/note/services/snapshot.js +172 -12
  54. package/server/dist/features/note/services/snapshot.js.map +1 -1
  55. package/server/dist/features/note/services/write-conflict.js +87 -0
  56. package/server/dist/features/note/services/write-conflict.js.map +1 -0
  57. package/server/dist/features/note/services/write.js +107 -0
  58. package/server/dist/features/note/services/write.js.map +1 -0
  59. package/server/dist/modules/auth-guard.js +18 -0
  60. package/server/dist/modules/auth-guard.js.map +1 -1
  61. package/server/dist/modules/blocknote-tree.js +41 -0
  62. package/server/dist/modules/blocknote-tree.js.map +1 -0
  63. package/server/dist/modules/error-handler.js +13 -3
  64. package/server/dist/modules/error-handler.js.map +1 -1
  65. package/server/dist/modules/rate-limit.js +18 -0
  66. package/server/dist/modules/rate-limit.js.map +1 -0
  67. package/server/dist/modules/recovery-retention.js +2 -2
  68. package/server/dist/modules/recovery-retention.js.map +1 -1
  69. package/server/dist/routes/api.js +2 -1
  70. package/server/dist/routes/api.js.map +1 -1
  71. package/server/dist/routes/auth-pages.js +2 -1
  72. package/server/dist/routes/auth-pages.js.map +1 -1
  73. package/server/client/dist/assets/Callout-CaSzc-1y.js +0 -1
  74. package/server/client/dist/assets/Note-ODOESft3.js +0 -21
  75. package/server/client/dist/assets/index-46ZUXbW5.js +0 -232
  76. package/server/client/dist/assets/note-core-GgFcksAF.js +0 -178
  77. package/server/client/dist/assets/note-vendor-B0t5F04o.js +0 -54
  78. package/server/dist/features/note/graphql/note.graphql.shared.js +0 -23
  79. package/server/dist/features/note/graphql/note.graphql.shared.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/features/note/services/content-blocks.ts"],"sourcesContent":["import { parseBlockNoteContent, walkBlockNoteTree } from '~/modules/blocknote-tree.js';\n\ninterface BlockNote<T = unknown> {\n id?: string;\n type?: string;\n props?: T;\n content?: unknown;\n children?: unknown;\n}\n\ninterface ReferenceProps {\n id?: string | number;\n title?: string;\n}\n\ninterface NoteGraphInput {\n id: string | number;\n title: string;\n content: string;\n}\n\nexport interface NoteGraphResult {\n nodes: Array<{ id: string; title: string; connections: number }>;\n links: Array<{ source: string; target: string }>;\n}\n\nexport const parseNoteContent = (content: string): unknown[] | null => {\n return parseBlockNoteContent(content);\n};\n\nexport const normalizeReferenceId = (id: unknown) => {\n if (typeof id === 'number') {\n return Number.isFinite(id) ? String(id) : null;\n }\n\n if (typeof id !== 'string') {\n return null;\n }\n\n const normalizedId = id.trim();\n\n return normalizedId.length > 0 ? normalizedId : null;\n};\n\nexport const extractBlocksByType = <T>(type: string, dataArray: unknown): BlockNote<T>[] => {\n const result: BlockNote<T>[] = [];\n\n walkBlockNoteTree(dataArray, (node) => {\n if (node.type === type) {\n result.push(node as BlockNote<T>);\n }\n });\n\n return result;\n};\n\nexport const extractReferenceBlocks = (dataArray: unknown) => {\n return extractBlocksByType<ReferenceProps>('reference', dataArray).filter((block) => {\n return normalizeReferenceId(block.props?.id) !== null;\n });\n};\n\nexport const extractReferenceBlocksFromContent = (content: string) => {\n const parsed = parseNoteContent(content);\n return parsed ? extractReferenceBlocks(parsed) : [];\n};\n\nexport const contentReferencesNote = (content: string, noteId: string | number) => {\n const targetId = normalizeReferenceId(noteId);\n\n if (!targetId) {\n return false;\n }\n\n return extractReferenceBlocksFromContent(content).some(\n (block) => normalizeReferenceId(block.props?.id) === targetId,\n );\n};\n\nexport const syncReferenceTitlesInContent = (content: string, titlesById: Map<string, string>) => {\n const parsed = parseNoteContent(content);\n\n if (!parsed) {\n return null;\n }\n\n let changed = false;\n\n for (const block of extractReferenceBlocks(parsed)) {\n const props = block.props;\n const id = normalizeReferenceId(props?.id);\n\n if (!props || id === null) {\n continue;\n }\n\n const title = titlesById.get(id);\n\n if (title !== undefined && props.title !== title) {\n block.props = {\n ...props,\n title,\n };\n changed = true;\n }\n }\n\n return changed ? JSON.stringify(parsed) : content;\n};\n\nexport const buildNoteGraph = (notes: NoteGraphInput[]): NoteGraphResult => {\n const existingNoteIds = new Set(notes.map((note) => String(note.id)));\n const links: NoteGraphResult['links'] = [];\n const connectionCount: Record<string, number> = {};\n const linkSet = new Set<string>();\n\n for (const note of notes) {\n const sourceId = String(note.id);\n\n for (const block of extractReferenceBlocksFromContent(note.content)) {\n const targetId = normalizeReferenceId(block.props?.id);\n\n if (!targetId || sourceId === targetId || !existingNoteIds.has(targetId)) {\n continue;\n }\n\n const linkKey = `${sourceId}-${targetId}`;\n const reverseLinkKey = `${targetId}-${sourceId}`;\n\n if (linkSet.has(linkKey) || linkSet.has(reverseLinkKey)) {\n continue;\n }\n\n linkSet.add(linkKey);\n links.push({\n source: sourceId,\n target: targetId,\n });\n connectionCount[sourceId] = (connectionCount[sourceId] || 0) + 1;\n connectionCount[targetId] = (connectionCount[targetId] || 0) + 1;\n }\n }\n\n return {\n nodes: notes.map((note) => {\n const id = String(note.id);\n\n return {\n id,\n title: note.title || 'Untitled',\n connections: connectionCount[id] || 0,\n };\n }),\n links,\n };\n};\n"],"mappings":"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","names":[]}
@@ -1,3 +1,4 @@
1
+ import { isBlockNoteTreeNode, walkBlockNoteTree } from "../../../modules/blocknote-tree.js";
1
2
  const NOTE_SEARCH_TEXT_SCHEMA_VERSION = 1;
2
3
  const FILE_BLOCK_VISIBLE_PROP_KEYS = ["name", "caption"];
3
4
  const NOTE_SEARCH_EXTRACTOR_NODE_TYPES = [
@@ -73,9 +74,6 @@ const isSearchExtractorNodeType = (type) => {
73
74
  };
74
75
  const normalizeSearchText = (value) => value.replace(/\s+/g, " ").trim();
75
76
  const normalizeSearchToken = (value) => normalizeSearchText(value).toLowerCase();
76
- const isRecord = (value) => {
77
- return typeof value === "object" && value !== null;
78
- };
79
77
  const pushVisibleSegment = (context, value) => {
80
78
  if (typeof value !== "string") {
81
79
  return;
@@ -87,7 +85,7 @@ const pushVisibleSegment = (context, value) => {
87
85
  context.segments.push(normalizedValue);
88
86
  };
89
87
  const pushVisibleProps = (context, props, keys) => {
90
- if (!isRecord(props)) {
88
+ if (!isBlockNoteTreeNode(props)) {
91
89
  return;
92
90
  }
93
91
  for (const key of keys) {
@@ -95,32 +93,15 @@ const pushVisibleProps = (context, props, keys) => {
95
93
  }
96
94
  };
97
95
  const collectVisibleSearchSegments = (node, context) => {
98
- if (Array.isArray(node)) {
99
- node.forEach((item) => collectVisibleSearchSegments(item, context));
100
- return;
101
- }
102
- if (!isRecord(node)) {
103
- return;
104
- }
105
- const type = typeof node.type === "string" ? node.type : void 0;
106
- if (type) {
107
- if (isSearchExtractorNodeType(type)) {
108
- SEARCH_NODE_EXTRACTORS[type](node, context);
96
+ walkBlockNoteTree(node, (item) => {
97
+ const type = typeof item.type === "string" ? item.type : void 0;
98
+ if (type) {
99
+ if (isSearchExtractorNodeType(type)) {
100
+ SEARCH_NODE_EXTRACTORS[type](item, context);
101
+ }
102
+ recordUnknownNoteSearchNodeType(type);
109
103
  }
110
- recordUnknownNoteSearchNodeType(type);
111
- }
112
- if ("content" in node) {
113
- collectVisibleSearchSegments(node.content, context);
114
- }
115
- if ("children" in node) {
116
- collectVisibleSearchSegments(node.children, context);
117
- }
118
- if ("rows" in node) {
119
- collectVisibleSearchSegments(node.rows, context);
120
- }
121
- if ("cells" in node) {
122
- collectVisibleSearchSegments(node.cells, context);
123
- }
104
+ });
124
105
  };
125
106
  const buildNoteSearchText = (note) => {
126
107
  const normalizedTitle = normalizeSearchText(note.title);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/features/note/services/search.ts"],"sourcesContent":["export interface NoteSearchQuery {\n included: string[];\n excluded: string[];\n hasFilters: boolean;\n}\n\ninterface SearchableNoteLike {\n title: string;\n content: string;\n searchableText?: string;\n searchableTextVersion?: number;\n}\n\nexport interface NoteSearchProjection {\n searchableText: string;\n searchableTextVersion: number;\n}\n\ninterface SearchProjectionContext {\n segments: string[];\n}\n\ntype SearchableJsonNode = Record<string, unknown>;\ntype SearchNodeExtractor = (node: SearchableJsonNode, context: SearchProjectionContext) => void;\ntype SearchExtractorNodeType = (typeof NOTE_SEARCH_EXTRACTOR_NODE_TYPES)[number];\n\n// If we later materialize searchable text into a DB column, bump this when the\n// extraction meaning changes so stored projections can be rebuilt safely.\nexport const NOTE_SEARCH_TEXT_SCHEMA_VERSION = 1;\n\nconst FILE_BLOCK_VISIBLE_PROP_KEYS = ['name', 'caption'] as const;\n\nexport const NOTE_SEARCH_EXTRACTOR_NODE_TYPES = [\n 'text',\n 'tag',\n 'reference',\n 'image',\n 'video',\n 'audio',\n 'file',\n] as const;\n\nexport const NOTE_SEARCH_PASS_THROUGH_NODE_TYPES = [\n 'link',\n 'paragraph',\n 'heading',\n 'bulletListItem',\n 'numberedListItem',\n 'checkListItem',\n 'toggleListItem',\n 'quote',\n 'codeBlock',\n 'divider',\n 'table',\n 'tableContent',\n 'tableCell',\n] as const;\n\nexport const NOTE_SEARCH_IGNORED_NODE_TYPES = ['tableOfContents'] as const;\n\nconst KNOWN_NOTE_SEARCH_NODE_TYPES = new Set<string>([\n ...NOTE_SEARCH_EXTRACTOR_NODE_TYPES,\n ...NOTE_SEARCH_PASS_THROUGH_NODE_TYPES,\n ...NOTE_SEARCH_IGNORED_NODE_TYPES,\n]);\nconst SEARCH_NODE_EXTRACTOR_TYPE_SET = new Set<string>(NOTE_SEARCH_EXTRACTOR_NODE_TYPES);\n\nconst unknownNoteSearchNodeTypeCounts = new Map<string, number>();\n\nconst SEARCH_NODE_EXTRACTORS: Record<SearchExtractorNodeType, SearchNodeExtractor> = {\n text: (node, context) => {\n pushVisibleSegment(context, node.text);\n },\n tag: (node, context) => {\n pushVisibleProps(context, node.props, ['tag']);\n },\n reference: (node, context) => {\n pushVisibleProps(context, node.props, ['title']);\n },\n image: (node, context) => {\n pushVisibleProps(context, node.props, FILE_BLOCK_VISIBLE_PROP_KEYS);\n },\n video: (node, context) => {\n pushVisibleProps(context, node.props, FILE_BLOCK_VISIBLE_PROP_KEYS);\n },\n audio: (node, context) => {\n pushVisibleProps(context, node.props, FILE_BLOCK_VISIBLE_PROP_KEYS);\n },\n file: (node, context) => {\n pushVisibleProps(context, node.props, FILE_BLOCK_VISIBLE_PROP_KEYS);\n },\n};\n\nconst recordUnknownNoteSearchNodeType = (type: string) => {\n if (KNOWN_NOTE_SEARCH_NODE_TYPES.has(type)) {\n return;\n }\n\n const nextCount = (unknownNoteSearchNodeTypeCounts.get(type) ?? 0) + 1;\n unknownNoteSearchNodeTypeCounts.set(type, nextCount);\n\n if (nextCount === 1) {\n process.emitWarning(\n `note-search encountered unsupported BlockNote node type \"${type}\". Only nested text will be indexed until explicit support is added.`,\n { code: 'OCEAN_BRAIN_NOTE_SEARCH_UNKNOWN_NODE' },\n );\n }\n};\n\nconst isSearchExtractorNodeType = (type: string): type is SearchExtractorNodeType => {\n return SEARCH_NODE_EXTRACTOR_TYPE_SET.has(type);\n};\n\nconst normalizeSearchText = (value: string) => value.replace(/\\s+/g, ' ').trim();\n\nconst normalizeSearchToken = (value: string) => normalizeSearchText(value).toLowerCase();\n\nconst isRecord = (value: unknown): value is SearchableJsonNode => {\n return typeof value === 'object' && value !== null;\n};\n\nconst pushVisibleSegment = (context: SearchProjectionContext, value: unknown) => {\n if (typeof value !== 'string') {\n return;\n }\n\n const normalizedValue = normalizeSearchText(value);\n\n if (!normalizedValue) {\n return;\n }\n\n context.segments.push(normalizedValue);\n};\n\nconst pushVisibleProps = (context: SearchProjectionContext, props: unknown, keys: readonly string[]) => {\n if (!isRecord(props)) {\n return;\n }\n\n for (const key of keys) {\n pushVisibleSegment(context, props[key]);\n }\n};\n\nconst collectVisibleSearchSegments = (node: unknown, context: SearchProjectionContext) => {\n if (Array.isArray(node)) {\n node.forEach((item) => collectVisibleSearchSegments(item, context));\n return;\n }\n\n if (!isRecord(node)) {\n return;\n }\n\n const type = typeof node.type === 'string' ? node.type : undefined;\n\n if (type) {\n if (isSearchExtractorNodeType(type)) {\n SEARCH_NODE_EXTRACTORS[type](node, context);\n }\n\n recordUnknownNoteSearchNodeType(type);\n }\n\n if ('content' in node) {\n collectVisibleSearchSegments(node.content, context);\n }\n\n if ('children' in node) {\n collectVisibleSearchSegments(node.children, context);\n }\n\n if ('rows' in node) {\n collectVisibleSearchSegments(node.rows, context);\n }\n\n if ('cells' in node) {\n collectVisibleSearchSegments(node.cells, context);\n }\n};\n\nexport const buildNoteSearchText = (note: Pick<SearchableNoteLike, 'title' | 'content'>) => {\n const normalizedTitle = normalizeSearchText(note.title);\n const normalizedContent = extractVisibleSearchTextFromContent(note.content);\n\n return normalizeSearchToken(`${normalizedTitle} ${normalizedContent}`);\n};\n\nexport const buildNoteSearchProjection = (\n note: Pick<SearchableNoteLike, 'title' | 'content'>,\n): NoteSearchProjection => {\n return {\n searchableText: buildNoteSearchText(note),\n searchableTextVersion: NOTE_SEARCH_TEXT_SCHEMA_VERSION,\n };\n};\n\nexport const parseNoteSearchQuery = (query: string): NoteSearchQuery => {\n const included: string[] = [];\n const excluded: string[] = [];\n\n for (const item of query\n .split(/\\s+/)\n .map((value) => value.trim())\n .filter(Boolean)) {\n if (item.startsWith('-') && item.length > 1) {\n excluded.push(normalizeSearchToken(item.slice(1)));\n continue;\n }\n\n included.push(normalizeSearchToken(item));\n }\n\n return {\n included,\n excluded,\n hasFilters: included.length > 0 || excluded.length > 0,\n };\n};\n\nexport const extractVisibleSearchTextFromContent = (content: string) => {\n try {\n const parsed = JSON.parse(content) as unknown;\n const context: SearchProjectionContext = { segments: [] };\n\n collectVisibleSearchSegments(parsed, context);\n\n return normalizeSearchText(context.segments.join(' '));\n } catch {\n return '';\n }\n};\n\nexport const matchesNoteSearchQuery = (note: SearchableNoteLike, query: string | NoteSearchQuery) => {\n const parsedQuery = typeof query === 'string' ? parseNoteSearchQuery(query) : query;\n\n if (!parsedQuery.hasFilters) {\n return true;\n }\n\n const haystack =\n note.searchableTextVersion === NOTE_SEARCH_TEXT_SCHEMA_VERSION && typeof note.searchableText === 'string'\n ? normalizeSearchToken(note.searchableText)\n : buildNoteSearchText(note);\n\n return (\n parsedQuery.included.every((term) => haystack.includes(term)) &&\n parsedQuery.excluded.every((term) => !haystack.includes(term))\n );\n};\n\nexport const filterNotesBySearchQuery = <T extends SearchableNoteLike>(notes: T[], query: string | NoteSearchQuery) => {\n const parsedQuery = typeof query === 'string' ? parseNoteSearchQuery(query) : query;\n\n if (!parsedQuery.hasFilters) {\n return notes;\n }\n\n return notes.filter((note) => matchesNoteSearchQuery(note, parsedQuery));\n};\n\nexport const getUnknownNoteSearchNodeTypeCounts = () => {\n return new Map(unknownNoteSearchNodeTypeCounts);\n};\n\nexport const resetUnknownNoteSearchNodeTypeCountsForTest = () => {\n unknownNoteSearchNodeTypeCounts.clear();\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/features/note/services/search.ts"],"sourcesContent":["import { isBlockNoteTreeNode, walkBlockNoteTree } from '~/modules/blocknote-tree.js';\n\nexport interface NoteSearchQuery {\n included: string[];\n excluded: string[];\n hasFilters: boolean;\n}\n\ninterface SearchableNoteLike {\n title: string;\n content: string;\n searchableText?: string;\n searchableTextVersion?: number;\n}\n\nexport interface NoteSearchProjection {\n searchableText: string;\n searchableTextVersion: number;\n}\n\ninterface SearchProjectionContext {\n segments: string[];\n}\n\ntype SearchableJsonNode = Record<string, unknown>;\ntype SearchNodeExtractor = (node: SearchableJsonNode, context: SearchProjectionContext) => void;\ntype SearchExtractorNodeType = (typeof NOTE_SEARCH_EXTRACTOR_NODE_TYPES)[number];\n\n// If we later materialize searchable text into a DB column, bump this when the\n// extraction meaning changes so stored projections can be rebuilt safely.\nexport const NOTE_SEARCH_TEXT_SCHEMA_VERSION = 1;\n\nconst FILE_BLOCK_VISIBLE_PROP_KEYS = ['name', 'caption'] as const;\n\nexport const NOTE_SEARCH_EXTRACTOR_NODE_TYPES = [\n 'text',\n 'tag',\n 'reference',\n 'image',\n 'video',\n 'audio',\n 'file',\n] as const;\n\nexport const NOTE_SEARCH_PASS_THROUGH_NODE_TYPES = [\n 'link',\n 'paragraph',\n 'heading',\n 'bulletListItem',\n 'numberedListItem',\n 'checkListItem',\n 'toggleListItem',\n 'quote',\n 'codeBlock',\n 'divider',\n 'table',\n 'tableContent',\n 'tableCell',\n] as const;\n\nexport const NOTE_SEARCH_IGNORED_NODE_TYPES = ['tableOfContents'] as const;\n\nconst KNOWN_NOTE_SEARCH_NODE_TYPES = new Set<string>([\n ...NOTE_SEARCH_EXTRACTOR_NODE_TYPES,\n ...NOTE_SEARCH_PASS_THROUGH_NODE_TYPES,\n ...NOTE_SEARCH_IGNORED_NODE_TYPES,\n]);\nconst SEARCH_NODE_EXTRACTOR_TYPE_SET = new Set<string>(NOTE_SEARCH_EXTRACTOR_NODE_TYPES);\n\nconst unknownNoteSearchNodeTypeCounts = new Map<string, number>();\n\nconst SEARCH_NODE_EXTRACTORS: Record<SearchExtractorNodeType, SearchNodeExtractor> = {\n text: (node, context) => {\n pushVisibleSegment(context, node.text);\n },\n tag: (node, context) => {\n pushVisibleProps(context, node.props, ['tag']);\n },\n reference: (node, context) => {\n pushVisibleProps(context, node.props, ['title']);\n },\n image: (node, context) => {\n pushVisibleProps(context, node.props, FILE_BLOCK_VISIBLE_PROP_KEYS);\n },\n video: (node, context) => {\n pushVisibleProps(context, node.props, FILE_BLOCK_VISIBLE_PROP_KEYS);\n },\n audio: (node, context) => {\n pushVisibleProps(context, node.props, FILE_BLOCK_VISIBLE_PROP_KEYS);\n },\n file: (node, context) => {\n pushVisibleProps(context, node.props, FILE_BLOCK_VISIBLE_PROP_KEYS);\n },\n};\n\nconst recordUnknownNoteSearchNodeType = (type: string) => {\n if (KNOWN_NOTE_SEARCH_NODE_TYPES.has(type)) {\n return;\n }\n\n const nextCount = (unknownNoteSearchNodeTypeCounts.get(type) ?? 0) + 1;\n unknownNoteSearchNodeTypeCounts.set(type, nextCount);\n\n if (nextCount === 1) {\n process.emitWarning(\n `note-search encountered unsupported BlockNote node type \"${type}\". Only nested text will be indexed until explicit support is added.`,\n { code: 'OCEAN_BRAIN_NOTE_SEARCH_UNKNOWN_NODE' },\n );\n }\n};\n\nconst isSearchExtractorNodeType = (type: string): type is SearchExtractorNodeType => {\n return SEARCH_NODE_EXTRACTOR_TYPE_SET.has(type);\n};\n\nconst normalizeSearchText = (value: string) => value.replace(/\\s+/g, ' ').trim();\n\nconst normalizeSearchToken = (value: string) => normalizeSearchText(value).toLowerCase();\n\nconst pushVisibleSegment = (context: SearchProjectionContext, value: unknown) => {\n if (typeof value !== 'string') {\n return;\n }\n\n const normalizedValue = normalizeSearchText(value);\n\n if (!normalizedValue) {\n return;\n }\n\n context.segments.push(normalizedValue);\n};\n\nconst pushVisibleProps = (context: SearchProjectionContext, props: unknown, keys: readonly string[]) => {\n if (!isBlockNoteTreeNode(props)) {\n return;\n }\n\n for (const key of keys) {\n pushVisibleSegment(context, props[key]);\n }\n};\n\nconst collectVisibleSearchSegments = (node: unknown, context: SearchProjectionContext) => {\n walkBlockNoteTree(node, (item) => {\n const type = typeof item.type === 'string' ? item.type : undefined;\n\n if (type) {\n if (isSearchExtractorNodeType(type)) {\n SEARCH_NODE_EXTRACTORS[type](item, context);\n }\n\n recordUnknownNoteSearchNodeType(type);\n }\n });\n};\n\nexport const buildNoteSearchText = (note: Pick<SearchableNoteLike, 'title' | 'content'>) => {\n const normalizedTitle = normalizeSearchText(note.title);\n const normalizedContent = extractVisibleSearchTextFromContent(note.content);\n\n return normalizeSearchToken(`${normalizedTitle} ${normalizedContent}`);\n};\n\nexport const buildNoteSearchProjection = (\n note: Pick<SearchableNoteLike, 'title' | 'content'>,\n): NoteSearchProjection => {\n return {\n searchableText: buildNoteSearchText(note),\n searchableTextVersion: NOTE_SEARCH_TEXT_SCHEMA_VERSION,\n };\n};\n\nexport const parseNoteSearchQuery = (query: string): NoteSearchQuery => {\n const included: string[] = [];\n const excluded: string[] = [];\n\n for (const item of query\n .split(/\\s+/)\n .map((value) => value.trim())\n .filter(Boolean)) {\n if (item.startsWith('-') && item.length > 1) {\n excluded.push(normalizeSearchToken(item.slice(1)));\n continue;\n }\n\n included.push(normalizeSearchToken(item));\n }\n\n return {\n included,\n excluded,\n hasFilters: included.length > 0 || excluded.length > 0,\n };\n};\n\nexport const extractVisibleSearchTextFromContent = (content: string) => {\n try {\n const parsed = JSON.parse(content) as unknown;\n const context: SearchProjectionContext = { segments: [] };\n\n collectVisibleSearchSegments(parsed, context);\n\n return normalizeSearchText(context.segments.join(' '));\n } catch {\n return '';\n }\n};\n\nexport const matchesNoteSearchQuery = (note: SearchableNoteLike, query: string | NoteSearchQuery) => {\n const parsedQuery = typeof query === 'string' ? parseNoteSearchQuery(query) : query;\n\n if (!parsedQuery.hasFilters) {\n return true;\n }\n\n const haystack =\n note.searchableTextVersion === NOTE_SEARCH_TEXT_SCHEMA_VERSION && typeof note.searchableText === 'string'\n ? normalizeSearchToken(note.searchableText)\n : buildNoteSearchText(note);\n\n return (\n parsedQuery.included.every((term) => haystack.includes(term)) &&\n parsedQuery.excluded.every((term) => !haystack.includes(term))\n );\n};\n\nexport const filterNotesBySearchQuery = <T extends SearchableNoteLike>(notes: T[], query: string | NoteSearchQuery) => {\n const parsedQuery = typeof query === 'string' ? parseNoteSearchQuery(query) : query;\n\n if (!parsedQuery.hasFilters) {\n return notes;\n }\n\n return notes.filter((note) => matchesNoteSearchQuery(note, parsedQuery));\n};\n\nexport const getUnknownNoteSearchNodeTypeCounts = () => {\n return new Map(unknownNoteSearchNodeTypeCounts);\n};\n\nexport const resetUnknownNoteSearchNodeTypeCountsForTest = () => {\n unknownNoteSearchNodeTypeCounts.clear();\n};\n"],"mappings":"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","names":[]}
@@ -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":[]}