ocean-brain 0.3.4 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +5 -0
- package/package.json +6 -5
- package/scripts/postinstall-prisma.mjs +3 -2
- package/server/client/dist/assets/Calendar-D4KkvcU8.js +25 -0
- package/server/client/dist/assets/Callout-CaSzc-1y.js +1 -0
- package/server/client/dist/assets/Graph-C8pBdJzV.js +1 -0
- package/server/client/dist/assets/{Image-ZxF459sC.js → Image-DlJ4MDo3.js} +1 -1
- package/server/client/dist/assets/{Image.es-BQfyKc2-.js → Image.es-C8g5NwNt.js} +1 -1
- package/server/client/dist/assets/Note-ODOESft3.js +21 -0
- package/server/client/dist/assets/Note-W4f6H9K0.css +1 -0
- package/server/client/dist/assets/Plus.es-Bkl1ZXE0.js +1 -0
- package/server/client/dist/assets/Reminders-FrTR9ny8.js +1 -0
- package/server/client/dist/assets/Search-BPf2LO1Y.js +1 -0
- package/server/client/dist/assets/{SurfaceCard-nZNf9lyE.js → SurfaceCard-WSk7zWeM.js} +1 -1
- package/server/client/dist/assets/Tag-uArdLjo1.js +1 -0
- package/server/client/dist/assets/TagNotes-DDvXHB9I.js +1 -0
- package/server/client/dist/assets/{Trash.es-BGoN5q9L.js → Trash.es-DumPfiUz.js} +1 -1
- package/server/client/dist/assets/ViewNotes-BVu9lEs-.js +1 -0
- package/server/client/dist/assets/Views-D4LGh30P.js +1 -0
- package/server/client/dist/assets/{image.api-0mMkElZk.js → image.api-D3Ei3fbf.js} +1 -1
- package/server/client/dist/assets/index-46ZUXbW5.js +232 -0
- package/server/client/dist/assets/index-C7YGbIuA.js +1 -0
- package/server/client/dist/assets/{index-DL7pUwq2.css → index-D2gRB1ig.css} +1 -1
- package/server/client/dist/assets/manage-image-DiuREdeE.js +1 -0
- package/server/client/dist/assets/manage-image-detail-D8Eorq3u.js +1 -0
- package/server/client/dist/assets/mcp-CvCGaYSz.js +1 -0
- package/server/client/dist/assets/{note-core-BCgMq5QA.js → note-core-GgFcksAF.js} +1 -1
- package/server/client/dist/assets/{note-vendor-BofYbzmZ.js → note-vendor-B0t5F04o.js} +5 -5
- package/server/client/dist/assets/{placeholder-i-uNe71U.js → placeholder-znZDfNAE.js} +2 -2
- package/server/client/dist/assets/trash-R0sww0ur.js +1 -0
- package/server/client/dist/assets/{useReminderMutate-tNMwwRnx.js → useReminderMutate-Bkj-oIm4.js} +2 -2
- package/server/client/dist/assets/view-dashboard-ClpF_qLM.js +90 -0
- package/server/client/dist/index.html +4 -4
- package/server/dist/app.js +8 -84
- package/server/dist/app.js.map +1 -1
- package/server/dist/features/auth/http/api.js +40 -0
- package/server/dist/features/auth/http/api.js.map +1 -0
- package/server/dist/features/auth/http/login-page.js +28 -0
- package/server/dist/features/auth/http/login-page.js.map +1 -0
- package/server/dist/features/auth/http/pages.js +50 -0
- package/server/dist/features/auth/http/pages.js.map +1 -0
- package/server/dist/features/auth/service.js +63 -0
- package/server/dist/features/auth/service.js.map +1 -0
- package/server/dist/features/cache/graphql/cache.field.resolver.js +5 -0
- package/server/dist/features/cache/graphql/cache.field.resolver.js.map +1 -0
- package/server/dist/features/cache/graphql/cache.mutation.resolver.js +49 -0
- package/server/dist/features/cache/graphql/cache.mutation.resolver.js.map +1 -0
- package/server/dist/features/cache/graphql/cache.query.resolver.js +21 -0
- package/server/dist/features/cache/graphql/cache.query.resolver.js.map +1 -0
- package/server/dist/features/cache/graphql/cache.resolver.js +12 -0
- package/server/dist/features/cache/graphql/cache.resolver.js.map +1 -0
- package/server/dist/features/cache/graphql/cache.type-defs.js +32 -0
- package/server/dist/features/cache/graphql/cache.type-defs.js.map +1 -0
- package/server/dist/features/cache/graphql/index.js +21 -0
- package/server/dist/features/cache/graphql/index.js.map +1 -0
- package/server/dist/features/image/graphql/image.field.resolver.js +14 -0
- package/server/dist/features/image/graphql/image.field.resolver.js.map +1 -0
- package/server/dist/features/image/graphql/image.mutation.resolver.js +21 -0
- package/server/dist/features/image/graphql/image.mutation.resolver.js.map +1 -0
- package/server/dist/features/image/graphql/image.query.resolver.js +40 -0
- package/server/dist/features/image/graphql/image.query.resolver.js.map +1 -0
- package/server/dist/features/image/graphql/image.resolver.js +12 -0
- package/server/dist/features/image/graphql/image.resolver.js.map +1 -0
- package/server/dist/features/image/graphql/image.type-defs.js +31 -0
- package/server/dist/features/image/graphql/image.type-defs.js.map +1 -0
- package/server/dist/features/image/graphql/index.js +16 -0
- package/server/dist/features/image/graphql/index.js.map +1 -0
- package/server/dist/features/image/http/upload.js +48 -0
- package/server/dist/features/image/http/upload.js.map +1 -0
- package/server/dist/features/image/services/delete.js +52 -0
- package/server/dist/features/image/services/delete.js.map +1 -0
- package/server/dist/{modules/remote-image.js → features/image/services/remote-fetch.js} +1 -1
- package/server/dist/features/image/services/remote-fetch.js.map +1 -0
- package/server/dist/{modules/image-upload.js → features/image/services/upload.js} +4 -8
- package/server/dist/features/image/services/upload.js.map +1 -0
- package/server/dist/{views/mcp-admin.js → features/mcp-admin/http/handlers.js} +3 -3
- package/server/dist/features/mcp-admin/http/handlers.js.map +1 -0
- package/server/dist/{modules/mcp-admin.js → features/mcp-admin/service.js} +3 -3
- package/server/dist/features/mcp-admin/service.js.map +1 -0
- package/server/dist/features/note/graphql/index.js +8 -0
- package/server/dist/features/note/graphql/index.js.map +1 -0
- package/server/dist/features/note/graphql/note.field.resolver.js +12 -0
- package/server/dist/features/note/graphql/note.field.resolver.js.map +1 -0
- package/server/dist/features/note/graphql/note.graphql.shared.js +23 -0
- package/server/dist/features/note/graphql/note.graphql.shared.js.map +1 -0
- package/server/dist/features/note/graphql/note.mutation.resolver.js +140 -0
- package/server/dist/features/note/graphql/note.mutation.resolver.js.map +1 -0
- package/server/dist/features/note/graphql/note.query.resolver.js +385 -0
- package/server/dist/features/note/graphql/note.query.resolver.js.map +1 -0
- package/server/dist/features/note/graphql/note.resolver.js +14 -0
- package/server/dist/features/note/graphql/note.resolver.js.map +1 -0
- package/server/dist/features/note/graphql/note.type-defs.js +187 -0
- package/server/dist/features/note/graphql/note.type-defs.js.map +1 -0
- package/server/dist/{views/note.js → features/note/http/mcp.js} +27 -15
- package/server/dist/features/note/http/mcp.js.map +1 -0
- package/server/dist/{modules/note-authoring.js → features/note/services/authoring.js} +25 -7
- package/server/dist/features/note/services/authoring.js.map +1 -0
- package/server/dist/{modules/note-cleanup.js → features/note/services/cleanup.js} +14 -23
- package/server/dist/features/note/services/cleanup.js.map +1 -0
- package/server/dist/features/note/services/search-backfill.js +84 -0
- package/server/dist/features/note/services/search-backfill.js.map +1 -0
- package/server/dist/features/note/services/search.js +197 -0
- package/server/dist/features/note/services/search.js.map +1 -0
- package/server/dist/{modules/note-snapshot.js → features/note/services/snapshot.js} +51 -49
- package/server/dist/features/note/services/snapshot.js.map +1 -0
- package/server/dist/{modules/note-tag-filter.js → features/note/services/tag-filter.js} +2 -6
- package/server/dist/features/note/services/tag-filter.js.map +1 -0
- package/server/dist/{modules/note-trash.js → features/note/services/trash.js} +64 -34
- package/server/dist/features/note/services/trash.js.map +1 -0
- package/server/dist/features/placeholder/graphql/index.js +27 -0
- package/server/dist/features/placeholder/graphql/index.js.map +1 -0
- package/server/dist/features/placeholder/graphql/placeholder.field.resolver.js +5 -0
- package/server/dist/features/placeholder/graphql/placeholder.field.resolver.js.map +1 -0
- package/server/dist/features/placeholder/graphql/placeholder.mutation.resolver.js +72 -0
- package/server/dist/features/placeholder/graphql/placeholder.mutation.resolver.js.map +1 -0
- package/server/dist/features/placeholder/graphql/placeholder.query.resolver.js +36 -0
- package/server/dist/features/placeholder/graphql/placeholder.query.resolver.js.map +1 -0
- package/server/dist/features/placeholder/graphql/placeholder.resolver.js +12 -0
- package/server/dist/features/placeholder/graphql/placeholder.resolver.js.map +1 -0
- package/server/dist/features/placeholder/graphql/placeholder.type-defs.js +50 -0
- package/server/dist/features/placeholder/graphql/placeholder.type-defs.js.map +1 -0
- package/server/dist/features/reminder/graphql/index.js +27 -0
- package/server/dist/features/reminder/graphql/index.js.map +1 -0
- package/server/dist/features/reminder/graphql/reminder.field.resolver.js +10 -0
- package/server/dist/features/reminder/graphql/reminder.field.resolver.js.map +1 -0
- package/server/dist/features/reminder/graphql/reminder.mutation.resolver.js +59 -0
- package/server/dist/features/reminder/graphql/reminder.mutation.resolver.js.map +1 -0
- package/server/dist/features/reminder/graphql/reminder.query.resolver.js +79 -0
- package/server/dist/features/reminder/graphql/reminder.query.resolver.js.map +1 -0
- package/server/dist/features/reminder/graphql/reminder.resolver.js +12 -0
- package/server/dist/features/reminder/graphql/reminder.resolver.js.map +1 -0
- package/server/dist/features/reminder/graphql/reminder.type-defs.js +51 -0
- package/server/dist/features/reminder/graphql/reminder.type-defs.js.map +1 -0
- package/server/dist/features/tag/graphql/index.js +14 -0
- package/server/dist/features/tag/graphql/index.js.map +1 -0
- package/server/dist/features/tag/graphql/tag.field.resolver.js +10 -0
- package/server/dist/features/tag/graphql/tag.field.resolver.js.map +1 -0
- package/server/dist/features/tag/graphql/tag.mutation.resolver.js +15 -0
- package/server/dist/features/tag/graphql/tag.mutation.resolver.js.map +1 -0
- package/server/dist/features/tag/graphql/tag.query.resolver.js +26 -0
- package/server/dist/features/tag/graphql/tag.query.resolver.js.map +1 -0
- package/server/dist/features/tag/graphql/tag.resolver.js +12 -0
- package/server/dist/features/tag/graphql/tag.resolver.js.map +1 -0
- package/server/dist/features/tag/graphql/tag.type-defs.js +46 -0
- package/server/dist/features/tag/graphql/tag.type-defs.js.map +1 -0
- package/server/dist/{views/tag.js → features/tag/http/mcp.js} +3 -3
- package/server/dist/features/tag/http/mcp.js.map +1 -0
- package/server/dist/{modules/tag-organization.js → features/tag/services/organization.js} +38 -8
- package/server/dist/features/tag/services/organization.js.map +1 -0
- package/server/dist/features/view/graphql/index.js +11 -0
- package/server/dist/features/view/graphql/index.js.map +1 -0
- package/server/dist/features/view/graphql/view.mutation.resolver.js +53 -0
- package/server/dist/features/view/graphql/view.mutation.resolver.js.map +1 -0
- package/server/dist/features/view/graphql/view.query.resolver.js +29 -0
- package/server/dist/features/view/graphql/view.query.resolver.js.map +1 -0
- package/server/dist/features/view/graphql/view.resolver.js +10 -0
- package/server/dist/features/view/graphql/view.resolver.js.map +1 -0
- package/server/dist/features/view/graphql/view.type-defs.js +63 -0
- package/server/dist/features/view/graphql/view.type-defs.js.map +1 -0
- package/server/dist/features/view/services/workspace.js +432 -0
- package/server/dist/features/view/services/workspace.js.map +1 -0
- package/server/dist/main.js +14 -0
- package/server/dist/main.js.map +1 -1
- package/server/dist/modules/auth-guard.js +16 -21
- package/server/dist/modules/auth-guard.js.map +1 -1
- package/server/dist/modules/auth-mode.js +16 -41
- package/server/dist/modules/auth-mode.js.map +1 -1
- package/server/dist/modules/blocknote.js +8 -17
- package/server/dist/modules/blocknote.js.map +1 -1
- package/server/dist/modules/data-maintenance.js +75 -0
- package/server/dist/modules/data-maintenance.js.map +1 -0
- package/server/dist/modules/error-handler.js.map +1 -1
- package/server/dist/modules/logger.js +5 -23
- package/server/dist/modules/logger.js.map +1 -1
- package/server/dist/modules/mcp-auth.js +4 -5
- package/server/dist/modules/mcp-auth.js.map +1 -1
- package/server/dist/modules/mcp-token.js.map +1 -1
- package/server/dist/modules/prisma-runtime.js +2 -7
- package/server/dist/modules/prisma-runtime.js.map +1 -1
- package/server/dist/modules/recovery-retention.js +1 -1
- package/server/dist/modules/recovery-retention.js.map +1 -1
- package/server/dist/modules/server-events-handler.js +34 -0
- package/server/dist/modules/server-events-handler.js.map +1 -0
- package/server/dist/modules/server-events.js +32 -0
- package/server/dist/modules/server-events.js.map +1 -0
- package/server/dist/modules/use-async.js.map +1 -1
- package/server/dist/paths.js.map +1 -1
- package/server/dist/routes/api.js +34 -0
- package/server/dist/routes/api.js.map +1 -0
- package/server/dist/routes/auth-pages.js +11 -0
- package/server/dist/routes/auth-pages.js.map +1 -0
- package/server/dist/routes/client.js +27 -0
- package/server/dist/routes/client.js.map +1 -0
- package/server/dist/routes/graphql.js +33 -0
- package/server/dist/routes/graphql.js.map +1 -0
- package/server/dist/routes/index.js +11 -0
- package/server/dist/routes/index.js.map +1 -0
- package/server/dist/routes/mcp.js +30 -0
- package/server/dist/routes/mcp.js.map +1 -0
- package/server/dist/schema/index.js +11 -8
- package/server/dist/schema/index.js.map +1 -1
- package/server/dist/types/index.js.map +1 -1
- package/server/prisma/migrations/20260413143000_0014_tag_name_unique/migration.sql +32 -0
- package/server/prisma/migrations/20260414143000_0015_note_searchable_text/migration.sql +5 -0
- package/server/prisma/migrations/20260416090000_0016_view_workspace/migration.sql +50 -0
- package/server/prisma/schema.prisma +66 -12
- package/server/client/dist/assets/Calendar-Kjy1PX6L.js +0 -25
- package/server/client/dist/assets/Graph-Bl5K2qUw.js +0 -1
- package/server/client/dist/assets/ModalActionRow-DyOb58KJ.js +0 -1
- package/server/client/dist/assets/Note-BfGma2Qc.css +0 -1
- package/server/client/dist/assets/Note-DN_ko2M3.js +0 -1
- package/server/client/dist/assets/Reminders-BK-Uv9Ps.js +0 -1
- package/server/client/dist/assets/Search-ClHeWvmI.js +0 -1
- package/server/client/dist/assets/Tag-Dph4V_kl.js +0 -1
- package/server/client/dist/assets/TagNotes-CxP5qHZj.js +0 -1
- package/server/client/dist/assets/index-6lIw6jYW.js +0 -1
- package/server/client/dist/assets/index-Ba2XPRI8.js +0 -205
- package/server/client/dist/assets/manage-image-Br_kKxbb.js +0 -1
- package/server/client/dist/assets/manage-image-detail-9DzqbiqI.js +0 -1
- package/server/client/dist/assets/mcp-C_So91eX.js +0 -1
- package/server/client/dist/assets/trash-Cg3nJ1m5.js +0 -1
- package/server/dist/modules/auth.js +0 -25
- package/server/dist/modules/auth.js.map +0 -1
- package/server/dist/modules/image-upload.js.map +0 -1
- package/server/dist/modules/mcp-admin.js.map +0 -1
- package/server/dist/modules/note-authoring.js.map +0 -1
- package/server/dist/modules/note-cleanup.js.map +0 -1
- package/server/dist/modules/note-snapshot.js.map +0 -1
- package/server/dist/modules/note-tag-filter.js.map +0 -1
- package/server/dist/modules/note-trash.js.map +0 -1
- package/server/dist/modules/remote-image.js.map +0 -1
- package/server/dist/modules/tag-organization.js.map +0 -1
- package/server/dist/schema/cache/index.js +0 -58
- package/server/dist/schema/cache/index.js.map +0 -1
- package/server/dist/schema/image/index.js +0 -88
- package/server/dist/schema/image/index.js.map +0 -1
- package/server/dist/schema/note/index.js +0 -582
- package/server/dist/schema/note/index.js.map +0 -1
- package/server/dist/schema/placeholder/index.js +0 -105
- package/server/dist/schema/placeholder/index.js.map +0 -1
- package/server/dist/schema/reminder/index.js +0 -161
- package/server/dist/schema/reminder/index.js.map +0 -1
- package/server/dist/schema/tag/index.js +0 -81
- package/server/dist/schema/tag/index.js.map +0 -1
- package/server/dist/urls.js +0 -9
- package/server/dist/urls.js.map +0 -1
- package/server/dist/views/auth.js +0 -305
- package/server/dist/views/auth.js.map +0 -1
- package/server/dist/views/image.js +0 -44
- package/server/dist/views/image.js.map +0 -1
- package/server/dist/views/index.js +0 -6
- package/server/dist/views/index.js.map +0 -1
- package/server/dist/views/mcp-admin.js.map +0 -1
- package/server/dist/views/note.js.map +0 -1
- 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 "
|
|
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 "
|
|
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({
|
|
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({
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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:
|
|
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=
|
|
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":[]}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
const normalizeNoteTagNames = (tagNames) => {
|
|
2
|
-
return Array.from(
|
|
3
|
-
new Set(
|
|
4
|
-
tagNames.map((tagName) => tagName.trim()).filter(Boolean)
|
|
5
|
-
)
|
|
6
|
-
);
|
|
2
|
+
return Array.from(new Set(tagNames.map((tagName) => tagName.trim()).filter(Boolean)));
|
|
7
3
|
};
|
|
8
4
|
const buildNoteTagNamesWhere = (tagNames, mode) => {
|
|
9
5
|
const normalizedTagNames = normalizeNoteTagNames(tagNames);
|
|
@@ -19,4 +15,4 @@ export {
|
|
|
19
15
|
buildNoteTagNamesWhere,
|
|
20
16
|
normalizeNoteTagNames
|
|
21
17
|
};
|
|
22
|
-
//# sourceMappingURL=
|
|
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,26 +1,38 @@
|
|
|
1
|
-
import models from "
|
|
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 "
|
|
6
|
+
} from "../../../modules/recovery-retention.js";
|
|
7
|
+
import { buildNoteSearchProjection, extractVisibleSearchTextFromContent } from "./search.js";
|
|
8
|
+
const TRASHED_NOTE_CONTENT_PREVIEW_MAX_LENGTH = 240;
|
|
7
9
|
class NoteRestoreConflictError extends Error {
|
|
8
10
|
constructor(message = "A live note with the same id already exists.") {
|
|
9
11
|
super(message);
|
|
10
12
|
this.name = "NoteRestoreConflictError";
|
|
11
13
|
}
|
|
12
14
|
}
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
const buildTrashedNoteContentPreview = (content) => {
|
|
16
|
+
const text = extractVisibleSearchTextFromContent(content);
|
|
17
|
+
if (text.length <= TRASHED_NOTE_CONTENT_PREVIEW_MAX_LENGTH) {
|
|
18
|
+
return text;
|
|
19
|
+
}
|
|
20
|
+
return `${text.slice(0, TRASHED_NOTE_CONTENT_PREVIEW_MAX_LENGTH).trimEnd()}\u2026`;
|
|
21
|
+
};
|
|
22
|
+
const serializeTrashedNote = (note) => {
|
|
23
|
+
return {
|
|
24
|
+
id: String(note.id),
|
|
25
|
+
title: note.title,
|
|
26
|
+
createdAt: note.createdAt.toISOString(),
|
|
27
|
+
updatedAt: note.updatedAt.toISOString(),
|
|
28
|
+
deletedAt: note.deletedAt.toISOString(),
|
|
29
|
+
contentPreview: buildTrashedNoteContentPreview(note.content),
|
|
30
|
+
pinned: note.pinned,
|
|
31
|
+
order: note.order,
|
|
32
|
+
layout: note.layout,
|
|
33
|
+
tagNames: note.tags.map((tag) => tag.name)
|
|
34
|
+
};
|
|
35
|
+
};
|
|
24
36
|
const restoreTagIdsInContent = (content, tagIdByName) => {
|
|
25
37
|
const rewriteNodes = (nodes) => {
|
|
26
38
|
return nodes.map((node) => ({
|
|
@@ -54,26 +66,17 @@ const restoreTagIdsInContent = (content, tagIdByName) => {
|
|
|
54
66
|
};
|
|
55
67
|
const createNoteTrashService = (deps) => ({
|
|
56
68
|
listTrashedNotes: async (input) => {
|
|
57
|
-
await deps.purgeExpiredDeletedNotes(
|
|
58
|
-
createRetentionCutoff(TRASH_RETENTION_DAYS),
|
|
59
|
-
RECOVERY_CLEANUP_BATCH_LIMIT
|
|
60
|
-
);
|
|
69
|
+
await deps.purgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);
|
|
61
70
|
const limit = Math.max(1, Number(input?.limit ?? 25));
|
|
62
71
|
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
|
-
]);
|
|
72
|
+
const [totalCount, notes] = await Promise.all([deps.countDeletedNotes(), deps.listDeletedNotes(offset, limit)]);
|
|
67
73
|
return {
|
|
68
74
|
totalCount,
|
|
69
75
|
notes: notes.map(serializeTrashedNote)
|
|
70
76
|
};
|
|
71
77
|
},
|
|
72
78
|
trashNoteById: async (id) => {
|
|
73
|
-
await deps.purgeExpiredDeletedNotes(
|
|
74
|
-
createRetentionCutoff(TRASH_RETENTION_DAYS),
|
|
75
|
-
RECOVERY_CLEANUP_BATCH_LIMIT
|
|
76
|
-
);
|
|
79
|
+
await deps.purgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);
|
|
77
80
|
const note = await deps.findLiveNote(id);
|
|
78
81
|
if (!note) {
|
|
79
82
|
return null;
|
|
@@ -81,11 +84,20 @@ const createNoteTrashService = (deps) => ({
|
|
|
81
84
|
const trashedNote = await deps.moveNoteToTrash(note);
|
|
82
85
|
return serializeTrashedNote(trashedNote);
|
|
83
86
|
},
|
|
87
|
+
getNoteById: 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 { blocksToMarkdown } = await import("../../../modules/blocknote.js");
|
|
94
|
+
return {
|
|
95
|
+
...serializeTrashedNote(deletedNote),
|
|
96
|
+
contentAsMarkdown: await blocksToMarkdown(deletedNote.content)
|
|
97
|
+
};
|
|
98
|
+
},
|
|
84
99
|
restoreNoteById: async (id) => {
|
|
85
|
-
await deps.purgeExpiredDeletedNotes(
|
|
86
|
-
createRetentionCutoff(TRASH_RETENTION_DAYS),
|
|
87
|
-
RECOVERY_CLEANUP_BATCH_LIMIT
|
|
88
|
-
);
|
|
100
|
+
await deps.purgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);
|
|
89
101
|
const deletedNote = await deps.findDeletedNote(id);
|
|
90
102
|
if (!deletedNote) {
|
|
91
103
|
return null;
|
|
@@ -94,6 +106,16 @@ const createNoteTrashService = (deps) => ({
|
|
|
94
106
|
throw new NoteRestoreConflictError();
|
|
95
107
|
}
|
|
96
108
|
return deps.restoreDeletedNote(deletedNote);
|
|
109
|
+
},
|
|
110
|
+
purgeNoteById: async (id) => {
|
|
111
|
+
await deps.purgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);
|
|
112
|
+
const deletedNote = await deps.findDeletedNote(id);
|
|
113
|
+
if (!deletedNote) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const summary = serializeTrashedNote(deletedNote);
|
|
117
|
+
await deps.purgeDeletedNote(deletedNote);
|
|
118
|
+
return summary;
|
|
97
119
|
}
|
|
98
120
|
});
|
|
99
121
|
const includeDeletedNote = {
|
|
@@ -146,6 +168,9 @@ const noteTrashService = createNoteTrashService({
|
|
|
146
168
|
return Boolean(note);
|
|
147
169
|
},
|
|
148
170
|
purgeExpiredDeletedNotes: defaultPurgeExpiredDeletedNotes,
|
|
171
|
+
purgeDeletedNote: async (deletedNote) => {
|
|
172
|
+
await models.deletedNote.delete({ where: { id: deletedNote.id } });
|
|
173
|
+
},
|
|
149
174
|
moveNoteToTrash: async (note) => {
|
|
150
175
|
return models.$transaction(async (tx) => {
|
|
151
176
|
await tx.deletedNote.create({
|
|
@@ -217,6 +242,10 @@ const noteTrashService = createNoteTrashService({
|
|
|
217
242
|
id: deletedNote.id,
|
|
218
243
|
title: deletedNote.title,
|
|
219
244
|
content: restoredContent,
|
|
245
|
+
...buildNoteSearchProjection({
|
|
246
|
+
title: deletedNote.title,
|
|
247
|
+
content: restoredContent
|
|
248
|
+
}),
|
|
220
249
|
createdAt: deletedNote.createdAt,
|
|
221
250
|
updatedAt: deletedNote.updatedAt,
|
|
222
251
|
pinned: deletedNote.pinned,
|
|
@@ -243,20 +272,21 @@ const noteTrashService = createNoteTrashService({
|
|
|
243
272
|
}
|
|
244
273
|
});
|
|
245
274
|
const listTrashedNotes = noteTrashService.listTrashedNotes;
|
|
275
|
+
const getTrashedNoteById = noteTrashService.getNoteById;
|
|
246
276
|
const trashNoteById = noteTrashService.trashNoteById;
|
|
247
277
|
const restoreTrashedNoteById = noteTrashService.restoreNoteById;
|
|
278
|
+
const purgeTrashedNoteById = noteTrashService.purgeNoteById;
|
|
248
279
|
const purgeExpiredTrashedNotes = async () => {
|
|
249
|
-
return defaultPurgeExpiredDeletedNotes(
|
|
250
|
-
createRetentionCutoff(TRASH_RETENTION_DAYS),
|
|
251
|
-
RECOVERY_CLEANUP_BATCH_LIMIT
|
|
252
|
-
);
|
|
280
|
+
return defaultPurgeExpiredDeletedNotes(createRetentionCutoff(TRASH_RETENTION_DAYS), RECOVERY_CLEANUP_BATCH_LIMIT);
|
|
253
281
|
};
|
|
254
282
|
export {
|
|
255
283
|
NoteRestoreConflictError,
|
|
256
284
|
createNoteTrashService,
|
|
285
|
+
getTrashedNoteById,
|
|
257
286
|
listTrashedNotes,
|
|
258
287
|
purgeExpiredTrashedNotes,
|
|
288
|
+
purgeTrashedNoteById,
|
|
259
289
|
restoreTrashedNoteById,
|
|
260
290
|
trashNoteById
|
|
261
291
|
};
|
|
262
|
-
//# sourceMappingURL=
|
|
292
|
+
//# sourceMappingURL=trash.js.map
|