ocean-brain 0.3.3 → 0.4.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 (253) hide show
  1. package/dist/mcp.js +151 -37
  2. package/package.json +5 -5
  3. package/scripts/postinstall-prisma.mjs +3 -2
  4. package/server/client/dist/assets/Calendar-CSXEuWmQ.js +25 -0
  5. package/server/client/dist/assets/Callout-Ap1OcxWN.js +1 -0
  6. package/server/client/dist/assets/Graph-C0Ax9qjI.js +1 -0
  7. package/server/client/dist/assets/{Image-ZxF459sC.js → Image-CSbcG-5a.js} +1 -1
  8. package/server/client/dist/assets/{Image.es-CFTbON7k.js → Image.es-Fbe7aDDo.js} +1 -1
  9. package/server/client/dist/assets/{ModalActionRow-Hg6MY61u.js → ModalActionRow-DjI5b4bl.js} +1 -1
  10. package/server/client/dist/assets/Note-BY6WgYLq.js +1 -0
  11. package/server/client/dist/assets/Note-W4f6H9K0.css +1 -0
  12. package/server/client/dist/assets/Reminders-vRksvSQE.js +1 -0
  13. package/server/client/dist/assets/Search-Cq4tuiJO.js +1 -0
  14. package/server/client/dist/assets/{SurfaceCard-BV6y4bCo.js → SurfaceCard-Br6TjX5W.js} +1 -1
  15. package/server/client/dist/assets/Tag-ARDLA-Pb.js +1 -0
  16. package/server/client/dist/assets/TagNotes-CDGVrCJU.js +1 -0
  17. package/server/client/dist/assets/{Trash.es-CPtRLE0y.js → Trash.es-BfCEiL3-.js} +1 -1
  18. package/server/client/dist/assets/ViewNotes-ZBOQ1adH.js +1 -0
  19. package/server/client/dist/assets/Views-DQHPfdwM.js +1 -0
  20. package/server/client/dist/assets/{image.api-Dsrx9S-G.js → image.api-DGXlQ_Ca.js} +1 -1
  21. package/server/client/dist/assets/index-B28TO4FC.js +1 -0
  22. package/server/client/dist/assets/{index-CD2bFEqK.css → index-Bm9l_L20.css} +1 -1
  23. package/server/client/dist/assets/index-Bsx4P7EA.js +210 -0
  24. package/server/client/dist/assets/manage-image-Dg297A_h.js +1 -0
  25. package/server/client/dist/assets/manage-image-detail-hk6jWyiB.js +1 -0
  26. package/server/client/dist/assets/mcp-CSjmBjFR.js +1 -0
  27. package/server/client/dist/assets/{note-core-BCgMq5QA.js → note-core-GgFcksAF.js} +1 -1
  28. package/server/client/dist/assets/{note-vendor-BofYbzmZ.js → note-vendor-5N5mRkIT.js} +3 -3
  29. package/server/client/dist/assets/{placeholder-MNsco0AT.js → placeholder-Bmiowo28.js} +4 -4
  30. package/server/client/dist/assets/trash-CM7SbVtW.js +1 -0
  31. package/server/client/dist/assets/{useReminderMutate-yJak8FzP.js → useReminderMutate-DsIvVpCQ.js} +2 -2
  32. package/server/client/dist/assets/view-dashboard-CP8nhfxQ.js +90 -0
  33. package/server/client/dist/index.html +4 -4
  34. package/server/dist/app.js +8 -84
  35. package/server/dist/app.js.map +1 -1
  36. package/server/dist/features/auth/http/api.js +43 -0
  37. package/server/dist/features/auth/http/api.js.map +1 -0
  38. package/server/dist/features/auth/http/login-page.js +28 -0
  39. package/server/dist/features/auth/http/login-page.js.map +1 -0
  40. package/server/dist/features/auth/http/pages.js +50 -0
  41. package/server/dist/features/auth/http/pages.js.map +1 -0
  42. package/server/dist/features/auth/service.js +94 -0
  43. package/server/dist/features/auth/service.js.map +1 -0
  44. package/server/dist/features/cache/graphql/cache.field.resolver.js +5 -0
  45. package/server/dist/features/cache/graphql/cache.field.resolver.js.map +1 -0
  46. package/server/dist/features/cache/graphql/cache.mutation.resolver.js +49 -0
  47. package/server/dist/features/cache/graphql/cache.mutation.resolver.js.map +1 -0
  48. package/server/dist/features/cache/graphql/cache.query.resolver.js +21 -0
  49. package/server/dist/features/cache/graphql/cache.query.resolver.js.map +1 -0
  50. package/server/dist/features/cache/graphql/cache.resolver.js +12 -0
  51. package/server/dist/features/cache/graphql/cache.resolver.js.map +1 -0
  52. package/server/dist/features/cache/graphql/cache.type-defs.js +32 -0
  53. package/server/dist/features/cache/graphql/cache.type-defs.js.map +1 -0
  54. package/server/dist/features/cache/graphql/index.js +21 -0
  55. package/server/dist/features/cache/graphql/index.js.map +1 -0
  56. package/server/dist/features/image/graphql/image.field.resolver.js +14 -0
  57. package/server/dist/features/image/graphql/image.field.resolver.js.map +1 -0
  58. package/server/dist/features/image/graphql/image.mutation.resolver.js +21 -0
  59. package/server/dist/features/image/graphql/image.mutation.resolver.js.map +1 -0
  60. package/server/dist/features/image/graphql/image.query.resolver.js +40 -0
  61. package/server/dist/features/image/graphql/image.query.resolver.js.map +1 -0
  62. package/server/dist/features/image/graphql/image.resolver.js +12 -0
  63. package/server/dist/features/image/graphql/image.resolver.js.map +1 -0
  64. package/server/dist/features/image/graphql/image.type-defs.js +31 -0
  65. package/server/dist/features/image/graphql/image.type-defs.js.map +1 -0
  66. package/server/dist/features/image/graphql/index.js +16 -0
  67. package/server/dist/features/image/graphql/index.js.map +1 -0
  68. package/server/dist/features/image/http/upload.js +48 -0
  69. package/server/dist/features/image/http/upload.js.map +1 -0
  70. package/server/dist/features/image/services/delete.js +52 -0
  71. package/server/dist/features/image/services/delete.js.map +1 -0
  72. package/server/dist/{modules/remote-image.js → features/image/services/remote-fetch.js} +1 -1
  73. package/server/dist/features/image/services/remote-fetch.js.map +1 -0
  74. package/server/dist/{modules/image-upload.js → features/image/services/upload.js} +4 -8
  75. package/server/dist/features/image/services/upload.js.map +1 -0
  76. package/server/dist/{views/mcp-admin.js → features/mcp-admin/http/handlers.js} +3 -3
  77. package/server/dist/features/mcp-admin/http/handlers.js.map +1 -0
  78. package/server/dist/{modules/mcp-admin.js → features/mcp-admin/service.js} +3 -3
  79. package/server/dist/features/mcp-admin/service.js.map +1 -0
  80. package/server/dist/features/note/graphql/index.js +8 -0
  81. package/server/dist/features/note/graphql/index.js.map +1 -0
  82. package/server/dist/features/note/graphql/note.field.resolver.js +12 -0
  83. package/server/dist/features/note/graphql/note.field.resolver.js.map +1 -0
  84. package/server/dist/features/note/graphql/note.graphql.shared.js +23 -0
  85. package/server/dist/features/note/graphql/note.graphql.shared.js.map +1 -0
  86. package/server/dist/features/note/graphql/note.mutation.resolver.js +140 -0
  87. package/server/dist/features/note/graphql/note.mutation.resolver.js.map +1 -0
  88. package/server/dist/features/note/graphql/note.query.resolver.js +382 -0
  89. package/server/dist/features/note/graphql/note.query.resolver.js.map +1 -0
  90. package/server/dist/features/note/graphql/note.resolver.js +14 -0
  91. package/server/dist/features/note/graphql/note.resolver.js.map +1 -0
  92. package/server/dist/features/note/graphql/note.type-defs.js +184 -0
  93. package/server/dist/features/note/graphql/note.type-defs.js.map +1 -0
  94. package/server/dist/{views/note.js → features/note/http/mcp.js} +27 -15
  95. package/server/dist/features/note/http/mcp.js.map +1 -0
  96. package/server/dist/{modules/note-authoring.js → features/note/services/authoring.js} +25 -7
  97. package/server/dist/features/note/services/authoring.js.map +1 -0
  98. package/server/dist/{modules/note-cleanup.js → features/note/services/cleanup.js} +14 -23
  99. package/server/dist/features/note/services/cleanup.js.map +1 -0
  100. package/server/dist/features/note/services/search-backfill.js +84 -0
  101. package/server/dist/features/note/services/search-backfill.js.map +1 -0
  102. package/server/dist/features/note/services/search.js +197 -0
  103. package/server/dist/features/note/services/search.js.map +1 -0
  104. package/server/dist/{modules/note-snapshot.js → features/note/services/snapshot.js} +51 -49
  105. package/server/dist/features/note/services/snapshot.js.map +1 -0
  106. package/server/dist/features/note/services/tag-filter.js +18 -0
  107. package/server/dist/features/note/services/tag-filter.js.map +1 -0
  108. package/server/dist/{modules/note-trash.js → features/note/services/trash.js} +28 -23
  109. package/server/dist/features/note/services/trash.js.map +1 -0
  110. package/server/dist/features/placeholder/graphql/index.js +27 -0
  111. package/server/dist/features/placeholder/graphql/index.js.map +1 -0
  112. package/server/dist/features/placeholder/graphql/placeholder.field.resolver.js +5 -0
  113. package/server/dist/features/placeholder/graphql/placeholder.field.resolver.js.map +1 -0
  114. package/server/dist/features/placeholder/graphql/placeholder.mutation.resolver.js +72 -0
  115. package/server/dist/features/placeholder/graphql/placeholder.mutation.resolver.js.map +1 -0
  116. package/server/dist/features/placeholder/graphql/placeholder.query.resolver.js +36 -0
  117. package/server/dist/features/placeholder/graphql/placeholder.query.resolver.js.map +1 -0
  118. package/server/dist/features/placeholder/graphql/placeholder.resolver.js +12 -0
  119. package/server/dist/features/placeholder/graphql/placeholder.resolver.js.map +1 -0
  120. package/server/dist/features/placeholder/graphql/placeholder.type-defs.js +50 -0
  121. package/server/dist/features/placeholder/graphql/placeholder.type-defs.js.map +1 -0
  122. package/server/dist/features/reminder/graphql/index.js +27 -0
  123. package/server/dist/features/reminder/graphql/index.js.map +1 -0
  124. package/server/dist/features/reminder/graphql/reminder.field.resolver.js +10 -0
  125. package/server/dist/features/reminder/graphql/reminder.field.resolver.js.map +1 -0
  126. package/server/dist/features/reminder/graphql/reminder.mutation.resolver.js +59 -0
  127. package/server/dist/features/reminder/graphql/reminder.mutation.resolver.js.map +1 -0
  128. package/server/dist/features/reminder/graphql/reminder.query.resolver.js +79 -0
  129. package/server/dist/features/reminder/graphql/reminder.query.resolver.js.map +1 -0
  130. package/server/dist/features/reminder/graphql/reminder.resolver.js +12 -0
  131. package/server/dist/features/reminder/graphql/reminder.resolver.js.map +1 -0
  132. package/server/dist/features/reminder/graphql/reminder.type-defs.js +51 -0
  133. package/server/dist/features/reminder/graphql/reminder.type-defs.js.map +1 -0
  134. package/server/dist/features/tag/graphql/index.js +14 -0
  135. package/server/dist/features/tag/graphql/index.js.map +1 -0
  136. package/server/dist/features/tag/graphql/tag.field.resolver.js +10 -0
  137. package/server/dist/features/tag/graphql/tag.field.resolver.js.map +1 -0
  138. package/server/dist/features/tag/graphql/tag.mutation.resolver.js +15 -0
  139. package/server/dist/features/tag/graphql/tag.mutation.resolver.js.map +1 -0
  140. package/server/dist/features/tag/graphql/tag.query.resolver.js +26 -0
  141. package/server/dist/features/tag/graphql/tag.query.resolver.js.map +1 -0
  142. package/server/dist/features/tag/graphql/tag.resolver.js +12 -0
  143. package/server/dist/features/tag/graphql/tag.resolver.js.map +1 -0
  144. package/server/dist/features/tag/graphql/tag.type-defs.js +46 -0
  145. package/server/dist/features/tag/graphql/tag.type-defs.js.map +1 -0
  146. package/server/dist/{views/tag.js → features/tag/http/mcp.js} +3 -3
  147. package/server/dist/features/tag/http/mcp.js.map +1 -0
  148. package/server/dist/{modules/tag-organization.js → features/tag/services/organization.js} +38 -8
  149. package/server/dist/features/tag/services/organization.js.map +1 -0
  150. package/server/dist/features/view/graphql/index.js +11 -0
  151. package/server/dist/features/view/graphql/index.js.map +1 -0
  152. package/server/dist/features/view/graphql/view.mutation.resolver.js +53 -0
  153. package/server/dist/features/view/graphql/view.mutation.resolver.js.map +1 -0
  154. package/server/dist/features/view/graphql/view.query.resolver.js +29 -0
  155. package/server/dist/features/view/graphql/view.query.resolver.js.map +1 -0
  156. package/server/dist/features/view/graphql/view.resolver.js +10 -0
  157. package/server/dist/features/view/graphql/view.resolver.js.map +1 -0
  158. package/server/dist/features/view/graphql/view.type-defs.js +63 -0
  159. package/server/dist/features/view/graphql/view.type-defs.js.map +1 -0
  160. package/server/dist/features/view/services/workspace.js +432 -0
  161. package/server/dist/features/view/services/workspace.js.map +1 -0
  162. package/server/dist/main.js +14 -0
  163. package/server/dist/main.js.map +1 -1
  164. package/server/dist/modules/auth-guard.js +10 -9
  165. package/server/dist/modules/auth-guard.js.map +1 -1
  166. package/server/dist/modules/auth-mode.js +1 -3
  167. package/server/dist/modules/auth-mode.js.map +1 -1
  168. package/server/dist/modules/blocknote.js +96 -33
  169. package/server/dist/modules/blocknote.js.map +1 -1
  170. package/server/dist/modules/data-maintenance.js +75 -0
  171. package/server/dist/modules/data-maintenance.js.map +1 -0
  172. package/server/dist/modules/error-handler.js.map +1 -1
  173. package/server/dist/modules/logger.js +5 -23
  174. package/server/dist/modules/logger.js.map +1 -1
  175. package/server/dist/modules/mcp-auth.js +4 -5
  176. package/server/dist/modules/mcp-auth.js.map +1 -1
  177. package/server/dist/modules/mcp-token.js.map +1 -1
  178. package/server/dist/modules/prisma-runtime.js +2 -7
  179. package/server/dist/modules/prisma-runtime.js.map +1 -1
  180. package/server/dist/modules/recovery-retention.js +1 -1
  181. package/server/dist/modules/recovery-retention.js.map +1 -1
  182. package/server/dist/modules/server-events-handler.js +34 -0
  183. package/server/dist/modules/server-events-handler.js.map +1 -0
  184. package/server/dist/modules/server-events.js +32 -0
  185. package/server/dist/modules/server-events.js.map +1 -0
  186. package/server/dist/modules/use-async.js.map +1 -1
  187. package/server/dist/paths.js.map +1 -1
  188. package/server/dist/routes/api.js +34 -0
  189. package/server/dist/routes/api.js.map +1 -0
  190. package/server/dist/routes/auth-pages.js +11 -0
  191. package/server/dist/routes/auth-pages.js.map +1 -0
  192. package/server/dist/routes/client.js +27 -0
  193. package/server/dist/routes/client.js.map +1 -0
  194. package/server/dist/routes/graphql.js +33 -0
  195. package/server/dist/routes/graphql.js.map +1 -0
  196. package/server/dist/routes/index.js +11 -0
  197. package/server/dist/routes/index.js.map +1 -0
  198. package/server/dist/routes/mcp.js +30 -0
  199. package/server/dist/routes/mcp.js.map +1 -0
  200. package/server/dist/schema/index.js +11 -8
  201. package/server/dist/schema/index.js.map +1 -1
  202. package/server/dist/types/index.js.map +1 -1
  203. package/server/prisma/migrations/20260413143000_0014_tag_name_unique/migration.sql +32 -0
  204. package/server/prisma/migrations/20260414143000_0015_note_searchable_text/migration.sql +5 -0
  205. package/server/prisma/migrations/20260416090000_0016_view_workspace/migration.sql +50 -0
  206. package/server/prisma/schema.prisma +66 -12
  207. package/server/client/dist/assets/Calendar-DyXzs5M3.js +0 -25
  208. package/server/client/dist/assets/Graph-C8K4PJOn.js +0 -1
  209. package/server/client/dist/assets/Note-BbgZvrC3.js +0 -1
  210. package/server/client/dist/assets/Note-PEevDKBh.css +0 -1
  211. package/server/client/dist/assets/Reminders-jFhljND4.js +0 -1
  212. package/server/client/dist/assets/Search-X6QIR_m2.js +0 -1
  213. package/server/client/dist/assets/Tag-Ba2Dw4gH.js +0 -1
  214. package/server/client/dist/assets/TagNotes-BRJv6sDI.js +0 -1
  215. package/server/client/dist/assets/index-BglnLSet.js +0 -205
  216. package/server/client/dist/assets/index-DpqKngJz.js +0 -1
  217. package/server/client/dist/assets/manage-image-C6Y_0M5H.js +0 -1
  218. package/server/client/dist/assets/manage-image-detail-Dv3xofpe.js +0 -1
  219. package/server/client/dist/assets/mcp-BijQkpfY.js +0 -1
  220. package/server/client/dist/assets/trash-CiFJm_PK.js +0 -1
  221. package/server/dist/modules/auth.js +0 -25
  222. package/server/dist/modules/auth.js.map +0 -1
  223. package/server/dist/modules/image-upload.js.map +0 -1
  224. package/server/dist/modules/mcp-admin.js.map +0 -1
  225. package/server/dist/modules/note-authoring.js.map +0 -1
  226. package/server/dist/modules/note-cleanup.js.map +0 -1
  227. package/server/dist/modules/note-snapshot.js.map +0 -1
  228. package/server/dist/modules/note-trash.js.map +0 -1
  229. package/server/dist/modules/remote-image.js.map +0 -1
  230. package/server/dist/modules/tag-organization.js.map +0 -1
  231. package/server/dist/schema/cache/index.js +0 -58
  232. package/server/dist/schema/cache/index.js.map +0 -1
  233. package/server/dist/schema/image/index.js +0 -88
  234. package/server/dist/schema/image/index.js.map +0 -1
  235. package/server/dist/schema/note/index.js +0 -551
  236. package/server/dist/schema/note/index.js.map +0 -1
  237. package/server/dist/schema/placeholder/index.js +0 -105
  238. package/server/dist/schema/placeholder/index.js.map +0 -1
  239. package/server/dist/schema/reminder/index.js +0 -161
  240. package/server/dist/schema/reminder/index.js.map +0 -1
  241. package/server/dist/schema/tag/index.js +0 -81
  242. package/server/dist/schema/tag/index.js.map +0 -1
  243. package/server/dist/urls.js +0 -9
  244. package/server/dist/urls.js.map +0 -1
  245. package/server/dist/views/auth.js +0 -305
  246. package/server/dist/views/auth.js.map +0 -1
  247. package/server/dist/views/image.js +0 -44
  248. package/server/dist/views/image.js.map +0 -1
  249. package/server/dist/views/index.js +0 -6
  250. package/server/dist/views/index.js.map +0 -1
  251. package/server/dist/views/mcp-admin.js.map +0 -1
  252. package/server/dist/views/note.js.map +0 -1
  253. package/server/dist/views/tag.js.map +0 -1
@@ -0,0 +1,197 @@
1
+ const NOTE_SEARCH_TEXT_SCHEMA_VERSION = 1;
2
+ const FILE_BLOCK_VISIBLE_PROP_KEYS = ["name", "caption"];
3
+ const NOTE_SEARCH_EXTRACTOR_NODE_TYPES = [
4
+ "text",
5
+ "tag",
6
+ "reference",
7
+ "image",
8
+ "video",
9
+ "audio",
10
+ "file"
11
+ ];
12
+ const NOTE_SEARCH_PASS_THROUGH_NODE_TYPES = [
13
+ "link",
14
+ "paragraph",
15
+ "heading",
16
+ "bulletListItem",
17
+ "numberedListItem",
18
+ "checkListItem",
19
+ "toggleListItem",
20
+ "quote",
21
+ "codeBlock",
22
+ "divider",
23
+ "table",
24
+ "tableContent",
25
+ "tableCell"
26
+ ];
27
+ const NOTE_SEARCH_IGNORED_NODE_TYPES = ["tableOfContents"];
28
+ const KNOWN_NOTE_SEARCH_NODE_TYPES = /* @__PURE__ */ new Set([
29
+ ...NOTE_SEARCH_EXTRACTOR_NODE_TYPES,
30
+ ...NOTE_SEARCH_PASS_THROUGH_NODE_TYPES,
31
+ ...NOTE_SEARCH_IGNORED_NODE_TYPES
32
+ ]);
33
+ const SEARCH_NODE_EXTRACTOR_TYPE_SET = new Set(NOTE_SEARCH_EXTRACTOR_NODE_TYPES);
34
+ const unknownNoteSearchNodeTypeCounts = /* @__PURE__ */ new Map();
35
+ const SEARCH_NODE_EXTRACTORS = {
36
+ text: (node, context) => {
37
+ pushVisibleSegment(context, node.text);
38
+ },
39
+ tag: (node, context) => {
40
+ pushVisibleProps(context, node.props, ["tag"]);
41
+ },
42
+ reference: (node, context) => {
43
+ pushVisibleProps(context, node.props, ["title"]);
44
+ },
45
+ image: (node, context) => {
46
+ pushVisibleProps(context, node.props, FILE_BLOCK_VISIBLE_PROP_KEYS);
47
+ },
48
+ video: (node, context) => {
49
+ pushVisibleProps(context, node.props, FILE_BLOCK_VISIBLE_PROP_KEYS);
50
+ },
51
+ audio: (node, context) => {
52
+ pushVisibleProps(context, node.props, FILE_BLOCK_VISIBLE_PROP_KEYS);
53
+ },
54
+ file: (node, context) => {
55
+ pushVisibleProps(context, node.props, FILE_BLOCK_VISIBLE_PROP_KEYS);
56
+ }
57
+ };
58
+ const recordUnknownNoteSearchNodeType = (type) => {
59
+ if (KNOWN_NOTE_SEARCH_NODE_TYPES.has(type)) {
60
+ return;
61
+ }
62
+ const nextCount = (unknownNoteSearchNodeTypeCounts.get(type) ?? 0) + 1;
63
+ unknownNoteSearchNodeTypeCounts.set(type, nextCount);
64
+ if (nextCount === 1) {
65
+ process.emitWarning(
66
+ `note-search encountered unsupported BlockNote node type "${type}". Only nested text will be indexed until explicit support is added.`,
67
+ { code: "OCEAN_BRAIN_NOTE_SEARCH_UNKNOWN_NODE" }
68
+ );
69
+ }
70
+ };
71
+ const isSearchExtractorNodeType = (type) => {
72
+ return SEARCH_NODE_EXTRACTOR_TYPE_SET.has(type);
73
+ };
74
+ const normalizeSearchText = (value) => value.replace(/\s+/g, " ").trim();
75
+ const normalizeSearchToken = (value) => normalizeSearchText(value).toLowerCase();
76
+ const isRecord = (value) => {
77
+ return typeof value === "object" && value !== null;
78
+ };
79
+ const pushVisibleSegment = (context, value) => {
80
+ if (typeof value !== "string") {
81
+ return;
82
+ }
83
+ const normalizedValue = normalizeSearchText(value);
84
+ if (!normalizedValue) {
85
+ return;
86
+ }
87
+ context.segments.push(normalizedValue);
88
+ };
89
+ const pushVisibleProps = (context, props, keys) => {
90
+ if (!isRecord(props)) {
91
+ return;
92
+ }
93
+ for (const key of keys) {
94
+ pushVisibleSegment(context, props[key]);
95
+ }
96
+ };
97
+ 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);
109
+ }
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
+ }
124
+ };
125
+ const buildNoteSearchText = (note) => {
126
+ const normalizedTitle = normalizeSearchText(note.title);
127
+ const normalizedContent = extractVisibleSearchTextFromContent(note.content);
128
+ return normalizeSearchToken(`${normalizedTitle} ${normalizedContent}`);
129
+ };
130
+ const buildNoteSearchProjection = (note) => {
131
+ return {
132
+ searchableText: buildNoteSearchText(note),
133
+ searchableTextVersion: NOTE_SEARCH_TEXT_SCHEMA_VERSION
134
+ };
135
+ };
136
+ const parseNoteSearchQuery = (query) => {
137
+ const included = [];
138
+ const excluded = [];
139
+ for (const item of query.split(/\s+/).map((value) => value.trim()).filter(Boolean)) {
140
+ if (item.startsWith("-") && item.length > 1) {
141
+ excluded.push(normalizeSearchToken(item.slice(1)));
142
+ continue;
143
+ }
144
+ included.push(normalizeSearchToken(item));
145
+ }
146
+ return {
147
+ included,
148
+ excluded,
149
+ hasFilters: included.length > 0 || excluded.length > 0
150
+ };
151
+ };
152
+ const extractVisibleSearchTextFromContent = (content) => {
153
+ try {
154
+ const parsed = JSON.parse(content);
155
+ const context = { segments: [] };
156
+ collectVisibleSearchSegments(parsed, context);
157
+ return normalizeSearchText(context.segments.join(" "));
158
+ } catch {
159
+ return "";
160
+ }
161
+ };
162
+ const matchesNoteSearchQuery = (note, query) => {
163
+ const parsedQuery = typeof query === "string" ? parseNoteSearchQuery(query) : query;
164
+ if (!parsedQuery.hasFilters) {
165
+ return true;
166
+ }
167
+ const haystack = note.searchableTextVersion === NOTE_SEARCH_TEXT_SCHEMA_VERSION && typeof note.searchableText === "string" ? normalizeSearchToken(note.searchableText) : buildNoteSearchText(note);
168
+ return parsedQuery.included.every((term) => haystack.includes(term)) && parsedQuery.excluded.every((term) => !haystack.includes(term));
169
+ };
170
+ const filterNotesBySearchQuery = (notes, query) => {
171
+ const parsedQuery = typeof query === "string" ? parseNoteSearchQuery(query) : query;
172
+ if (!parsedQuery.hasFilters) {
173
+ return notes;
174
+ }
175
+ return notes.filter((note) => matchesNoteSearchQuery(note, parsedQuery));
176
+ };
177
+ const getUnknownNoteSearchNodeTypeCounts = () => {
178
+ return new Map(unknownNoteSearchNodeTypeCounts);
179
+ };
180
+ const resetUnknownNoteSearchNodeTypeCountsForTest = () => {
181
+ unknownNoteSearchNodeTypeCounts.clear();
182
+ };
183
+ export {
184
+ NOTE_SEARCH_EXTRACTOR_NODE_TYPES,
185
+ NOTE_SEARCH_IGNORED_NODE_TYPES,
186
+ NOTE_SEARCH_PASS_THROUGH_NODE_TYPES,
187
+ NOTE_SEARCH_TEXT_SCHEMA_VERSION,
188
+ buildNoteSearchProjection,
189
+ buildNoteSearchText,
190
+ extractVisibleSearchTextFromContent,
191
+ filterNotesBySearchQuery,
192
+ getUnknownNoteSearchNodeTypeCounts,
193
+ matchesNoteSearchQuery,
194
+ parseNoteSearchQuery,
195
+ resetUnknownNoteSearchNodeTypeCountsForTest
196
+ };
197
+ //# sourceMappingURL=search.js.map
@@ -0,0 +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,10 +1,11 @@
1
- import models from "../models.js";
1
+ import models from "../../../models.js";
2
2
  import {
3
3
  createRetentionCutoff,
4
4
  RECOVERY_CLEANUP_BATCH_LIMIT,
5
5
  SNAPSHOT_MAX_PER_NOTE,
6
6
  SNAPSHOT_RETENTION_DAYS
7
- } from "./recovery-retention.js";
7
+ } from "../../../modules/recovery-retention.js";
8
+ import { buildNoteSearchProjection } from "./search.js";
8
9
  const parseMeta = (value) => {
9
10
  if (!value) {
10
11
  return {};
@@ -35,6 +36,9 @@ const serializePayload = (note) => {
35
36
  };
36
37
  return JSON.stringify(payload);
37
38
  };
39
+ const hasSameSnapshotPayload = (snapshot, payload) => {
40
+ return snapshot?.payload === payload;
41
+ };
38
42
  const parsePayload = (payload) => {
39
43
  const parsed = JSON.parse(payload);
40
44
  if (typeof parsed.title !== "string" || typeof parsed.content !== "string" || typeof parsed.pinned !== "boolean" || typeof parsed.order !== "number" || parsed.layout !== "narrow" && parsed.layout !== "wide" && parsed.layout !== "full") {
@@ -64,16 +68,15 @@ const defaultPurgeExpiredSnapshots = async (before, limit) => {
64
68
  if (expiredSnapshots.length === 0) {
65
69
  return 0;
66
70
  }
67
- const deleted = await models.noteSnapshot.deleteMany({ where: { id: { in: expiredSnapshots.map((snapshot) => snapshot.id) } } });
71
+ const deleted = await models.noteSnapshot.deleteMany({
72
+ where: { id: { in: expiredSnapshots.map((snapshot) => snapshot.id) } }
73
+ });
68
74
  return deleted.count;
69
75
  };
70
76
  const defaultTrimOverflowSnapshots = async (noteId, keep, limit) => {
71
77
  const overflowSnapshots = await models.noteSnapshot.findMany({
72
78
  where: { noteId },
73
- orderBy: [
74
- { createdAt: "desc" },
75
- { id: "desc" }
76
- ],
79
+ orderBy: [{ createdAt: "desc" }, { id: "desc" }],
77
80
  skip: keep,
78
81
  take: limit,
79
82
  select: { id: true }
@@ -81,15 +84,14 @@ const defaultTrimOverflowSnapshots = async (noteId, keep, limit) => {
81
84
  if (overflowSnapshots.length === 0) {
82
85
  return 0;
83
86
  }
84
- const deleted = await models.noteSnapshot.deleteMany({ where: { id: { in: overflowSnapshots.map((snapshot) => snapshot.id) } } });
87
+ const deleted = await models.noteSnapshot.deleteMany({
88
+ where: { id: { in: overflowSnapshots.map((snapshot) => snapshot.id) } }
89
+ });
85
90
  return deleted.count;
86
91
  };
87
92
  const createNoteSnapshotService = (deps) => ({
88
93
  captureBaseline: async (input) => {
89
- await deps.purgeExpiredSnapshots(
90
- createRetentionCutoff(SNAPSHOT_RETENTION_DAYS),
91
- RECOVERY_CLEANUP_BATCH_LIMIT
92
- );
94
+ await deps.purgeExpiredSnapshots(createRetentionCutoff(SNAPSHOT_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);
93
95
  const existing = input.editSessionId ? await deps.findSnapshotByEditSessionId(input.noteId, input.editSessionId) : null;
94
96
  if (existing) {
95
97
  return serializeSnapshot(existing);
@@ -98,38 +100,29 @@ const createNoteSnapshotService = (deps) => ({
98
100
  if (!note) {
99
101
  return null;
100
102
  }
103
+ const payload = serializePayload(note);
104
+ const latestSnapshot = await deps.findLatestSnapshot(note.id);
105
+ if (latestSnapshot && hasSameSnapshotPayload(latestSnapshot, payload)) {
106
+ return serializeSnapshot(latestSnapshot);
107
+ }
101
108
  const snapshot = await deps.createSnapshot({
102
109
  noteId: note.id,
103
110
  title: note.title,
104
- payload: serializePayload(note),
111
+ payload,
105
112
  ...input.editSessionId ? { editSessionId: input.editSessionId } : {},
106
113
  ...input.meta ? { meta: input.meta } : {}
107
114
  });
108
- await deps.trimOverflowSnapshots(
109
- note.id,
110
- SNAPSHOT_MAX_PER_NOTE,
111
- RECOVERY_CLEANUP_BATCH_LIMIT
112
- );
115
+ await deps.trimOverflowSnapshots(note.id, SNAPSHOT_MAX_PER_NOTE, RECOVERY_CLEANUP_BATCH_LIMIT);
113
116
  return serializeSnapshot(snapshot);
114
117
  },
115
118
  listSnapshots: async (noteId, limit = 5) => {
116
- await deps.purgeExpiredSnapshots(
117
- createRetentionCutoff(SNAPSHOT_RETENTION_DAYS),
118
- RECOVERY_CLEANUP_BATCH_LIMIT
119
- );
120
- await deps.trimOverflowSnapshots(
121
- noteId,
122
- SNAPSHOT_MAX_PER_NOTE,
123
- RECOVERY_CLEANUP_BATCH_LIMIT
124
- );
119
+ await deps.purgeExpiredSnapshots(createRetentionCutoff(SNAPSHOT_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);
120
+ await deps.trimOverflowSnapshots(noteId, SNAPSHOT_MAX_PER_NOTE, RECOVERY_CLEANUP_BATCH_LIMIT);
125
121
  const snapshots = await deps.listSnapshots(noteId, limit);
126
122
  return snapshots.map(serializeSnapshot);
127
123
  },
128
124
  restoreSnapshot: async (snapshotId, options) => {
129
- await deps.purgeExpiredSnapshots(
130
- createRetentionCutoff(SNAPSHOT_RETENTION_DAYS),
131
- RECOVERY_CLEANUP_BATCH_LIMIT
132
- );
125
+ await deps.purgeExpiredSnapshots(createRetentionCutoff(SNAPSHOT_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);
133
126
  const snapshot = await deps.findSnapshotById(snapshotId);
134
127
  if (!snapshot) {
135
128
  return null;
@@ -138,17 +131,17 @@ const createNoteSnapshotService = (deps) => ({
138
131
  if (!note) {
139
132
  return null;
140
133
  }
141
- await deps.createSnapshot({
142
- noteId: note.id,
143
- title: note.title,
144
- payload: serializePayload(note),
145
- ...options?.meta ? { meta: options.meta } : {}
146
- });
147
- await deps.trimOverflowSnapshots(
148
- note.id,
149
- SNAPSHOT_MAX_PER_NOTE,
150
- RECOVERY_CLEANUP_BATCH_LIMIT
151
- );
134
+ const currentPayload = serializePayload(note);
135
+ const latestSnapshot = await deps.findLatestSnapshot(note.id);
136
+ if (!hasSameSnapshotPayload(latestSnapshot, currentPayload)) {
137
+ await deps.createSnapshot({
138
+ noteId: note.id,
139
+ title: note.title,
140
+ payload: currentPayload,
141
+ ...options?.meta ? { meta: options.meta } : {}
142
+ });
143
+ }
144
+ await deps.trimOverflowSnapshots(note.id, SNAPSHOT_MAX_PER_NOTE, RECOVERY_CLEANUP_BATCH_LIMIT);
152
145
  const payload = parsePayload(snapshot.payload);
153
146
  return deps.updateNote(snapshot.noteId, payload);
154
147
  }
@@ -165,6 +158,12 @@ const defaultNoteSnapshotService = createNoteSnapshotService({
165
158
  }
166
159
  });
167
160
  },
161
+ findLatestSnapshot: async (noteId) => {
162
+ return models.noteSnapshot.findFirst({
163
+ where: { noteId },
164
+ orderBy: [{ createdAt: "desc" }, { id: "desc" }]
165
+ });
166
+ },
168
167
  createSnapshot: async (input) => {
169
168
  return models.noteSnapshot.create({
170
169
  data: {
@@ -181,7 +180,7 @@ const defaultNoteSnapshotService = createNoteSnapshotService({
181
180
  listSnapshots: async (noteId, limit) => {
182
181
  return models.noteSnapshot.findMany({
183
182
  where: { noteId },
184
- orderBy: { createdAt: "desc" },
183
+ orderBy: [{ createdAt: "desc" }, { id: "desc" }],
185
184
  take: limit
186
185
  });
187
186
  },
@@ -191,7 +190,13 @@ const defaultNoteSnapshotService = createNoteSnapshotService({
191
190
  updateNote: async (id, input) => {
192
191
  return models.note.update({
193
192
  where: { id },
194
- data: input
193
+ data: {
194
+ ...input,
195
+ ...buildNoteSearchProjection({
196
+ title: input.title,
197
+ content: input.content
198
+ })
199
+ }
195
200
  });
196
201
  }
197
202
  });
@@ -205,10 +210,7 @@ const restoreNoteSnapshot = async (snapshotId, options) => {
205
210
  return defaultNoteSnapshotService.restoreSnapshot(snapshotId, options);
206
211
  };
207
212
  const purgeExpiredNoteSnapshots = async () => {
208
- return defaultPurgeExpiredSnapshots(
209
- createRetentionCutoff(SNAPSHOT_RETENTION_DAYS),
210
- RECOVERY_CLEANUP_BATCH_LIMIT
211
- );
213
+ return defaultPurgeExpiredSnapshots(createRetentionCutoff(SNAPSHOT_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);
212
214
  };
213
215
  export {
214
216
  MCP_SNAPSHOT_META,
@@ -220,4 +222,4 @@ export {
220
222
  purgeExpiredNoteSnapshots,
221
223
  restoreNoteSnapshot
222
224
  };
223
- //# sourceMappingURL=note-snapshot.js.map
225
+ //# sourceMappingURL=snapshot.js.map
@@ -0,0 +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":[]}
@@ -0,0 +1,18 @@
1
+ const normalizeNoteTagNames = (tagNames) => {
2
+ return Array.from(new Set(tagNames.map((tagName) => tagName.trim()).filter(Boolean)));
3
+ };
4
+ const buildNoteTagNamesWhere = (tagNames, mode) => {
5
+ const normalizedTagNames = normalizeNoteTagNames(tagNames);
6
+ if (normalizedTagNames.length === 0) {
7
+ return { id: -1 };
8
+ }
9
+ if (mode === "or") {
10
+ return { tags: { some: { name: { in: normalizedTagNames } } } };
11
+ }
12
+ return { AND: normalizedTagNames.map((tagName) => ({ tags: { some: { name: tagName } } })) };
13
+ };
14
+ export {
15
+ buildNoteTagNamesWhere,
16
+ normalizeNoteTagNames
17
+ };
18
+ //# sourceMappingURL=tag-filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/features/note/services/tag-filter.ts"],"sourcesContent":["import type { Prisma } from '~/models.js';\n\nexport type NoteTagMatchMode = 'and' | 'or';\n\nexport const normalizeNoteTagNames = (tagNames: string[]) => {\n return Array.from(new Set(tagNames.map((tagName) => tagName.trim()).filter(Boolean)));\n};\n\nexport const buildNoteTagNamesWhere = (tagNames: string[], mode: NoteTagMatchMode): Prisma.NoteWhereInput => {\n const normalizedTagNames = normalizeNoteTagNames(tagNames);\n\n if (normalizedTagNames.length === 0) {\n return { id: -1 };\n }\n\n if (mode === 'or') {\n return { tags: { some: { name: { in: normalizedTagNames } } } };\n }\n\n return { AND: normalizedTagNames.map((tagName) => ({ tags: { some: { name: tagName } } })) };\n};\n"],"mappings":"AAIO,MAAM,wBAAwB,CAAC,aAAuB;AACzD,SAAO,MAAM,KAAK,IAAI,IAAI,SAAS,IAAI,CAAC,YAAY,QAAQ,KAAK,CAAC,EAAE,OAAO,OAAO,CAAC,CAAC;AACxF;AAEO,MAAM,yBAAyB,CAAC,UAAoB,SAAkD;AACzG,QAAM,qBAAqB,sBAAsB,QAAQ;AAEzD,MAAI,mBAAmB,WAAW,GAAG;AACjC,WAAO,EAAE,IAAI,GAAG;AAAA,EACpB;AAEA,MAAI,SAAS,MAAM;AACf,WAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,mBAAmB,EAAE,EAAE,EAAE;AAAA,EAClE;AAEA,SAAO,EAAE,KAAK,mBAAmB,IAAI,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,EAAE,EAAE,EAAE,EAAE;AAC/F;","names":[]}
@@ -1,9 +1,10 @@
1
- import models from "../models.js";
1
+ import models from "../../../models.js";
2
2
  import {
3
3
  createRetentionCutoff,
4
4
  RECOVERY_CLEANUP_BATCH_LIMIT,
5
5
  TRASH_RETENTION_DAYS
6
- } from "./recovery-retention.js";
6
+ } from "../../../modules/recovery-retention.js";
7
+ import { buildNoteSearchProjection } from "./search.js";
7
8
  class NoteRestoreConflictError extends Error {
8
9
  constructor(message = "A live note with the same id already exists.") {
9
10
  super(message);
@@ -54,26 +55,17 @@ const restoreTagIdsInContent = (content, tagIdByName) => {
54
55
  };
55
56
  const createNoteTrashService = (deps) => ({
56
57
  listTrashedNotes: async (input) => {
57
- await deps.purgeExpiredDeletedNotes(
58
- createRetentionCutoff(TRASH_RETENTION_DAYS),
59
- RECOVERY_CLEANUP_BATCH_LIMIT
60
- );
58
+ await deps.purgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);
61
59
  const limit = Math.max(1, Number(input?.limit ?? 25));
62
60
  const offset = Math.max(0, Number(input?.offset ?? 0));
63
- const [totalCount, notes] = await Promise.all([
64
- deps.countDeletedNotes(),
65
- deps.listDeletedNotes(offset, limit)
66
- ]);
61
+ const [totalCount, notes] = await Promise.all([deps.countDeletedNotes(), deps.listDeletedNotes(offset, limit)]);
67
62
  return {
68
63
  totalCount,
69
64
  notes: notes.map(serializeTrashedNote)
70
65
  };
71
66
  },
72
67
  trashNoteById: async (id) => {
73
- await deps.purgeExpiredDeletedNotes(
74
- createRetentionCutoff(TRASH_RETENTION_DAYS),
75
- RECOVERY_CLEANUP_BATCH_LIMIT
76
- );
68
+ await deps.purgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);
77
69
  const note = await deps.findLiveNote(id);
78
70
  if (!note) {
79
71
  return null;
@@ -82,10 +74,7 @@ const createNoteTrashService = (deps) => ({
82
74
  return serializeTrashedNote(trashedNote);
83
75
  },
84
76
  restoreNoteById: async (id) => {
85
- await deps.purgeExpiredDeletedNotes(
86
- createRetentionCutoff(TRASH_RETENTION_DAYS),
87
- RECOVERY_CLEANUP_BATCH_LIMIT
88
- );
77
+ await deps.purgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);
89
78
  const deletedNote = await deps.findDeletedNote(id);
90
79
  if (!deletedNote) {
91
80
  return null;
@@ -94,6 +83,16 @@ const createNoteTrashService = (deps) => ({
94
83
  throw new NoteRestoreConflictError();
95
84
  }
96
85
  return deps.restoreDeletedNote(deletedNote);
86
+ },
87
+ purgeNoteById: async (id) => {
88
+ await deps.purgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);
89
+ const deletedNote = await deps.findDeletedNote(id);
90
+ if (!deletedNote) {
91
+ return null;
92
+ }
93
+ const summary = serializeTrashedNote(deletedNote);
94
+ await deps.purgeDeletedNote(deletedNote);
95
+ return summary;
97
96
  }
98
97
  });
99
98
  const includeDeletedNote = {
@@ -146,6 +145,9 @@ const noteTrashService = createNoteTrashService({
146
145
  return Boolean(note);
147
146
  },
148
147
  purgeExpiredDeletedNotes: defaultPurgeExpiredDeletedNotes,
148
+ purgeDeletedNote: async (deletedNote) => {
149
+ await models.deletedNote.delete({ where: { id: deletedNote.id } });
150
+ },
149
151
  moveNoteToTrash: async (note) => {
150
152
  return models.$transaction(async (tx) => {
151
153
  await tx.deletedNote.create({
@@ -217,6 +219,10 @@ const noteTrashService = createNoteTrashService({
217
219
  id: deletedNote.id,
218
220
  title: deletedNote.title,
219
221
  content: restoredContent,
222
+ ...buildNoteSearchProjection({
223
+ title: deletedNote.title,
224
+ content: restoredContent
225
+ }),
220
226
  createdAt: deletedNote.createdAt,
221
227
  updatedAt: deletedNote.updatedAt,
222
228
  pinned: deletedNote.pinned,
@@ -245,18 +251,17 @@ const noteTrashService = createNoteTrashService({
245
251
  const listTrashedNotes = noteTrashService.listTrashedNotes;
246
252
  const trashNoteById = noteTrashService.trashNoteById;
247
253
  const restoreTrashedNoteById = noteTrashService.restoreNoteById;
254
+ const purgeTrashedNoteById = noteTrashService.purgeNoteById;
248
255
  const purgeExpiredTrashedNotes = async () => {
249
- return defaultPurgeExpiredDeletedNotes(
250
- createRetentionCutoff(TRASH_RETENTION_DAYS),
251
- RECOVERY_CLEANUP_BATCH_LIMIT
252
- );
256
+ return defaultPurgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);
253
257
  };
254
258
  export {
255
259
  NoteRestoreConflictError,
256
260
  createNoteTrashService,
257
261
  listTrashedNotes,
258
262
  purgeExpiredTrashedNotes,
263
+ purgeTrashedNoteById,
259
264
  restoreTrashedNoteById,
260
265
  trashNoteById
261
266
  };
262
- //# sourceMappingURL=note-trash.js.map
267
+ //# sourceMappingURL=trash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/features/note/services/trash.ts"],"sourcesContent":["import models, { type NoteLayout, type ReminderPriority } from '~/models.js';\nimport {\n createRetentionCutoff,\n RECOVERY_CLEANUP_BATCH_LIMIT,\n TRASH_RETENTION_DAYS,\n} from '~/modules/recovery-retention.js';\nimport { buildNoteSearchProjection } from './search.js';\n\ninterface LiveTagRecord {\n id: number;\n name: string;\n}\n\ninterface LiveReminderRecord {\n id: number;\n reminderDate: Date;\n completed: boolean;\n priority: ReminderPriority;\n content: string | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\ninterface LiveNoteRecord {\n id: number;\n title: string;\n content: string;\n createdAt: Date;\n updatedAt: Date;\n pinned: boolean;\n order: number;\n layout: NoteLayout;\n tags: LiveTagRecord[];\n reminders: LiveReminderRecord[];\n}\n\ninterface RestoredNoteRecord {\n id: number;\n title: string;\n content: string;\n createdAt: Date;\n updatedAt: Date;\n pinned: boolean;\n order: number;\n layout: NoteLayout;\n}\n\ninterface DeletedTagRecord {\n name: string;\n}\n\ninterface DeletedReminderRecord {\n originalId: number | null;\n reminderDate: Date;\n completed: boolean;\n priority: ReminderPriority;\n content: string | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\ninterface DeletedNoteRecord {\n id: number;\n title: string;\n content: string;\n createdAt: Date;\n updatedAt: Date;\n deletedAt: Date;\n pinned: boolean;\n order: number;\n layout: NoteLayout;\n tags: DeletedTagRecord[];\n reminders: DeletedReminderRecord[];\n}\n\ninterface BlockNoteNode {\n type: string;\n props?: Record<string, unknown>;\n content?: BlockNoteNode[];\n children?: BlockNoteNode[];\n}\n\nexport interface TrashedNoteSummary {\n id: string;\n title: string;\n createdAt: string;\n updatedAt: string;\n deletedAt: string;\n pinned: boolean;\n order: number;\n layout: NoteLayout;\n tagNames: string[];\n}\n\nexport interface TrashedNotesResult {\n totalCount: number;\n notes: TrashedNoteSummary[];\n}\n\nexport class NoteRestoreConflictError extends Error {\n constructor(message = 'A live note with the same id already exists.') {\n super(message);\n this.name = 'NoteRestoreConflictError';\n }\n}\n\ninterface NoteTrashServiceDeps {\n countDeletedNotes: () => Promise<number>;\n findDeletedNote: (id: number) => Promise<DeletedNoteRecord | null>;\n findLiveNote: (id: number) => Promise<LiveNoteRecord | null>;\n listDeletedNotes: (skip: number, take: number) => Promise<DeletedNoteRecord[]>;\n liveNoteExists: (id: number) => Promise<boolean>;\n moveNoteToTrash: (note: LiveNoteRecord) => Promise<DeletedNoteRecord>;\n purgeDeletedNote: (note: DeletedNoteRecord) => Promise<void>;\n purgeExpiredDeletedNotes: (before: Date, limit: number) => Promise<number>;\n restoreDeletedNote: (note: DeletedNoteRecord) => Promise<RestoredNoteRecord>;\n}\n\nconst serializeTrashedNote = (note: DeletedNoteRecord): TrashedNoteSummary => ({\n id: String(note.id),\n title: note.title,\n createdAt: note.createdAt.toISOString(),\n updatedAt: note.updatedAt.toISOString(),\n deletedAt: note.deletedAt.toISOString(),\n pinned: note.pinned,\n order: note.order,\n layout: note.layout,\n tagNames: note.tags.map((tag) => tag.name),\n});\n\nconst restoreTagIdsInContent = (content: string, tagIdByName: Map<string, number>) => {\n const rewriteNodes = (nodes: BlockNoteNode[]): BlockNoteNode[] => {\n return nodes.map((node) => ({\n ...node,\n content: node.content?.map((contentNode) => {\n if (contentNode.type !== 'tag') {\n return contentNode;\n }\n\n const tagName = typeof contentNode.props?.tag === 'string' ? contentNode.props.tag : null;\n const restoredTagId = tagName ? tagIdByName.get(tagName) : null;\n\n if (!restoredTagId) {\n return contentNode;\n }\n\n return {\n ...contentNode,\n props: {\n ...contentNode.props,\n id: String(restoredTagId),\n },\n };\n }),\n children: node.children ? rewriteNodes(node.children) : node.children,\n }));\n };\n\n try {\n const parsed = JSON.parse(content) as BlockNoteNode[];\n return JSON.stringify(rewriteNodes(parsed));\n } catch {\n return content;\n }\n};\n\nexport const createNoteTrashService = (deps: NoteTrashServiceDeps) => ({\n listTrashedNotes: async (input?: { limit?: number; offset?: number }): Promise<TrashedNotesResult> => {\n await deps.purgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\n\n const limit = Math.max(1, Number(input?.limit ?? 25));\n const offset = Math.max(0, Number(input?.offset ?? 0));\n const [totalCount, notes] = await Promise.all([deps.countDeletedNotes(), deps.listDeletedNotes(offset, limit)]);\n\n return {\n totalCount,\n notes: notes.map(serializeTrashedNote),\n };\n },\n\n trashNoteById: async (id: number): Promise<TrashedNoteSummary | null> => {\n await deps.purgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\n\n const note = await deps.findLiveNote(id);\n\n if (!note) {\n return null;\n }\n\n const trashedNote = await deps.moveNoteToTrash(note);\n return serializeTrashedNote(trashedNote);\n },\n\n restoreNoteById: async (id: number) => {\n await deps.purgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\n\n const deletedNote = await deps.findDeletedNote(id);\n\n if (!deletedNote) {\n return null;\n }\n\n if (await deps.liveNoteExists(id)) {\n throw new NoteRestoreConflictError();\n }\n\n return deps.restoreDeletedNote(deletedNote);\n },\n\n purgeNoteById: async (id: number): Promise<TrashedNoteSummary | null> => {\n await deps.purgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\n\n const deletedNote = await deps.findDeletedNote(id);\n\n if (!deletedNote) {\n return null;\n }\n\n const summary = serializeTrashedNote(deletedNote);\n await deps.purgeDeletedNote(deletedNote);\n return summary;\n },\n});\n\nconst includeDeletedNote = {\n reminders: { orderBy: { reminderDate: 'asc' as const } },\n tags: { orderBy: { name: 'asc' as const } },\n};\n\nconst defaultPurgeExpiredDeletedNotes = async (before: Date, limit: number) => {\n const expiredNotes = await models.deletedNote.findMany({\n where: { deletedAt: { lt: before } },\n orderBy: { deletedAt: 'asc' },\n take: limit,\n select: { id: true },\n });\n\n if (expiredNotes.length === 0) {\n return 0;\n }\n\n const deleted = await models.deletedNote.deleteMany({ where: { id: { in: expiredNotes.map((note) => note.id) } } });\n\n return deleted.count;\n};\n\nconst noteTrashService = createNoteTrashService({\n countDeletedNotes: async () => models.deletedNote.count(),\n findDeletedNote: async (id) => {\n return models.deletedNote.findUnique({\n where: { id },\n include: includeDeletedNote,\n });\n },\n findLiveNote: async (id) => {\n return models.note.findUnique({\n where: { id },\n include: {\n reminders: { orderBy: { reminderDate: 'asc' } },\n tags: { orderBy: { name: 'asc' } },\n },\n });\n },\n listDeletedNotes: async (skip, take) => {\n return models.deletedNote.findMany({\n skip,\n take,\n orderBy: { deletedAt: 'desc' },\n include: includeDeletedNote,\n });\n },\n liveNoteExists: async (id) => {\n const note = await models.note.findUnique({\n where: { id },\n select: { id: true },\n });\n return Boolean(note);\n },\n purgeExpiredDeletedNotes: defaultPurgeExpiredDeletedNotes,\n purgeDeletedNote: async (deletedNote) => {\n await models.deletedNote.delete({ where: { id: deletedNote.id } });\n },\n moveNoteToTrash: async (note) => {\n return models.$transaction(async (tx) => {\n await tx.deletedNote.create({\n data: {\n id: note.id,\n title: note.title,\n content: note.content,\n createdAt: note.createdAt,\n updatedAt: note.updatedAt,\n pinned: note.pinned,\n order: note.order,\n layout: note.layout,\n tags: { create: note.tags.map((tag) => ({ name: tag.name })) },\n reminders: {\n create: note.reminders.map((reminder) => ({\n originalId: reminder.id,\n reminderDate: reminder.reminderDate,\n completed: reminder.completed,\n priority: reminder.priority,\n content: reminder.content,\n createdAt: reminder.createdAt,\n updatedAt: reminder.updatedAt,\n })),\n },\n },\n });\n\n await tx.note.delete({ where: { id: note.id } });\n\n if (note.tags.length > 0) {\n await tx.tag.deleteMany({\n where: {\n id: { in: note.tags.map((tag) => tag.id) },\n notes: { none: {} },\n },\n });\n }\n\n return tx.deletedNote.findUniqueOrThrow({\n where: { id: note.id },\n include: includeDeletedNote,\n });\n });\n },\n restoreDeletedNote: async (deletedNote) => {\n return models.$transaction(async (tx) => {\n const tagNames = Array.from(new Set(deletedNote.tags.map((tag) => tag.name).filter(Boolean)));\n\n if (tagNames.length > 0) {\n const existingTags = await tx.tag.findMany({\n where: { name: { in: tagNames } },\n select: { name: true },\n });\n const existingTagNames = new Set(existingTags.map((tag) => tag.name));\n const missingTagNames = tagNames.filter((tagName) => !existingTagNames.has(tagName));\n\n for (const tagName of missingTagNames) {\n await tx.tag.create({ data: { name: tagName } });\n }\n }\n\n const restoreTagIds =\n tagNames.length > 0\n ? await tx.tag.findMany({\n where: { name: { in: tagNames } },\n select: {\n id: true,\n name: true,\n },\n })\n : [];\n\n const restoredContent =\n restoreTagIds.length > 0\n ? restoreTagIdsInContent(\n deletedNote.content,\n new Map(restoreTagIds.map((tag) => [tag.name, tag.id])),\n )\n : deletedNote.content;\n\n const note = await tx.note.create({\n data: {\n id: deletedNote.id,\n title: deletedNote.title,\n content: restoredContent,\n ...buildNoteSearchProjection({\n title: deletedNote.title,\n content: restoredContent,\n }),\n createdAt: deletedNote.createdAt,\n updatedAt: deletedNote.updatedAt,\n pinned: deletedNote.pinned,\n order: deletedNote.order,\n layout: deletedNote.layout,\n ...(restoreTagIds.length > 0\n ? { tags: { connect: restoreTagIds.map((tag) => ({ id: tag.id })) } }\n : {}),\n ...(deletedNote.reminders.length > 0\n ? {\n reminders: {\n create: deletedNote.reminders.map((reminder) => ({\n reminderDate: reminder.reminderDate,\n completed: reminder.completed,\n priority: reminder.priority,\n content: reminder.content,\n createdAt: reminder.createdAt,\n updatedAt: reminder.updatedAt,\n })),\n },\n }\n : {}),\n },\n });\n\n await tx.deletedNote.delete({ where: { id: deletedNote.id } });\n\n return note;\n });\n },\n});\n\nexport const listTrashedNotes = noteTrashService.listTrashedNotes;\nexport const trashNoteById = noteTrashService.trashNoteById;\nexport const restoreTrashedNoteById = noteTrashService.restoreNoteById;\nexport const purgeTrashedNoteById = noteTrashService.purgeNoteById;\nexport const purgeExpiredTrashedNotes = async () => {\n return defaultPurgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);\n};\n"],"mappings":"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","names":[]}