ocean-brain 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/package.json +1 -1
  2. package/server/client/dist/assets/{Calendar-DI0PhQTq.js → Calendar-BU9XgX2N.js} +1 -1
  3. package/server/client/dist/assets/Callout-C6tVCxOT.js +1 -0
  4. package/server/client/dist/assets/{Graph-Dhuog4tc.js → Graph-Dond2hGK.js} +1 -1
  5. package/server/client/dist/assets/{Image-DlJ4MDo3.js → Image-DxWhlIDG.js} +1 -1
  6. package/server/client/dist/assets/{Image.es-COwqkQuh.js → Image.es-BjGK7y6Y.js} +1 -1
  7. package/server/client/dist/assets/Note-Cr6GtXUM.js +21 -0
  8. package/server/client/dist/assets/{Plus.es-KeV96R_e.js → Plus.es-EcmtS_uw.js} +1 -1
  9. package/server/client/dist/assets/{Reminders-D7MCTNxb.js → Reminders-BlLLT_x0.js} +1 -1
  10. package/server/client/dist/assets/{Search-B-fwTr5y.js → Search-CrZ0ap8x.js} +1 -1
  11. package/server/client/dist/assets/{SurfaceCard-CHjcCuqZ.js → SurfaceCard-CO0oVxeL.js} +1 -1
  12. package/server/client/dist/assets/{Tag-BbV2z-Db.js → Tag-vws5AfNW.js} +1 -1
  13. package/server/client/dist/assets/{TagNotes-2HKF09Jm.js → TagNotes-Fmf1Kgim.js} +1 -1
  14. package/server/client/dist/assets/{Trash.es-BqRTZPrI.js → Trash.es-DmwfSy3l.js} +1 -1
  15. package/server/client/dist/assets/{ViewNotes-TfM3Uzbp.js → ViewNotes-BALN7IfH.js} +1 -1
  16. package/server/client/dist/assets/{Views-CUwtnfa8.js → Views-Ccc_Ml5U.js} +1 -1
  17. package/server/client/dist/assets/{image.api-CMdQbCyN.js → image.api-DeF9ck7g.js} +1 -1
  18. package/server/client/dist/assets/{index-zhxZjg7V.js → index-BSVE2cGd.js} +1 -1
  19. package/server/client/dist/assets/{index-D2gRB1ig.css → index-BZUGWdOU.css} +1 -1
  20. package/server/client/dist/assets/{index-B4l2UvuC.js → index-BvKBTjQq.js} +51 -37
  21. package/server/client/dist/assets/{manage-image-DdXb5Bkj.js → manage-image-IoTXaMBP.js} +1 -1
  22. package/server/client/dist/assets/{manage-image-detail-CrrXCQIX.js → manage-image-detail-BRTXwOsh.js} +1 -1
  23. package/server/client/dist/assets/{mcp-DqGLZv90.js → mcp-Bh743HBz.js} +1 -1
  24. package/server/client/dist/assets/note-core-Z6kqulGB.js +180 -0
  25. package/server/client/dist/assets/note-vendor-i1M2FSI2.js +54 -0
  26. package/server/client/dist/assets/{placeholder-BXLtvdtN.js → placeholder-XQqo1Fvr.js} +3 -3
  27. package/server/client/dist/assets/{trash-3eUAjFRh.js → trash-hiJIg6-c.js} +1 -1
  28. package/server/client/dist/assets/{useReminderMutate-v1EXqM90.js → useReminderMutate-D21PurIS.js} +1 -1
  29. package/server/client/dist/assets/{view-dashboard-T6gmohr-.js → view-dashboard-BuaZ4MvI.js} +1 -1
  30. package/server/client/dist/index.html +4 -4
  31. package/server/dist/features/note/graphql/note.field.resolver.js +14 -1
  32. package/server/dist/features/note/graphql/note.field.resolver.js.map +1 -1
  33. package/server/dist/features/note/graphql/note.mutation.resolver.js +73 -36
  34. package/server/dist/features/note/graphql/note.mutation.resolver.js.map +1 -1
  35. package/server/dist/features/note/graphql/note.query.resolver.js +74 -76
  36. package/server/dist/features/note/graphql/note.query.resolver.js.map +1 -1
  37. package/server/dist/features/note/graphql/note.resolver.js +3 -2
  38. package/server/dist/features/note/graphql/note.resolver.js.map +1 -1
  39. package/server/dist/features/note/graphql/note.type-defs.js +4 -1
  40. package/server/dist/features/note/graphql/note.type-defs.js.map +1 -1
  41. package/server/dist/features/note/services/cleanup.js +4 -3
  42. package/server/dist/features/note/services/cleanup.js.map +1 -1
  43. package/server/dist/features/note/services/content-blocks.js +113 -0
  44. package/server/dist/features/note/services/content-blocks.js.map +1 -0
  45. package/server/dist/features/note/services/search.js +10 -29
  46. package/server/dist/features/note/services/search.js.map +1 -1
  47. package/server/dist/features/note/services/snapshot.js +172 -12
  48. package/server/dist/features/note/services/snapshot.js.map +1 -1
  49. package/server/dist/features/note/services/write-conflict.js +87 -0
  50. package/server/dist/features/note/services/write-conflict.js.map +1 -0
  51. package/server/dist/features/note/services/write.js +107 -0
  52. package/server/dist/features/note/services/write.js.map +1 -0
  53. package/server/dist/modules/blocknote-tree.js +41 -0
  54. package/server/dist/modules/blocknote-tree.js.map +1 -0
  55. package/server/dist/modules/error-handler.js +7 -4
  56. package/server/dist/modules/error-handler.js.map +1 -1
  57. package/server/dist/modules/recovery-retention.js +2 -2
  58. package/server/dist/modules/recovery-retention.js.map +1 -1
  59. package/server/client/dist/assets/Callout-BAqbXI0L.js +0 -1
  60. package/server/client/dist/assets/Note-ChUWU3Su.js +0 -21
  61. package/server/client/dist/assets/note-core-GgFcksAF.js +0 -178
  62. package/server/client/dist/assets/note-vendor-B0t5F04o.js +0 -54
  63. package/server/dist/features/note/graphql/note.graphql.shared.js +0 -23
  64. package/server/dist/features/note/graphql/note.graphql.shared.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/features/note/graphql/note.query.resolver.ts"],"sourcesContent":["import type { IResolvers } from '@graphql-tools/utils';\nimport { getNoteCleanupPreview, listNoteCleanupCandidates } from '~/features/note/services/cleanup.js';\nimport {\n filterNotesBySearchQuery,\n NOTE_SEARCH_TEXT_SCHEMA_VERSION,\n parseNoteSearchQuery,\n} from '~/features/note/services/search.js';\nimport { listNoteSnapshots } from '~/features/note/services/snapshot.js';\nimport {\n buildNoteTagNamesWhere,\n type NoteTagMatchMode,\n normalizeNoteTagNames,\n} from '~/features/note/services/tag-filter.js';\nimport { getTrashedNoteById, listTrashedNotes } from '~/features/note/services/trash.js';\nimport type { Note, Prisma } from '~/models.js';\nimport models from '~/models.js';\nimport { runDataMaintenanceInBackground } from '~/modules/data-maintenance.js';\nimport type { Pagination, SearchFilter } from '~/types/index.js';\nimport { extractBlocksByType } from './note.graphql.shared.js';\n\ninterface AllNotesResolverDeps {\n countNotes: (args: { where?: Prisma.NoteWhereInput }) => Promise<number>;\n findNotes: (args: {\n orderBy: Prisma.NoteOrderByWithRelationInput[];\n where?: Prisma.NoteWhereInput;\n take?: number;\n skip?: number;\n }) => Promise<Note[]>;\n triggerSearchBackfill: () => void;\n}\n\nconst buildAllNotesOrderBy = (searchFilter: SearchFilter) => {\n const sortBy = searchFilter.sortBy || 'updatedAt';\n const sortOrder = searchFilter.sortOrder || 'desc';\n const pinnedFirst = searchFilter.pinnedFirst || false;\n const orderBy: Prisma.NoteOrderByWithRelationInput[] = [];\n\n if (pinnedFirst) {\n orderBy.push({ pinned: 'desc' });\n }\n\n if (sortBy === 'createdAt') {\n orderBy.push({ createdAt: sortOrder as 'asc' | 'desc' });\n } else {\n orderBy.push({ updatedAt: sortOrder as 'asc' | 'desc' });\n }\n\n return orderBy;\n};\n\nconst buildAllNotesSearchWhere = (searchFilter: SearchFilter) => {\n const parsedQuery = parseNoteSearchQuery(searchFilter.query);\n\n if (!parsedQuery.hasFilters) {\n return undefined;\n }\n\n return {\n AND: [\n { searchableTextVersion: NOTE_SEARCH_TEXT_SCHEMA_VERSION },\n ...parsedQuery.included.map((keyword) => ({ searchableText: { contains: keyword } })),\n ...parsedQuery.excluded.map((keyword) => ({ NOT: { searchableText: { contains: keyword } } })),\n ],\n } satisfies Prisma.NoteWhereInput;\n};\n\nconst buildAllNotesStaleCandidateWhere = (searchFilter: SearchFilter) => {\n const parsedQuery = parseNoteSearchQuery(searchFilter.query);\n\n return {\n AND: [\n { searchableTextVersion: { not: NOTE_SEARCH_TEXT_SCHEMA_VERSION } },\n ...parsedQuery.included.map((keyword) => ({\n OR: [{ title: { contains: keyword } }, { content: { contains: keyword } }],\n })),\n ],\n } satisfies Prisma.NoteWhereInput;\n};\n\nconst compareNotesForSearch = (left: Note, right: Note, searchFilter: SearchFilter) => {\n if (searchFilter.pinnedFirst && left.pinned !== right.pinned) {\n return left.pinned ? -1 : 1;\n }\n\n const sortBy = searchFilter.sortBy || 'updatedAt';\n const sortOrder = searchFilter.sortOrder || 'desc';\n const leftValue = sortBy === 'createdAt' ? left.createdAt.getTime() : left.updatedAt.getTime();\n const rightValue = sortBy === 'createdAt' ? right.createdAt.getTime() : right.updatedAt.getTime();\n\n if (leftValue === rightValue) {\n return 0;\n }\n\n return sortOrder === 'asc' ? leftValue - rightValue : rightValue - leftValue;\n};\n\nconst mergeSortedNotesForSearch = (left: Note[], right: Note[], searchFilter: SearchFilter) => {\n const merged: Note[] = [];\n let leftIndex = 0;\n let rightIndex = 0;\n\n while (leftIndex < left.length && rightIndex < right.length) {\n if (compareNotesForSearch(left[leftIndex], right[rightIndex], searchFilter) <= 0) {\n merged.push(left[leftIndex]);\n leftIndex += 1;\n } else {\n merged.push(right[rightIndex]);\n rightIndex += 1;\n }\n }\n\n while (leftIndex < left.length) {\n merged.push(left[leftIndex]);\n leftIndex += 1;\n }\n\n while (rightIndex < right.length) {\n merged.push(right[rightIndex]);\n rightIndex += 1;\n }\n\n return merged;\n};\n\nexport const createAllNotesQueryResolver = (\n deps: AllNotesResolverDeps = {\n countNotes: ({ where }) => models.note.count({ where }),\n findNotes: ({ orderBy, where, take, skip }) =>\n models.note.findMany({\n orderBy,\n where,\n take,\n skip,\n }),\n triggerSearchBackfill: () => {\n void runDataMaintenanceInBackground();\n },\n },\n) => {\n return async (\n _: unknown,\n {\n searchFilter,\n pagination,\n }: {\n searchFilter: SearchFilter;\n pagination: Pagination;\n },\n ) => {\n const orderBy = buildAllNotesOrderBy(searchFilter);\n const limit = Number(pagination.limit);\n const offset = Number(pagination.offset);\n const where = buildAllNotesSearchWhere(searchFilter);\n\n if (!where) {\n const [notes, totalCount] = await Promise.all([\n deps.findNotes({\n orderBy,\n take: limit,\n skip: offset,\n }),\n deps.countNotes({}),\n ]);\n\n return {\n totalCount,\n notes,\n };\n }\n\n const staleCandidateNotes = await deps.findNotes({\n orderBy,\n where: buildAllNotesStaleCandidateWhere(searchFilter),\n });\n\n if (staleCandidateNotes.length === 0) {\n const [notes, totalCount] = await Promise.all([\n deps.findNotes({\n orderBy,\n where,\n take: limit,\n skip: offset,\n }),\n deps.countNotes({ where }),\n ]);\n\n return {\n totalCount,\n notes,\n };\n }\n\n deps.triggerSearchBackfill();\n\n const parsedQuery = parseNoteSearchQuery(searchFilter.query);\n const staleNotes = filterNotesBySearchQuery(staleCandidateNotes, parsedQuery);\n\n if (staleNotes.length === 0) {\n const [notes, totalCount] = await Promise.all([\n deps.findNotes({\n orderBy,\n where,\n take: limit,\n skip: offset,\n }),\n deps.countNotes({ where }),\n ]);\n\n return {\n totalCount,\n notes,\n };\n }\n\n const [freshNotesPrefix, freshTotalCount] = await Promise.all([\n deps.findNotes({\n orderBy,\n where,\n take: offset + limit,\n }),\n deps.countNotes({ where }),\n ]);\n\n const mergedNotes = mergeSortedNotesForSearch(freshNotesPrefix, staleNotes, searchFilter);\n\n return {\n totalCount: freshTotalCount + staleNotes.length,\n notes: mergedNotes.slice(offset, offset + limit),\n };\n };\n};\n\ntype NoteQueryResolvers = NonNullable<IResolvers['Query']>;\n\nexport const noteQueryResolvers: NoteQueryResolvers = {\n allNotes: createAllNotesQueryResolver(),\n notesInDateRange: async (\n _,\n {\n dateRange,\n }: {\n dateRange: {\n start: string;\n end: string;\n };\n },\n ) => {\n const where: Prisma.NoteWhereInput = {\n OR: [\n {\n updatedAt: {\n gte: new Date(dateRange.start),\n lte: new Date(dateRange.end),\n },\n },\n {\n createdAt: {\n gte: new Date(dateRange.start),\n lte: new Date(dateRange.end),\n },\n },\n ],\n };\n\n return models.note.findMany({\n orderBy: { createdAt: 'asc' },\n where,\n });\n },\n tagNotes: async (\n _,\n {\n searchFilter,\n pagination,\n }: {\n searchFilter: SearchFilter;\n pagination: Pagination;\n },\n ) => {\n const where: Prisma.NoteWhereInput = { tags: { some: { id: Number(searchFilter.query) } } };\n const notes = models.note.findMany({\n orderBy: { updatedAt: 'desc' },\n where,\n take: Number(pagination.limit),\n skip: Number(pagination.offset),\n });\n\n return {\n totalCount: models.note.count({ where }),\n notes,\n };\n },\n notesByTagNames: async (\n _,\n {\n tagNames,\n mode,\n pagination,\n }: {\n tagNames: string[];\n mode: NoteTagMatchMode;\n pagination: Pagination;\n },\n ) => {\n const normalizedTagNames = normalizeNoteTagNames(tagNames);\n\n if (normalizedTagNames.length === 0) {\n return {\n totalCount: 0,\n notes: [],\n };\n }\n\n const where = buildNoteTagNamesWhere(normalizedTagNames, mode);\n const notes = models.note.findMany({\n orderBy: { updatedAt: 'desc' },\n where,\n take: Number(pagination.limit),\n skip: Number(pagination.offset),\n });\n\n return {\n totalCount: models.note.count({ where }),\n notes,\n };\n },\n pinnedNotes: async () =>\n models.note.findMany({\n orderBy: [{ order: 'asc' }, { updatedAt: 'desc' }],\n where: { pinned: true },\n }),\n imageNotes: async (_, { src }: { src: string }) =>\n models.note.findMany({\n orderBy: { updatedAt: 'desc' },\n where: { content: { contains: src } },\n }),\n backReferences: async (_, { id }: { id: string }) => {\n return models.note.findMany({\n orderBy: [{ pinned: 'desc' }, { updatedAt: 'desc' }],\n where: { content: { contains: `reference\",\"props\":{\"id\":\"${id}\"` } },\n });\n },\n note: async (_, { id }: { id: string }) => {\n const note = await models.note.findUnique({ where: { id: Number(id) } });\n\n if (!note) {\n throw 'NOT FOUND';\n }\n\n if (!note.content) {\n return note;\n }\n\n const blocks = extractBlocksByType<{\n id: string;\n title: string;\n }>('reference', JSON.parse(note.content));\n\n if (blocks.length === 0) {\n return note;\n }\n\n const referenceIds = blocks.map((block) => Number(block.props.id));\n const references = await models.note.findMany({ where: { id: { in: referenceIds } } });\n const newContent = references.reduce<string>((content, referenceNote: Note) => {\n const reference = blocks.find((block) => Number(block.props.id) === referenceNote.id);\n\n if (reference && reference.props.title !== referenceNote.title) {\n return content.replace(\n `reference\",\"props\":{\"id\":\"${reference.props.id}\",\"title\":\"${reference.props.title}\"`,\n `reference\",\"props\":{\"id\":\"${referenceNote.id}\",\"title\":\"${referenceNote.title}\"`,\n );\n }\n\n return content;\n }, note.content);\n\n if (newContent === note.content) {\n return note;\n }\n\n try {\n JSON.parse(newContent);\n\n return await models.note.update({\n where: { id: note.id },\n data: { content: newContent },\n });\n } catch {\n // Keep the stored content unchanged if the synchronized payload becomes invalid.\n return note;\n }\n },\n noteCleanupCandidates: async (\n _,\n {\n query,\n pagination = {\n limit: 20,\n offset: 0,\n },\n }: {\n query?: string;\n pagination: Pagination;\n },\n ) => {\n const result = await listNoteCleanupCandidates({\n keywords: query\n ? query\n .split(/[,\\s]+/)\n .map((keyword) => keyword.trim())\n .filter(Boolean)\n : undefined,\n limit: Number(pagination.limit),\n offset: Number(pagination.offset),\n });\n\n return result.notes;\n },\n noteCleanupPreview: async (_, { id }: { id: string }) => {\n return getNoteCleanupPreview(Number(id));\n },\n noteSnapshots: async (\n _,\n {\n id,\n limit = 5,\n }: {\n id: string;\n limit?: number;\n },\n ) => {\n return listNoteSnapshots(Number(id), Number(limit));\n },\n trashedNote: async (_, { id }: { id: string }) => {\n return getTrashedNoteById(Number(id));\n },\n trashedNotes: async (\n _,\n {\n pagination = {\n limit: 25,\n offset: 0,\n },\n }: {\n pagination: Pagination;\n },\n ) => {\n return listTrashedNotes({\n limit: Number(pagination.limit),\n offset: Number(pagination.offset),\n });\n },\n noteGraph: async () => {\n const notes = await models.note.findMany({\n select: {\n id: true,\n title: true,\n content: true,\n },\n });\n\n const nodes: Array<{ id: string; title: string; connections: number }> = [];\n const links: Array<{ source: string; target: string }> = [];\n const connectionCount: Record<string, number> = {};\n const linkSet = new Set<string>();\n\n for (const note of notes) {\n if (!note.content) {\n continue;\n }\n\n try {\n const blocks = extractBlocksByType<{ id: string }>('reference', JSON.parse(note.content));\n\n for (const block of blocks) {\n const targetId = block.props.id;\n\n if (targetId && String(note.id) !== targetId) {\n const linkKey = `${note.id}-${targetId}`;\n const reverseLinkKey = `${targetId}-${note.id}`;\n\n if (!linkSet.has(linkKey) && !linkSet.has(reverseLinkKey)) {\n linkSet.add(linkKey);\n links.push({\n source: String(note.id),\n target: targetId,\n });\n connectionCount[String(note.id)] = (connectionCount[String(note.id)] || 0) + 1;\n connectionCount[targetId] = (connectionCount[targetId] || 0) + 1;\n }\n }\n }\n } catch {\n // Skip notes with invalid JSON content\n }\n }\n\n for (const note of notes) {\n nodes.push({\n id: String(note.id),\n title: note.title || 'Untitled',\n connections: connectionCount[String(note.id)] || 0,\n });\n }\n\n return {\n nodes,\n links,\n };\n },\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/features/note/graphql/note.query.resolver.ts"],"sourcesContent":["import type { IResolvers } from '@graphql-tools/utils';\nimport { getNoteCleanupPreview, listNoteCleanupCandidates } from '~/features/note/services/cleanup.js';\nimport {\n buildNoteGraph,\n contentReferencesNote,\n extractReferenceBlocksFromContent,\n normalizeReferenceId,\n syncReferenceTitlesInContent,\n} from '~/features/note/services/content-blocks.js';\nimport {\n buildNoteSearchProjection,\n filterNotesBySearchQuery,\n NOTE_SEARCH_TEXT_SCHEMA_VERSION,\n parseNoteSearchQuery,\n} from '~/features/note/services/search.js';\nimport { getNoteSnapshot, listNoteSnapshots } from '~/features/note/services/snapshot.js';\nimport {\n buildNoteTagNamesWhere,\n type NoteTagMatchMode,\n normalizeNoteTagNames,\n} from '~/features/note/services/tag-filter.js';\nimport { getTrashedNoteById, listTrashedNotes } from '~/features/note/services/trash.js';\nimport type { Note, Prisma } from '~/models.js';\nimport models from '~/models.js';\nimport { runDataMaintenanceInBackground } from '~/modules/data-maintenance.js';\nimport type { Pagination, SearchFilter } from '~/types/index.js';\n\ninterface AllNotesResolverDeps {\n countNotes: (args: { where?: Prisma.NoteWhereInput }) => Promise<number>;\n findNotes: (args: {\n orderBy: Prisma.NoteOrderByWithRelationInput[];\n where?: Prisma.NoteWhereInput;\n take?: number;\n skip?: number;\n }) => Promise<Note[]>;\n triggerSearchBackfill: () => void;\n}\n\nconst buildAllNotesOrderBy = (searchFilter: SearchFilter) => {\n const sortBy = searchFilter.sortBy || 'updatedAt';\n const sortOrder = searchFilter.sortOrder || 'desc';\n const pinnedFirst = searchFilter.pinnedFirst || false;\n const orderBy: Prisma.NoteOrderByWithRelationInput[] = [];\n\n if (pinnedFirst) {\n orderBy.push({ pinned: 'desc' });\n }\n\n if (sortBy === 'createdAt') {\n orderBy.push({ createdAt: sortOrder as 'asc' | 'desc' });\n } else {\n orderBy.push({ updatedAt: sortOrder as 'asc' | 'desc' });\n }\n\n return orderBy;\n};\n\nconst buildAllNotesSearchWhere = (searchFilter: SearchFilter) => {\n const parsedQuery = parseNoteSearchQuery(searchFilter.query);\n\n if (!parsedQuery.hasFilters) {\n return undefined;\n }\n\n return {\n AND: [\n { searchableTextVersion: NOTE_SEARCH_TEXT_SCHEMA_VERSION },\n ...parsedQuery.included.map((keyword) => ({ searchableText: { contains: keyword } })),\n ...parsedQuery.excluded.map((keyword) => ({ NOT: { searchableText: { contains: keyword } } })),\n ],\n } satisfies Prisma.NoteWhereInput;\n};\n\nconst buildAllNotesStaleCandidateWhere = (searchFilter: SearchFilter) => {\n const parsedQuery = parseNoteSearchQuery(searchFilter.query);\n\n return {\n AND: [\n { searchableTextVersion: { not: NOTE_SEARCH_TEXT_SCHEMA_VERSION } },\n ...parsedQuery.included.map((keyword) => ({\n OR: [{ title: { contains: keyword } }, { content: { contains: keyword } }],\n })),\n ],\n } satisfies Prisma.NoteWhereInput;\n};\n\nconst compareNotesForSearch = (left: Note, right: Note, searchFilter: SearchFilter) => {\n if (searchFilter.pinnedFirst && left.pinned !== right.pinned) {\n return left.pinned ? -1 : 1;\n }\n\n const sortBy = searchFilter.sortBy || 'updatedAt';\n const sortOrder = searchFilter.sortOrder || 'desc';\n const leftValue = sortBy === 'createdAt' ? left.createdAt.getTime() : left.updatedAt.getTime();\n const rightValue = sortBy === 'createdAt' ? right.createdAt.getTime() : right.updatedAt.getTime();\n\n if (leftValue === rightValue) {\n return 0;\n }\n\n return sortOrder === 'asc' ? leftValue - rightValue : rightValue - leftValue;\n};\n\nconst mergeSortedNotesForSearch = (left: Note[], right: Note[], searchFilter: SearchFilter) => {\n const merged: Note[] = [];\n let leftIndex = 0;\n let rightIndex = 0;\n\n while (leftIndex < left.length && rightIndex < right.length) {\n if (compareNotesForSearch(left[leftIndex], right[rightIndex], searchFilter) <= 0) {\n merged.push(left[leftIndex]);\n leftIndex += 1;\n } else {\n merged.push(right[rightIndex]);\n rightIndex += 1;\n }\n }\n\n while (leftIndex < left.length) {\n merged.push(left[leftIndex]);\n leftIndex += 1;\n }\n\n while (rightIndex < right.length) {\n merged.push(right[rightIndex]);\n rightIndex += 1;\n }\n\n return merged;\n};\n\nconst isRecordNotFoundError = (error: unknown) => {\n return (\n typeof error === 'object' && error !== null && 'code' in error && (error as { code?: unknown }).code === 'P2025'\n );\n};\n\nexport const createAllNotesQueryResolver = (\n deps: AllNotesResolverDeps = {\n countNotes: ({ where }) => models.note.count({ where }),\n findNotes: ({ orderBy, where, take, skip }) =>\n models.note.findMany({\n orderBy,\n where,\n take,\n skip,\n }),\n triggerSearchBackfill: () => {\n void runDataMaintenanceInBackground();\n },\n },\n) => {\n return async (\n _: unknown,\n {\n searchFilter,\n pagination,\n }: {\n searchFilter: SearchFilter;\n pagination: Pagination;\n },\n ) => {\n const orderBy = buildAllNotesOrderBy(searchFilter);\n const limit = Number(pagination.limit);\n const offset = Number(pagination.offset);\n const where = buildAllNotesSearchWhere(searchFilter);\n\n if (!where) {\n const [notes, totalCount] = await Promise.all([\n deps.findNotes({\n orderBy,\n take: limit,\n skip: offset,\n }),\n deps.countNotes({}),\n ]);\n\n return {\n totalCount,\n notes,\n };\n }\n\n const staleCandidateNotes = await deps.findNotes({\n orderBy,\n where: buildAllNotesStaleCandidateWhere(searchFilter),\n });\n\n if (staleCandidateNotes.length === 0) {\n const [notes, totalCount] = await Promise.all([\n deps.findNotes({\n orderBy,\n where,\n take: limit,\n skip: offset,\n }),\n deps.countNotes({ where }),\n ]);\n\n return {\n totalCount,\n notes,\n };\n }\n\n deps.triggerSearchBackfill();\n\n const parsedQuery = parseNoteSearchQuery(searchFilter.query);\n const staleNotes = filterNotesBySearchQuery(staleCandidateNotes, parsedQuery);\n\n if (staleNotes.length === 0) {\n const [notes, totalCount] = await Promise.all([\n deps.findNotes({\n orderBy,\n where,\n take: limit,\n skip: offset,\n }),\n deps.countNotes({ where }),\n ]);\n\n return {\n totalCount,\n notes,\n };\n }\n\n const [freshNotesPrefix, freshTotalCount] = await Promise.all([\n deps.findNotes({\n orderBy,\n where,\n take: offset + limit,\n }),\n deps.countNotes({ where }),\n ]);\n\n const mergedNotes = mergeSortedNotesForSearch(freshNotesPrefix, staleNotes, searchFilter);\n\n return {\n totalCount: freshTotalCount + staleNotes.length,\n notes: mergedNotes.slice(offset, offset + limit),\n };\n };\n};\n\ntype NoteQueryResolvers = NonNullable<IResolvers['Query']>;\n\ninterface BackReferencesQueryResolverDeps {\n findCandidateNotes: (noteId: number) => Promise<Note[]>;\n}\n\nexport const createBackReferencesQueryResolver = (\n deps: BackReferencesQueryResolverDeps = {\n findCandidateNotes: (noteId) =>\n models.note.findMany({\n orderBy: [{ pinned: 'desc' }, { updatedAt: 'desc' }],\n where: {\n NOT: { id: noteId },\n },\n }),\n },\n) => {\n return async (_: unknown, { id }: { id: string }) => {\n const notes = await deps.findCandidateNotes(Number(id));\n\n return notes.filter((note) => contentReferencesNote(note.content, id));\n };\n};\n\ninterface NoteGraphQueryResolverDeps {\n findNotes: () => Promise<Array<Pick<Note, 'id' | 'title' | 'content'>>>;\n}\n\nexport const createNoteGraphQueryResolver = (\n deps: NoteGraphQueryResolverDeps = {\n findNotes: () =>\n models.note.findMany({\n select: {\n id: true,\n title: true,\n content: true,\n },\n }),\n },\n) => {\n return async () => {\n const notes = await deps.findNotes();\n\n return buildNoteGraph(notes);\n };\n};\n\nexport const noteQueryResolvers: NoteQueryResolvers = {\n allNotes: createAllNotesQueryResolver(),\n notesInDateRange: async (\n _,\n {\n dateRange,\n }: {\n dateRange: {\n start: string;\n end: string;\n };\n },\n ) => {\n const where: Prisma.NoteWhereInput = {\n OR: [\n {\n updatedAt: {\n gte: new Date(dateRange.start),\n lte: new Date(dateRange.end),\n },\n },\n {\n createdAt: {\n gte: new Date(dateRange.start),\n lte: new Date(dateRange.end),\n },\n },\n ],\n };\n\n return models.note.findMany({\n orderBy: { createdAt: 'asc' },\n where,\n });\n },\n tagNotes: async (\n _,\n {\n searchFilter,\n pagination,\n }: {\n searchFilter: SearchFilter;\n pagination: Pagination;\n },\n ) => {\n const where: Prisma.NoteWhereInput = { tags: { some: { id: Number(searchFilter.query) } } };\n const notes = models.note.findMany({\n orderBy: { updatedAt: 'desc' },\n where,\n take: Number(pagination.limit),\n skip: Number(pagination.offset),\n });\n\n return {\n totalCount: models.note.count({ where }),\n notes,\n };\n },\n notesByTagNames: async (\n _,\n {\n tagNames,\n mode,\n pagination,\n }: {\n tagNames: string[];\n mode: NoteTagMatchMode;\n pagination: Pagination;\n },\n ) => {\n const normalizedTagNames = normalizeNoteTagNames(tagNames);\n\n if (normalizedTagNames.length === 0) {\n return {\n totalCount: 0,\n notes: [],\n };\n }\n\n const where = buildNoteTagNamesWhere(normalizedTagNames, mode);\n const notes = models.note.findMany({\n orderBy: { updatedAt: 'desc' },\n where,\n take: Number(pagination.limit),\n skip: Number(pagination.offset),\n });\n\n return {\n totalCount: models.note.count({ where }),\n notes,\n };\n },\n pinnedNotes: async () =>\n models.note.findMany({\n orderBy: [{ order: 'asc' }, { updatedAt: 'desc' }],\n where: { pinned: true },\n }),\n imageNotes: async (_, { src }: { src: string }) =>\n models.note.findMany({\n orderBy: { updatedAt: 'desc' },\n where: { content: { contains: src } },\n }),\n backReferences: createBackReferencesQueryResolver(),\n note: async (_, { id }: { id: string }) => {\n const note = await models.note.findUnique({ where: { id: Number(id) } });\n\n if (!note) {\n throw 'NOT FOUND';\n }\n\n if (!note.content) {\n return note;\n }\n\n const blocks = extractReferenceBlocksFromContent(note.content);\n\n if (blocks.length === 0) {\n return note;\n }\n\n const referenceIds = Array.from(\n new Set(\n blocks\n .map((block) => normalizeReferenceId(block.props?.id))\n .filter((referenceId): referenceId is string => referenceId !== null)\n .map(Number)\n .filter(Number.isFinite),\n ),\n );\n const references = await models.note.findMany({ where: { id: { in: referenceIds } } });\n const titlesById = new Map(\n references.map((referenceNote: Note) => [String(referenceNote.id), referenceNote.title]),\n );\n const newContent = syncReferenceTitlesInContent(note.content, titlesById);\n\n if (!newContent || newContent === note.content) {\n return note;\n }\n\n try {\n return await models.note.update({\n where: {\n id: note.id,\n updatedAt: note.updatedAt,\n },\n data: {\n content: newContent,\n ...buildNoteSearchProjection({\n title: note.title,\n content: newContent,\n }),\n },\n });\n } catch (error) {\n if (isRecordNotFoundError(error)) {\n return note;\n }\n\n throw error;\n }\n },\n noteCleanupCandidates: async (\n _,\n {\n query,\n pagination = {\n limit: 20,\n offset: 0,\n },\n }: {\n query?: string;\n pagination: Pagination;\n },\n ) => {\n const result = await listNoteCleanupCandidates({\n keywords: query\n ? query\n .split(/[,\\s]+/)\n .map((keyword) => keyword.trim())\n .filter(Boolean)\n : undefined,\n limit: Number(pagination.limit),\n offset: Number(pagination.offset),\n });\n\n return result.notes;\n },\n noteCleanupPreview: async (_, { id }: { id: string }) => {\n return getNoteCleanupPreview(Number(id));\n },\n noteSnapshots: async (\n _,\n {\n id,\n limit = 10,\n }: {\n id: string;\n limit?: number;\n },\n ) => {\n return listNoteSnapshots(Number(id), Number(limit));\n },\n noteSnapshot: async (_, { id }: { id: string }) => {\n return getNoteSnapshot(Number(id));\n },\n trashedNote: async (_, { id }: { id: string }) => {\n return getTrashedNoteById(Number(id));\n },\n trashedNotes: async (\n _,\n {\n pagination = {\n limit: 25,\n offset: 0,\n },\n }: {\n pagination: Pagination;\n },\n ) => {\n return listTrashedNotes({\n limit: Number(pagination.limit),\n offset: Number(pagination.offset),\n });\n },\n noteGraph: createNoteGraphQueryResolver(),\n};\n"],"mappings":"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","names":[]}
@@ -1,11 +1,12 @@
1
- import { noteFieldResolvers } from "./note.field.resolver.js";
1
+ import { noteFieldResolvers, noteSnapshotFieldResolvers } from "./note.field.resolver.js";
2
2
  import { noteMutationResolvers } from "./note.mutation.resolver.js";
3
3
  import { noteQueryResolvers } from "./note.query.resolver.js";
4
4
  import { createAllNotesQueryResolver as createAllNotesQueryResolver2 } from "./note.query.resolver.js";
5
5
  const noteResolvers = {
6
6
  Query: noteQueryResolvers,
7
7
  Mutation: noteMutationResolvers,
8
- Note: noteFieldResolvers
8
+ Note: noteFieldResolvers,
9
+ NoteSnapshot: noteSnapshotFieldResolvers
9
10
  };
10
11
  export {
11
12
  createAllNotesQueryResolver2 as createAllNotesQueryResolver,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/features/note/graphql/note.resolver.ts"],"sourcesContent":["import type { IResolvers } from '@graphql-tools/utils';\nimport { noteFieldResolvers } from './note.field.resolver.js';\nimport { noteMutationResolvers } from './note.mutation.resolver.js';\nimport { createAllNotesQueryResolver, noteQueryResolvers } from './note.query.resolver.js';\n\nexport { createAllNotesQueryResolver } from './note.query.resolver.js';\n\nexport const noteResolvers: IResolvers = {\n Query: noteQueryResolvers,\n Mutation: noteMutationResolvers,\n Note: noteFieldResolvers,\n};\n"],"mappings":"AACA,SAAS,0BAA0B;AACnC,SAAS,6BAA6B;AACtC,SAAsC,0BAA0B;AAEhE,SAAS,+BAAAA,oCAAmC;AAErC,MAAM,gBAA4B;AAAA,EACrC,OAAO;AAAA,EACP,UAAU;AAAA,EACV,MAAM;AACV;","names":["createAllNotesQueryResolver"]}
1
+ {"version":3,"sources":["../../../../src/features/note/graphql/note.resolver.ts"],"sourcesContent":["import type { IResolvers } from '@graphql-tools/utils';\nimport { noteFieldResolvers, noteSnapshotFieldResolvers } from './note.field.resolver.js';\nimport { noteMutationResolvers } from './note.mutation.resolver.js';\nimport { createAllNotesQueryResolver, noteQueryResolvers } from './note.query.resolver.js';\n\nexport { createAllNotesQueryResolver } from './note.query.resolver.js';\n\nexport const noteResolvers: IResolvers = {\n Query: noteQueryResolvers,\n Mutation: noteMutationResolvers,\n Note: noteFieldResolvers,\n NoteSnapshot: noteSnapshotFieldResolvers,\n};\n"],"mappings":"AACA,SAAS,oBAAoB,kCAAkC;AAC/D,SAAS,6BAA6B;AACtC,SAAsC,0BAA0B;AAEhE,SAAS,+BAAAA,oCAAmC;AAErC,MAAM,gBAA4B;AAAA,EACrC,OAAO;AAAA,EACP,UAAU;AAAA,EACV,MAAM;AAAA,EACN,cAAc;AAClB;","names":["createAllNotesQueryResolver"]}
@@ -104,6 +104,8 @@ const noteType = gql`
104
104
  type NoteSnapshot {
105
105
  id: ID!
106
106
  title: String!
107
+ contentPreview: String!
108
+ contentAsMarkdown: String!
107
109
  createdAt: String!
108
110
  meta: NoteSnapshotMeta!
109
111
  }
@@ -156,6 +158,7 @@ const noteQuery = gql`
156
158
  noteCleanupCandidates(query: String, pagination: PaginationInput): [NoteCleanupCandidate!]!
157
159
  noteCleanupPreview(id: ID!): NoteCleanupPreview
158
160
  noteSnapshots(id: ID!, limit: Int): [NoteSnapshot!]!
161
+ noteSnapshot(id: ID!): NoteSnapshot
159
162
  trashedNote(id: ID!): DeletedNote
160
163
  trashedNotes(pagination: PaginationInput): DeletedNotes!
161
164
  noteGraph: NoteGraph!
@@ -164,7 +167,7 @@ const noteQuery = gql`
164
167
  const noteMutation = gql`
165
168
  type Mutation {
166
169
  createNote(note: NoteInput!): Note!
167
- updateNote(id: ID!, note: NoteInput!, editSessionId: String): Note!
170
+ updateNote(id: ID!, note: NoteInput!, editSessionId: String, expectedUpdatedAt: String, force: Boolean): Note!
168
171
  deleteNote(id: ID!): Boolean!
169
172
  restoreNoteSnapshot(id: ID!): Note!
170
173
  restoreTrashedNote(id: ID!): Note!
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/features/note/graphql/note.type-defs.ts"],"sourcesContent":["import { gql } from '~/modules/graphql.js';\n\nexport const noteType = gql`\n input PaginationInput {\n limit: Int!\n offset: Int!\n }\n\n input SearchFilterInput {\n query: String!\n sortBy: String\n sortOrder: String\n pinnedFirst: Boolean\n }\n\n input DateRangeInput {\n start: String!\n end: String!\n }\n\n enum NoteLayout {\n narrow\n wide\n full\n }\n\n enum TagMatchMode {\n and\n or\n }\n\n input NoteInput {\n title: String\n content: String\n layout: NoteLayout\n }\n\n input NoteOrderInput {\n id: ID!\n order: Int!\n }\n\n type Tag {\n id: ID!\n name: String!\n createdAt: String!\n updatedAt: String!\n }\n\n type Note {\n id: ID!\n title: String!\n content: String!\n contentAsMarkdown: String!\n createdAt: String!\n updatedAt: String!\n pinned: Boolean!\n order: Int!\n layout: NoteLayout!\n tags: [Tag!]!\n }\n\n type Notes {\n totalCount: Int!\n notes: [Note!]!\n }\n\n type NoteCleanupBackReference {\n id: ID!\n title: String!\n }\n\n type NoteCleanupCandidate {\n id: ID!\n title: String!\n updatedAt: String!\n pinned: Boolean!\n tagNames: [String!]!\n reminderCount: Int!\n backReferenceCount: Int!\n matchedTerms: [String!]!\n reasons: [String!]!\n requiresForce: Boolean!\n forceReasons: [String!]!\n }\n\n type NoteCleanupPreview {\n id: ID!\n title: String!\n updatedAt: String!\n pinned: Boolean!\n tagNames: [String!]!\n reminderCount: Int!\n backReferences: [NoteCleanupBackReference!]!\n orphanedTagNames: [String!]!\n requiresForce: Boolean!\n forceReasons: [String!]!\n }\n\n type NoteSnapshotMeta {\n entrypoint: String\n label: String\n }\n\n type NoteSnapshot {\n id: ID!\n title: String!\n createdAt: String!\n meta: NoteSnapshotMeta!\n }\n\n type DeletedNote {\n id: ID!\n title: String!\n createdAt: String!\n updatedAt: String!\n deletedAt: String!\n contentPreview: String!\n contentAsMarkdown: String\n pinned: Boolean!\n order: Int!\n layout: NoteLayout!\n tagNames: [String!]!\n }\n\n type DeletedNotes {\n totalCount: Int!\n notes: [DeletedNote!]!\n }\n`;\n\nexport const noteQuery = gql`\n type GraphNode {\n id: ID!\n title: String!\n connections: Int!\n }\n\n type GraphLink {\n source: ID!\n target: ID!\n }\n\n type NoteGraph {\n nodes: [GraphNode!]!\n links: [GraphLink!]!\n }\n\n type Query {\n allNotes(searchFilter: SearchFilterInput, pagination: PaginationInput): Notes!\n tagNotes(searchFilter: SearchFilterInput, pagination: PaginationInput): Notes!\n notesByTagNames(tagNames: [String!]!, mode: TagMatchMode!, pagination: PaginationInput): Notes!\n notesInDateRange(dateRange: DateRangeInput): [Note!]!\n pinnedNotes: [Note!]!\n imageNotes(src: String!): [Note!]!\n backReferences(id: ID!): [Note]!\n note(id: ID!): Note!\n noteCleanupCandidates(query: String, pagination: PaginationInput): [NoteCleanupCandidate!]!\n noteCleanupPreview(id: ID!): NoteCleanupPreview\n noteSnapshots(id: ID!, limit: Int): [NoteSnapshot!]!\n trashedNote(id: ID!): DeletedNote\n trashedNotes(pagination: PaginationInput): DeletedNotes!\n noteGraph: NoteGraph!\n }\n`;\n\nexport const noteMutation = gql`\n type Mutation {\n createNote(note: NoteInput!): Note!\n updateNote(id: ID!, note: NoteInput!, editSessionId: String): Note!\n deleteNote(id: ID!): Boolean!\n restoreNoteSnapshot(id: ID!): Note!\n restoreTrashedNote(id: ID!): Note!\n purgeTrashedNote(id: ID!): Boolean!\n pinNote(id: ID!, pinned: Boolean!): Note!\n reorderNotes(notes: [NoteOrderInput!]!): [Note!]!\n }\n`;\n\nexport const noteTypeDefs = `\n ${noteType}\n ${noteQuery}\n ${noteMutation}\n`;\n"],"mappings":"AAAA,SAAS,WAAW;AAEb,MAAM,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiIjB,MAAM,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmClB,MAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAarB,MAAM,eAAe;AAAA,MACtB,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,YAAY;AAAA;","names":[]}
1
+ {"version":3,"sources":["../../../../src/features/note/graphql/note.type-defs.ts"],"sourcesContent":["import { gql } from '~/modules/graphql.js';\n\nexport const noteType = gql`\n input PaginationInput {\n limit: Int!\n offset: Int!\n }\n\n input SearchFilterInput {\n query: String!\n sortBy: String\n sortOrder: String\n pinnedFirst: Boolean\n }\n\n input DateRangeInput {\n start: String!\n end: String!\n }\n\n enum NoteLayout {\n narrow\n wide\n full\n }\n\n enum TagMatchMode {\n and\n or\n }\n\n input NoteInput {\n title: String\n content: String\n layout: NoteLayout\n }\n\n input NoteOrderInput {\n id: ID!\n order: Int!\n }\n\n type Tag {\n id: ID!\n name: String!\n createdAt: String!\n updatedAt: String!\n }\n\n type Note {\n id: ID!\n title: String!\n content: String!\n contentAsMarkdown: String!\n createdAt: String!\n updatedAt: String!\n pinned: Boolean!\n order: Int!\n layout: NoteLayout!\n tags: [Tag!]!\n }\n\n type Notes {\n totalCount: Int!\n notes: [Note!]!\n }\n\n type NoteCleanupBackReference {\n id: ID!\n title: String!\n }\n\n type NoteCleanupCandidate {\n id: ID!\n title: String!\n updatedAt: String!\n pinned: Boolean!\n tagNames: [String!]!\n reminderCount: Int!\n backReferenceCount: Int!\n matchedTerms: [String!]!\n reasons: [String!]!\n requiresForce: Boolean!\n forceReasons: [String!]!\n }\n\n type NoteCleanupPreview {\n id: ID!\n title: String!\n updatedAt: String!\n pinned: Boolean!\n tagNames: [String!]!\n reminderCount: Int!\n backReferences: [NoteCleanupBackReference!]!\n orphanedTagNames: [String!]!\n requiresForce: Boolean!\n forceReasons: [String!]!\n }\n\n type NoteSnapshotMeta {\n entrypoint: String\n label: String\n }\n\n type NoteSnapshot {\n id: ID!\n title: String!\n contentPreview: String!\n contentAsMarkdown: String!\n createdAt: String!\n meta: NoteSnapshotMeta!\n }\n\n type DeletedNote {\n id: ID!\n title: String!\n createdAt: String!\n updatedAt: String!\n deletedAt: String!\n contentPreview: String!\n contentAsMarkdown: String\n pinned: Boolean!\n order: Int!\n layout: NoteLayout!\n tagNames: [String!]!\n }\n\n type DeletedNotes {\n totalCount: Int!\n notes: [DeletedNote!]!\n }\n`;\n\nexport const noteQuery = gql`\n type GraphNode {\n id: ID!\n title: String!\n connections: Int!\n }\n\n type GraphLink {\n source: ID!\n target: ID!\n }\n\n type NoteGraph {\n nodes: [GraphNode!]!\n links: [GraphLink!]!\n }\n\n type Query {\n allNotes(searchFilter: SearchFilterInput, pagination: PaginationInput): Notes!\n tagNotes(searchFilter: SearchFilterInput, pagination: PaginationInput): Notes!\n notesByTagNames(tagNames: [String!]!, mode: TagMatchMode!, pagination: PaginationInput): Notes!\n notesInDateRange(dateRange: DateRangeInput): [Note!]!\n pinnedNotes: [Note!]!\n imageNotes(src: String!): [Note!]!\n backReferences(id: ID!): [Note]!\n note(id: ID!): Note!\n noteCleanupCandidates(query: String, pagination: PaginationInput): [NoteCleanupCandidate!]!\n noteCleanupPreview(id: ID!): NoteCleanupPreview\n noteSnapshots(id: ID!, limit: Int): [NoteSnapshot!]!\n noteSnapshot(id: ID!): NoteSnapshot\n trashedNote(id: ID!): DeletedNote\n trashedNotes(pagination: PaginationInput): DeletedNotes!\n noteGraph: NoteGraph!\n }\n`;\n\nexport const noteMutation = gql`\n type Mutation {\n createNote(note: NoteInput!): Note!\n updateNote(id: ID!, note: NoteInput!, editSessionId: String, expectedUpdatedAt: String, force: Boolean): Note!\n deleteNote(id: ID!): Boolean!\n restoreNoteSnapshot(id: ID!): Note!\n restoreTrashedNote(id: ID!): Note!\n purgeTrashedNote(id: ID!): Boolean!\n pinNote(id: ID!, pinned: Boolean!): Note!\n reorderNotes(notes: [NoteOrderInput!]!): [Note!]!\n }\n`;\n\nexport const noteTypeDefs = `\n ${noteType}\n ${noteQuery}\n ${noteMutation}\n`;\n"],"mappings":"AAAA,SAAS,WAAW;AAEb,MAAM,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmIjB,MAAM,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoClB,MAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAarB,MAAM,eAAe;AAAA,MACtB,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,YAAY;AAAA;","names":[]}
@@ -1,4 +1,5 @@
1
1
  import models from "../../../models.js";
2
+ import { contentReferencesNote } from "./content-blocks.js";
2
3
  import { trashNoteById as moveNoteToTrashById } from "./trash.js";
3
4
  const DEFAULT_CLEANUP_KEYWORDS = ["temp", "temporary", "tmp", "draft", "test", "wip"];
4
5
  const normalizeCleanupKeywords = (keywords, query) => {
@@ -152,14 +153,14 @@ const noteCleanupService = createNoteCleanupService({
152
153
  findBackReferences: (noteId) => models.note.findMany({
153
154
  select: {
154
155
  id: true,
155
- title: true
156
+ title: true,
157
+ content: true
156
158
  },
157
159
  where: {
158
- content: { contains: `reference","props":{"id":"${noteId}"` },
159
160
  NOT: { id: noteId }
160
161
  },
161
162
  orderBy: [{ pinned: "desc" }, { updatedAt: "desc" }]
162
- }),
163
+ }).then((notes) => notes.filter((note) => contentReferencesNote(note.content, noteId))),
163
164
  findCandidateNotes: (keywords, skip, take, olderThanDays) => models.note.findMany({
164
165
  include: { tags: true },
165
166
  orderBy: { updatedAt: "asc" },
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/features/note/services/cleanup.ts"],"sourcesContent":["import models from '~/models.js';\nimport { trashNoteById as moveNoteToTrashById } from './trash.js';\n\nexport interface NoteCleanupBackReference {\n id: string;\n title: string;\n}\n\nexport interface NoteCleanupPreview {\n id: string;\n title: string;\n updatedAt: string;\n pinned: boolean;\n tagNames: string[];\n reminderCount: number;\n backReferences: NoteCleanupBackReference[];\n orphanedTagNames: string[];\n requiresForce: boolean;\n forceReasons: string[];\n}\n\nexport interface NoteCleanupCandidate {\n id: string;\n title: string;\n updatedAt: string;\n pinned: boolean;\n tagNames: string[];\n reminderCount: number;\n backReferenceCount: number;\n matchedTerms: string[];\n reasons: string[];\n requiresForce: boolean;\n forceReasons: string[];\n}\n\nexport interface NoteCleanupCandidatesResult {\n keywords: string[];\n limit: number;\n offset: number;\n olderThanDays: number;\n notes: NoteCleanupCandidate[];\n totalCount: number;\n}\n\nexport interface NoteCleanupService {\n deleteNoteById: (id: number) => Promise<NoteCleanupPreview | null>;\n getDeletePreview: (id: number) => Promise<NoteCleanupPreview | null>;\n listCleanupCandidates: (input?: {\n keywords?: string[];\n limit?: number;\n offset?: number;\n olderThanDays?: number;\n query?: string;\n }) => Promise<NoteCleanupCandidatesResult>;\n}\n\ninterface NoteRecord {\n id: number;\n title: string;\n content: string;\n updatedAt: Date;\n pinned: boolean;\n tags: Array<{\n id: number;\n name: string;\n }>;\n}\n\ninterface NoteCleanupPreviewInternal extends NoteCleanupPreview {\n orphanedTagIds: number[];\n}\n\nconst DEFAULT_CLEANUP_KEYWORDS = ['temp', 'temporary', 'tmp', 'draft', 'test', 'wip'];\n\nexport const normalizeCleanupKeywords = (keywords?: string[], query?: string) => {\n const queryKeywords = typeof query === 'string' ? query.split(/[,\\s]+/).map((keyword) => keyword.trim()) : [];\n const source =\n keywords && keywords.length > 0\n ? keywords\n : queryKeywords.length > 0\n ? queryKeywords\n : DEFAULT_CLEANUP_KEYWORDS;\n\n return Array.from(new Set(source.map((keyword) => keyword.trim().toLowerCase()).filter(Boolean)));\n};\n\nconst buildForceReasons = (input: { pinned: boolean; reminderCount: number; backReferenceCount: number }) => {\n const forceReasons: string[] = [];\n\n if (input.pinned) {\n forceReasons.push('note_is_pinned');\n }\n\n if (input.reminderCount > 0) {\n forceReasons.push('has_reminders');\n }\n\n if (input.backReferenceCount > 0) {\n forceReasons.push('has_back_references');\n }\n\n return forceReasons;\n};\n\nexport const createNoteCleanupService = (deps: {\n countCandidateNotes: (keywords: string[], olderThanDays: number) => Promise<number>;\n countReminders: (noteId: number) => Promise<number>;\n deleteNoteAndPruneTags: (noteId: number, orphanTagIds: number[]) => Promise<void>;\n findBackReferences: (noteId: number) => Promise<Array<{ id: number; title: string }>>;\n findCandidateNotes: (\n keywords: string[],\n skip: number,\n take: number,\n olderThanDays: number,\n ) => Promise<NoteRecord[]>;\n findNote: (id: number) => Promise<NoteRecord | null>;\n getTagNoteCounts: (tagIds: number[]) => Promise<Map<number, number>>;\n}): NoteCleanupService => {\n const buildCandidate = async (note: NoteRecord, matchedTerms?: string[]): Promise<NoteCleanupCandidate> => {\n const [reminderCount, backReferences] = await Promise.all([\n deps.countReminders(note.id),\n deps.findBackReferences(note.id),\n ]);\n const normalizedMatchedTerms = matchedTerms ?? [];\n const forceReasons = buildForceReasons({\n pinned: note.pinned,\n reminderCount,\n backReferenceCount: backReferences.length,\n });\n\n return {\n id: String(note.id),\n title: note.title,\n updatedAt: note.updatedAt.toISOString(),\n pinned: note.pinned,\n tagNames: note.tags.map((tag) => tag.name),\n reminderCount,\n backReferenceCount: backReferences.length,\n matchedTerms: normalizedMatchedTerms,\n reasons: [\n ...(normalizedMatchedTerms.length > 0 ? [`matched_terms:${normalizedMatchedTerms.join(',')}`] : []),\n ...(note.pinned ? [] : ['not_pinned']),\n ...(note.tags.length === 0 ? ['tagless'] : []),\n ...(reminderCount === 0 ? ['no_reminders'] : []),\n ...(backReferences.length === 0 ? ['no_back_references'] : []),\n ],\n requiresForce: forceReasons.length > 0,\n forceReasons,\n };\n };\n\n const getDeletePreviewInternal = async (id: number): Promise<NoteCleanupPreviewInternal | null> => {\n const note = await deps.findNote(id);\n\n if (!note) {\n return null;\n }\n\n const [candidate, backReferences, tagNoteCounts] = await Promise.all([\n buildCandidate(note),\n deps.findBackReferences(note.id),\n deps.getTagNoteCounts(note.tags.map((tag) => tag.id)),\n ]);\n\n const orphanedTags = note.tags.filter((tag) => (tagNoteCounts.get(tag.id) ?? 0) <= 1);\n\n return {\n id: candidate.id,\n title: candidate.title,\n updatedAt: candidate.updatedAt,\n pinned: candidate.pinned,\n tagNames: candidate.tagNames,\n reminderCount: candidate.reminderCount,\n backReferences: backReferences.map((backReference) => ({\n id: String(backReference.id),\n title: backReference.title,\n })),\n orphanedTagNames: orphanedTags.map((tag) => tag.name),\n orphanedTagIds: orphanedTags.map((tag) => tag.id),\n requiresForce: candidate.requiresForce || orphanedTags.length > 0,\n forceReasons: [...candidate.forceReasons, ...(orphanedTags.length > 0 ? ['orphan_tags'] : [])],\n };\n };\n\n const getDeletePreview = async (id: number): Promise<NoteCleanupPreview | null> => {\n const preview = await getDeletePreviewInternal(id);\n\n if (!preview) {\n return null;\n }\n\n const { orphanedTagIds, ...publicPreview } = preview;\n void orphanedTagIds;\n return publicPreview;\n };\n\n return {\n listCleanupCandidates: async (input = {}) => {\n const limit = Math.max(1, Number(input.limit ?? 20));\n const offset = Math.max(0, Number(input.offset ?? 0));\n const olderThanDays = Math.max(0, Number(input.olderThanDays ?? 0));\n const keywords = normalizeCleanupKeywords(input.keywords, input.query);\n\n const [totalCount, notes] = await Promise.all([\n deps.countCandidateNotes(keywords, olderThanDays),\n deps.findCandidateNotes(keywords, offset, limit, olderThanDays),\n ]);\n\n const candidates = await Promise.all(\n notes.map((note) => {\n const haystack = `${note.title}\\n${note.content}`.toLowerCase();\n const matchedTerms = keywords.filter((keyword) => haystack.includes(keyword));\n return buildCandidate(note, matchedTerms);\n }),\n );\n\n return {\n keywords,\n limit,\n offset,\n olderThanDays,\n notes: candidates,\n totalCount,\n };\n },\n getDeletePreview,\n deleteNoteById: async (id: number) => {\n const preview = await getDeletePreviewInternal(id);\n\n if (!preview) {\n return null;\n }\n\n await deps.deleteNoteAndPruneTags(id, preview.orphanedTagIds);\n\n const { orphanedTagIds, ...publicPreview } = preview;\n void orphanedTagIds;\n return publicPreview;\n },\n };\n};\n\nconst buildKeywordClauses = (keywords: string[]) => {\n return keywords.flatMap((keyword) => [{ title: { contains: keyword } }, { content: { contains: keyword } }]);\n};\n\nconst buildStaleWhere = (olderThanDays: number) => {\n if (olderThanDays <= 0) {\n return {};\n }\n\n return { updatedAt: { lte: new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000) } };\n};\n\nconst noteCleanupService = createNoteCleanupService({\n countCandidateNotes: (keywords: string[], olderThanDays: number) =>\n models.note.count({\n where: {\n ...buildStaleWhere(olderThanDays),\n OR: buildKeywordClauses(keywords),\n },\n }),\n countReminders: (noteId: number) => models.reminder.count({ where: { noteId } }),\n deleteNoteAndPruneTags: async (noteId: number, orphanTagIds: number[]) => {\n void orphanTagIds;\n await moveNoteToTrashById(noteId);\n },\n findBackReferences: (noteId: number) =>\n models.note.findMany({\n select: {\n id: true,\n title: true,\n },\n where: {\n content: { contains: `reference\",\"props\":{\"id\":\"${noteId}\"` },\n NOT: { id: noteId },\n },\n orderBy: [{ pinned: 'desc' }, { updatedAt: 'desc' }],\n }),\n findCandidateNotes: (keywords: string[], skip: number, take: number, olderThanDays: number) =>\n models.note.findMany({\n include: { tags: true },\n orderBy: { updatedAt: 'asc' },\n skip,\n take,\n where: {\n ...buildStaleWhere(olderThanDays),\n OR: buildKeywordClauses(keywords),\n },\n }),\n findNote: (id: number) =>\n models.note.findUnique({\n where: { id },\n include: { tags: true },\n }),\n getTagNoteCounts: async (tagIds: number[]) => {\n if (tagIds.length === 0) {\n return new Map<number, number>();\n }\n\n const tags = await models.tag.findMany({\n where: { id: { in: tagIds } },\n include: { _count: { select: { notes: true } } },\n });\n\n return new Map(tags.map((tag) => [tag.id, tag._count.notes]));\n },\n});\n\nexport const listNoteCleanupCandidates = noteCleanupService.listCleanupCandidates;\nexport const getNoteCleanupPreview = noteCleanupService.getDeletePreview;\nexport const deleteNoteById = noteCleanupService.deleteNoteById;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/features/note/services/cleanup.ts"],"sourcesContent":["import models from '~/models.js';\nimport { contentReferencesNote } from './content-blocks.js';\nimport { trashNoteById as moveNoteToTrashById } from './trash.js';\n\nexport interface NoteCleanupBackReference {\n id: string;\n title: string;\n}\n\nexport interface NoteCleanupPreview {\n id: string;\n title: string;\n updatedAt: string;\n pinned: boolean;\n tagNames: string[];\n reminderCount: number;\n backReferences: NoteCleanupBackReference[];\n orphanedTagNames: string[];\n requiresForce: boolean;\n forceReasons: string[];\n}\n\nexport interface NoteCleanupCandidate {\n id: string;\n title: string;\n updatedAt: string;\n pinned: boolean;\n tagNames: string[];\n reminderCount: number;\n backReferenceCount: number;\n matchedTerms: string[];\n reasons: string[];\n requiresForce: boolean;\n forceReasons: string[];\n}\n\nexport interface NoteCleanupCandidatesResult {\n keywords: string[];\n limit: number;\n offset: number;\n olderThanDays: number;\n notes: NoteCleanupCandidate[];\n totalCount: number;\n}\n\nexport interface NoteCleanupService {\n deleteNoteById: (id: number) => Promise<NoteCleanupPreview | null>;\n getDeletePreview: (id: number) => Promise<NoteCleanupPreview | null>;\n listCleanupCandidates: (input?: {\n keywords?: string[];\n limit?: number;\n offset?: number;\n olderThanDays?: number;\n query?: string;\n }) => Promise<NoteCleanupCandidatesResult>;\n}\n\ninterface NoteRecord {\n id: number;\n title: string;\n content: string;\n updatedAt: Date;\n pinned: boolean;\n tags: Array<{\n id: number;\n name: string;\n }>;\n}\n\ninterface NoteCleanupPreviewInternal extends NoteCleanupPreview {\n orphanedTagIds: number[];\n}\n\nconst DEFAULT_CLEANUP_KEYWORDS = ['temp', 'temporary', 'tmp', 'draft', 'test', 'wip'];\n\nexport const normalizeCleanupKeywords = (keywords?: string[], query?: string) => {\n const queryKeywords = typeof query === 'string' ? query.split(/[,\\s]+/).map((keyword) => keyword.trim()) : [];\n const source =\n keywords && keywords.length > 0\n ? keywords\n : queryKeywords.length > 0\n ? queryKeywords\n : DEFAULT_CLEANUP_KEYWORDS;\n\n return Array.from(new Set(source.map((keyword) => keyword.trim().toLowerCase()).filter(Boolean)));\n};\n\nconst buildForceReasons = (input: { pinned: boolean; reminderCount: number; backReferenceCount: number }) => {\n const forceReasons: string[] = [];\n\n if (input.pinned) {\n forceReasons.push('note_is_pinned');\n }\n\n if (input.reminderCount > 0) {\n forceReasons.push('has_reminders');\n }\n\n if (input.backReferenceCount > 0) {\n forceReasons.push('has_back_references');\n }\n\n return forceReasons;\n};\n\nexport const createNoteCleanupService = (deps: {\n countCandidateNotes: (keywords: string[], olderThanDays: number) => Promise<number>;\n countReminders: (noteId: number) => Promise<number>;\n deleteNoteAndPruneTags: (noteId: number, orphanTagIds: number[]) => Promise<void>;\n findBackReferences: (noteId: number) => Promise<Array<{ id: number; title: string }>>;\n findCandidateNotes: (\n keywords: string[],\n skip: number,\n take: number,\n olderThanDays: number,\n ) => Promise<NoteRecord[]>;\n findNote: (id: number) => Promise<NoteRecord | null>;\n getTagNoteCounts: (tagIds: number[]) => Promise<Map<number, number>>;\n}): NoteCleanupService => {\n const buildCandidate = async (note: NoteRecord, matchedTerms?: string[]): Promise<NoteCleanupCandidate> => {\n const [reminderCount, backReferences] = await Promise.all([\n deps.countReminders(note.id),\n deps.findBackReferences(note.id),\n ]);\n const normalizedMatchedTerms = matchedTerms ?? [];\n const forceReasons = buildForceReasons({\n pinned: note.pinned,\n reminderCount,\n backReferenceCount: backReferences.length,\n });\n\n return {\n id: String(note.id),\n title: note.title,\n updatedAt: note.updatedAt.toISOString(),\n pinned: note.pinned,\n tagNames: note.tags.map((tag) => tag.name),\n reminderCount,\n backReferenceCount: backReferences.length,\n matchedTerms: normalizedMatchedTerms,\n reasons: [\n ...(normalizedMatchedTerms.length > 0 ? [`matched_terms:${normalizedMatchedTerms.join(',')}`] : []),\n ...(note.pinned ? [] : ['not_pinned']),\n ...(note.tags.length === 0 ? ['tagless'] : []),\n ...(reminderCount === 0 ? ['no_reminders'] : []),\n ...(backReferences.length === 0 ? ['no_back_references'] : []),\n ],\n requiresForce: forceReasons.length > 0,\n forceReasons,\n };\n };\n\n const getDeletePreviewInternal = async (id: number): Promise<NoteCleanupPreviewInternal | null> => {\n const note = await deps.findNote(id);\n\n if (!note) {\n return null;\n }\n\n const [candidate, backReferences, tagNoteCounts] = await Promise.all([\n buildCandidate(note),\n deps.findBackReferences(note.id),\n deps.getTagNoteCounts(note.tags.map((tag) => tag.id)),\n ]);\n\n const orphanedTags = note.tags.filter((tag) => (tagNoteCounts.get(tag.id) ?? 0) <= 1);\n\n return {\n id: candidate.id,\n title: candidate.title,\n updatedAt: candidate.updatedAt,\n pinned: candidate.pinned,\n tagNames: candidate.tagNames,\n reminderCount: candidate.reminderCount,\n backReferences: backReferences.map((backReference) => ({\n id: String(backReference.id),\n title: backReference.title,\n })),\n orphanedTagNames: orphanedTags.map((tag) => tag.name),\n orphanedTagIds: orphanedTags.map((tag) => tag.id),\n requiresForce: candidate.requiresForce || orphanedTags.length > 0,\n forceReasons: [...candidate.forceReasons, ...(orphanedTags.length > 0 ? ['orphan_tags'] : [])],\n };\n };\n\n const getDeletePreview = async (id: number): Promise<NoteCleanupPreview | null> => {\n const preview = await getDeletePreviewInternal(id);\n\n if (!preview) {\n return null;\n }\n\n const { orphanedTagIds, ...publicPreview } = preview;\n void orphanedTagIds;\n return publicPreview;\n };\n\n return {\n listCleanupCandidates: async (input = {}) => {\n const limit = Math.max(1, Number(input.limit ?? 20));\n const offset = Math.max(0, Number(input.offset ?? 0));\n const olderThanDays = Math.max(0, Number(input.olderThanDays ?? 0));\n const keywords = normalizeCleanupKeywords(input.keywords, input.query);\n\n const [totalCount, notes] = await Promise.all([\n deps.countCandidateNotes(keywords, olderThanDays),\n deps.findCandidateNotes(keywords, offset, limit, olderThanDays),\n ]);\n\n const candidates = await Promise.all(\n notes.map((note) => {\n const haystack = `${note.title}\\n${note.content}`.toLowerCase();\n const matchedTerms = keywords.filter((keyword) => haystack.includes(keyword));\n return buildCandidate(note, matchedTerms);\n }),\n );\n\n return {\n keywords,\n limit,\n offset,\n olderThanDays,\n notes: candidates,\n totalCount,\n };\n },\n getDeletePreview,\n deleteNoteById: async (id: number) => {\n const preview = await getDeletePreviewInternal(id);\n\n if (!preview) {\n return null;\n }\n\n await deps.deleteNoteAndPruneTags(id, preview.orphanedTagIds);\n\n const { orphanedTagIds, ...publicPreview } = preview;\n void orphanedTagIds;\n return publicPreview;\n },\n };\n};\n\nconst buildKeywordClauses = (keywords: string[]) => {\n return keywords.flatMap((keyword) => [{ title: { contains: keyword } }, { content: { contains: keyword } }]);\n};\n\nconst buildStaleWhere = (olderThanDays: number) => {\n if (olderThanDays <= 0) {\n return {};\n }\n\n return { updatedAt: { lte: new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000) } };\n};\n\nconst noteCleanupService = createNoteCleanupService({\n countCandidateNotes: (keywords: string[], olderThanDays: number) =>\n models.note.count({\n where: {\n ...buildStaleWhere(olderThanDays),\n OR: buildKeywordClauses(keywords),\n },\n }),\n countReminders: (noteId: number) => models.reminder.count({ where: { noteId } }),\n deleteNoteAndPruneTags: async (noteId: number, orphanTagIds: number[]) => {\n void orphanTagIds;\n await moveNoteToTrashById(noteId);\n },\n findBackReferences: (noteId: number) =>\n models.note\n .findMany({\n select: {\n id: true,\n title: true,\n content: true,\n },\n where: {\n NOT: { id: noteId },\n },\n orderBy: [{ pinned: 'desc' }, { updatedAt: 'desc' }],\n })\n .then((notes) => notes.filter((note) => contentReferencesNote(note.content, noteId))),\n findCandidateNotes: (keywords: string[], skip: number, take: number, olderThanDays: number) =>\n models.note.findMany({\n include: { tags: true },\n orderBy: { updatedAt: 'asc' },\n skip,\n take,\n where: {\n ...buildStaleWhere(olderThanDays),\n OR: buildKeywordClauses(keywords),\n },\n }),\n findNote: (id: number) =>\n models.note.findUnique({\n where: { id },\n include: { tags: true },\n }),\n getTagNoteCounts: async (tagIds: number[]) => {\n if (tagIds.length === 0) {\n return new Map<number, number>();\n }\n\n const tags = await models.tag.findMany({\n where: { id: { in: tagIds } },\n include: { _count: { select: { notes: true } } },\n });\n\n return new Map(tags.map((tag) => [tag.id, tag._count.notes]));\n },\n});\n\nexport const listNoteCleanupCandidates = noteCleanupService.listCleanupCandidates;\nexport const getNoteCleanupPreview = noteCleanupService.getDeletePreview;\nexport const deleteNoteById = noteCleanupService.deleteNoteById;\n"],"mappings":"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","names":[]}
@@ -0,0 +1,113 @@
1
+ import { parseBlockNoteContent, walkBlockNoteTree } from "../../../modules/blocknote-tree.js";
2
+ const parseNoteContent = (content) => {
3
+ return parseBlockNoteContent(content);
4
+ };
5
+ const normalizeReferenceId = (id) => {
6
+ if (typeof id === "number") {
7
+ return Number.isFinite(id) ? String(id) : null;
8
+ }
9
+ if (typeof id !== "string") {
10
+ return null;
11
+ }
12
+ const normalizedId = id.trim();
13
+ return normalizedId.length > 0 ? normalizedId : null;
14
+ };
15
+ const extractBlocksByType = (type, dataArray) => {
16
+ const result = [];
17
+ walkBlockNoteTree(dataArray, (node) => {
18
+ if (node.type === type) {
19
+ result.push(node);
20
+ }
21
+ });
22
+ return result;
23
+ };
24
+ const extractReferenceBlocks = (dataArray) => {
25
+ return extractBlocksByType("reference", dataArray).filter((block) => {
26
+ return normalizeReferenceId(block.props?.id) !== null;
27
+ });
28
+ };
29
+ const extractReferenceBlocksFromContent = (content) => {
30
+ const parsed = parseNoteContent(content);
31
+ return parsed ? extractReferenceBlocks(parsed) : [];
32
+ };
33
+ const contentReferencesNote = (content, noteId) => {
34
+ const targetId = normalizeReferenceId(noteId);
35
+ if (!targetId) {
36
+ return false;
37
+ }
38
+ return extractReferenceBlocksFromContent(content).some(
39
+ (block) => normalizeReferenceId(block.props?.id) === targetId
40
+ );
41
+ };
42
+ const syncReferenceTitlesInContent = (content, titlesById) => {
43
+ const parsed = parseNoteContent(content);
44
+ if (!parsed) {
45
+ return null;
46
+ }
47
+ let changed = false;
48
+ for (const block of extractReferenceBlocks(parsed)) {
49
+ const props = block.props;
50
+ const id = normalizeReferenceId(props?.id);
51
+ if (!props || id === null) {
52
+ continue;
53
+ }
54
+ const title = titlesById.get(id);
55
+ if (title !== void 0 && props.title !== title) {
56
+ block.props = {
57
+ ...props,
58
+ title
59
+ };
60
+ changed = true;
61
+ }
62
+ }
63
+ return changed ? JSON.stringify(parsed) : content;
64
+ };
65
+ const buildNoteGraph = (notes) => {
66
+ const existingNoteIds = new Set(notes.map((note) => String(note.id)));
67
+ const links = [];
68
+ const connectionCount = {};
69
+ const linkSet = /* @__PURE__ */ new Set();
70
+ for (const note of notes) {
71
+ const sourceId = String(note.id);
72
+ for (const block of extractReferenceBlocksFromContent(note.content)) {
73
+ const targetId = normalizeReferenceId(block.props?.id);
74
+ if (!targetId || sourceId === targetId || !existingNoteIds.has(targetId)) {
75
+ continue;
76
+ }
77
+ const linkKey = `${sourceId}-${targetId}`;
78
+ const reverseLinkKey = `${targetId}-${sourceId}`;
79
+ if (linkSet.has(linkKey) || linkSet.has(reverseLinkKey)) {
80
+ continue;
81
+ }
82
+ linkSet.add(linkKey);
83
+ links.push({
84
+ source: sourceId,
85
+ target: targetId
86
+ });
87
+ connectionCount[sourceId] = (connectionCount[sourceId] || 0) + 1;
88
+ connectionCount[targetId] = (connectionCount[targetId] || 0) + 1;
89
+ }
90
+ }
91
+ return {
92
+ nodes: notes.map((note) => {
93
+ const id = String(note.id);
94
+ return {
95
+ id,
96
+ title: note.title || "Untitled",
97
+ connections: connectionCount[id] || 0
98
+ };
99
+ }),
100
+ links
101
+ };
102
+ };
103
+ export {
104
+ buildNoteGraph,
105
+ contentReferencesNote,
106
+ extractBlocksByType,
107
+ extractReferenceBlocks,
108
+ extractReferenceBlocksFromContent,
109
+ normalizeReferenceId,
110
+ parseNoteContent,
111
+ syncReferenceTitlesInContent
112
+ };
113
+ //# sourceMappingURL=content-blocks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/features/note/services/content-blocks.ts"],"sourcesContent":["import { parseBlockNoteContent, walkBlockNoteTree } from '~/modules/blocknote-tree.js';\n\ninterface BlockNote<T = unknown> {\n id?: string;\n type?: string;\n props?: T;\n content?: unknown;\n children?: unknown;\n}\n\ninterface ReferenceProps {\n id?: string | number;\n title?: string;\n}\n\ninterface NoteGraphInput {\n id: string | number;\n title: string;\n content: string;\n}\n\nexport interface NoteGraphResult {\n nodes: Array<{ id: string; title: string; connections: number }>;\n links: Array<{ source: string; target: string }>;\n}\n\nexport const parseNoteContent = (content: string): unknown[] | null => {\n return parseBlockNoteContent(content);\n};\n\nexport const normalizeReferenceId = (id: unknown) => {\n if (typeof id === 'number') {\n return Number.isFinite(id) ? String(id) : null;\n }\n\n if (typeof id !== 'string') {\n return null;\n }\n\n const normalizedId = id.trim();\n\n return normalizedId.length > 0 ? normalizedId : null;\n};\n\nexport const extractBlocksByType = <T>(type: string, dataArray: unknown): BlockNote<T>[] => {\n const result: BlockNote<T>[] = [];\n\n walkBlockNoteTree(dataArray, (node) => {\n if (node.type === type) {\n result.push(node as BlockNote<T>);\n }\n });\n\n return result;\n};\n\nexport const extractReferenceBlocks = (dataArray: unknown) => {\n return extractBlocksByType<ReferenceProps>('reference', dataArray).filter((block) => {\n return normalizeReferenceId(block.props?.id) !== null;\n });\n};\n\nexport const extractReferenceBlocksFromContent = (content: string) => {\n const parsed = parseNoteContent(content);\n return parsed ? extractReferenceBlocks(parsed) : [];\n};\n\nexport const contentReferencesNote = (content: string, noteId: string | number) => {\n const targetId = normalizeReferenceId(noteId);\n\n if (!targetId) {\n return false;\n }\n\n return extractReferenceBlocksFromContent(content).some(\n (block) => normalizeReferenceId(block.props?.id) === targetId,\n );\n};\n\nexport const syncReferenceTitlesInContent = (content: string, titlesById: Map<string, string>) => {\n const parsed = parseNoteContent(content);\n\n if (!parsed) {\n return null;\n }\n\n let changed = false;\n\n for (const block of extractReferenceBlocks(parsed)) {\n const props = block.props;\n const id = normalizeReferenceId(props?.id);\n\n if (!props || id === null) {\n continue;\n }\n\n const title = titlesById.get(id);\n\n if (title !== undefined && props.title !== title) {\n block.props = {\n ...props,\n title,\n };\n changed = true;\n }\n }\n\n return changed ? JSON.stringify(parsed) : content;\n};\n\nexport const buildNoteGraph = (notes: NoteGraphInput[]): NoteGraphResult => {\n const existingNoteIds = new Set(notes.map((note) => String(note.id)));\n const links: NoteGraphResult['links'] = [];\n const connectionCount: Record<string, number> = {};\n const linkSet = new Set<string>();\n\n for (const note of notes) {\n const sourceId = String(note.id);\n\n for (const block of extractReferenceBlocksFromContent(note.content)) {\n const targetId = normalizeReferenceId(block.props?.id);\n\n if (!targetId || sourceId === targetId || !existingNoteIds.has(targetId)) {\n continue;\n }\n\n const linkKey = `${sourceId}-${targetId}`;\n const reverseLinkKey = `${targetId}-${sourceId}`;\n\n if (linkSet.has(linkKey) || linkSet.has(reverseLinkKey)) {\n continue;\n }\n\n linkSet.add(linkKey);\n links.push({\n source: sourceId,\n target: targetId,\n });\n connectionCount[sourceId] = (connectionCount[sourceId] || 0) + 1;\n connectionCount[targetId] = (connectionCount[targetId] || 0) + 1;\n }\n }\n\n return {\n nodes: notes.map((note) => {\n const id = String(note.id);\n\n return {\n id,\n title: note.title || 'Untitled',\n connections: connectionCount[id] || 0,\n };\n }),\n links,\n };\n};\n"],"mappings":"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","names":[]}
@@ -1,3 +1,4 @@
1
+ import { isBlockNoteTreeNode, walkBlockNoteTree } from "../../../modules/blocknote-tree.js";
1
2
  const NOTE_SEARCH_TEXT_SCHEMA_VERSION = 1;
2
3
  const FILE_BLOCK_VISIBLE_PROP_KEYS = ["name", "caption"];
3
4
  const NOTE_SEARCH_EXTRACTOR_NODE_TYPES = [
@@ -73,9 +74,6 @@ const isSearchExtractorNodeType = (type) => {
73
74
  };
74
75
  const normalizeSearchText = (value) => value.replace(/\s+/g, " ").trim();
75
76
  const normalizeSearchToken = (value) => normalizeSearchText(value).toLowerCase();
76
- const isRecord = (value) => {
77
- return typeof value === "object" && value !== null;
78
- };
79
77
  const pushVisibleSegment = (context, value) => {
80
78
  if (typeof value !== "string") {
81
79
  return;
@@ -87,7 +85,7 @@ const pushVisibleSegment = (context, value) => {
87
85
  context.segments.push(normalizedValue);
88
86
  };
89
87
  const pushVisibleProps = (context, props, keys) => {
90
- if (!isRecord(props)) {
88
+ if (!isBlockNoteTreeNode(props)) {
91
89
  return;
92
90
  }
93
91
  for (const key of keys) {
@@ -95,32 +93,15 @@ const pushVisibleProps = (context, props, keys) => {
95
93
  }
96
94
  };
97
95
  const collectVisibleSearchSegments = (node, context) => {
98
- if (Array.isArray(node)) {
99
- node.forEach((item) => collectVisibleSearchSegments(item, context));
100
- return;
101
- }
102
- if (!isRecord(node)) {
103
- return;
104
- }
105
- const type = typeof node.type === "string" ? node.type : void 0;
106
- if (type) {
107
- if (isSearchExtractorNodeType(type)) {
108
- SEARCH_NODE_EXTRACTORS[type](node, context);
96
+ walkBlockNoteTree(node, (item) => {
97
+ const type = typeof item.type === "string" ? item.type : void 0;
98
+ if (type) {
99
+ if (isSearchExtractorNodeType(type)) {
100
+ SEARCH_NODE_EXTRACTORS[type](item, context);
101
+ }
102
+ recordUnknownNoteSearchNodeType(type);
109
103
  }
110
- recordUnknownNoteSearchNodeType(type);
111
- }
112
- if ("content" in node) {
113
- collectVisibleSearchSegments(node.content, context);
114
- }
115
- if ("children" in node) {
116
- collectVisibleSearchSegments(node.children, context);
117
- }
118
- if ("rows" in node) {
119
- collectVisibleSearchSegments(node.rows, context);
120
- }
121
- if ("cells" in node) {
122
- collectVisibleSearchSegments(node.cells, context);
123
- }
104
+ });
124
105
  };
125
106
  const buildNoteSearchText = (note) => {
126
107
  const normalizedTitle = normalizeSearchText(note.title);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/features/note/services/search.ts"],"sourcesContent":["export interface NoteSearchQuery {\n included: string[];\n excluded: string[];\n hasFilters: boolean;\n}\n\ninterface SearchableNoteLike {\n title: string;\n content: string;\n searchableText?: string;\n searchableTextVersion?: number;\n}\n\nexport interface NoteSearchProjection {\n searchableText: string;\n searchableTextVersion: number;\n}\n\ninterface SearchProjectionContext {\n segments: string[];\n}\n\ntype SearchableJsonNode = Record<string, unknown>;\ntype SearchNodeExtractor = (node: SearchableJsonNode, context: SearchProjectionContext) => void;\ntype SearchExtractorNodeType = (typeof NOTE_SEARCH_EXTRACTOR_NODE_TYPES)[number];\n\n// If we later materialize searchable text into a DB column, bump this when the\n// extraction meaning changes so stored projections can be rebuilt safely.\nexport const NOTE_SEARCH_TEXT_SCHEMA_VERSION = 1;\n\nconst FILE_BLOCK_VISIBLE_PROP_KEYS = ['name', 'caption'] as const;\n\nexport const NOTE_SEARCH_EXTRACTOR_NODE_TYPES = [\n 'text',\n 'tag',\n 'reference',\n 'image',\n 'video',\n 'audio',\n 'file',\n] as const;\n\nexport const NOTE_SEARCH_PASS_THROUGH_NODE_TYPES = [\n 'link',\n 'paragraph',\n 'heading',\n 'bulletListItem',\n 'numberedListItem',\n 'checkListItem',\n 'toggleListItem',\n 'quote',\n 'codeBlock',\n 'divider',\n 'table',\n 'tableContent',\n 'tableCell',\n] as const;\n\nexport const NOTE_SEARCH_IGNORED_NODE_TYPES = ['tableOfContents'] as const;\n\nconst KNOWN_NOTE_SEARCH_NODE_TYPES = new Set<string>([\n ...NOTE_SEARCH_EXTRACTOR_NODE_TYPES,\n ...NOTE_SEARCH_PASS_THROUGH_NODE_TYPES,\n ...NOTE_SEARCH_IGNORED_NODE_TYPES,\n]);\nconst SEARCH_NODE_EXTRACTOR_TYPE_SET = new Set<string>(NOTE_SEARCH_EXTRACTOR_NODE_TYPES);\n\nconst unknownNoteSearchNodeTypeCounts = new Map<string, number>();\n\nconst SEARCH_NODE_EXTRACTORS: Record<SearchExtractorNodeType, SearchNodeExtractor> = {\n text: (node, context) => {\n pushVisibleSegment(context, node.text);\n },\n tag: (node, context) => {\n pushVisibleProps(context, node.props, ['tag']);\n },\n reference: (node, context) => {\n pushVisibleProps(context, node.props, ['title']);\n },\n image: (node, context) => {\n pushVisibleProps(context, node.props, FILE_BLOCK_VISIBLE_PROP_KEYS);\n },\n video: (node, context) => {\n pushVisibleProps(context, node.props, FILE_BLOCK_VISIBLE_PROP_KEYS);\n },\n audio: (node, context) => {\n pushVisibleProps(context, node.props, FILE_BLOCK_VISIBLE_PROP_KEYS);\n },\n file: (node, context) => {\n pushVisibleProps(context, node.props, FILE_BLOCK_VISIBLE_PROP_KEYS);\n },\n};\n\nconst recordUnknownNoteSearchNodeType = (type: string) => {\n if (KNOWN_NOTE_SEARCH_NODE_TYPES.has(type)) {\n return;\n }\n\n const nextCount = (unknownNoteSearchNodeTypeCounts.get(type) ?? 0) + 1;\n unknownNoteSearchNodeTypeCounts.set(type, nextCount);\n\n if (nextCount === 1) {\n process.emitWarning(\n `note-search encountered unsupported BlockNote node type \"${type}\". Only nested text will be indexed until explicit support is added.`,\n { code: 'OCEAN_BRAIN_NOTE_SEARCH_UNKNOWN_NODE' },\n );\n }\n};\n\nconst isSearchExtractorNodeType = (type: string): type is SearchExtractorNodeType => {\n return SEARCH_NODE_EXTRACTOR_TYPE_SET.has(type);\n};\n\nconst normalizeSearchText = (value: string) => value.replace(/\\s+/g, ' ').trim();\n\nconst normalizeSearchToken = (value: string) => normalizeSearchText(value).toLowerCase();\n\nconst isRecord = (value: unknown): value is SearchableJsonNode => {\n return typeof value === 'object' && value !== null;\n};\n\nconst pushVisibleSegment = (context: SearchProjectionContext, value: unknown) => {\n if (typeof value !== 'string') {\n return;\n }\n\n const normalizedValue = normalizeSearchText(value);\n\n if (!normalizedValue) {\n return;\n }\n\n context.segments.push(normalizedValue);\n};\n\nconst pushVisibleProps = (context: SearchProjectionContext, props: unknown, keys: readonly string[]) => {\n if (!isRecord(props)) {\n return;\n }\n\n for (const key of keys) {\n pushVisibleSegment(context, props[key]);\n }\n};\n\nconst collectVisibleSearchSegments = (node: unknown, context: SearchProjectionContext) => {\n if (Array.isArray(node)) {\n node.forEach((item) => collectVisibleSearchSegments(item, context));\n return;\n }\n\n if (!isRecord(node)) {\n return;\n }\n\n const type = typeof node.type === 'string' ? node.type : undefined;\n\n if (type) {\n if (isSearchExtractorNodeType(type)) {\n SEARCH_NODE_EXTRACTORS[type](node, context);\n }\n\n recordUnknownNoteSearchNodeType(type);\n }\n\n if ('content' in node) {\n collectVisibleSearchSegments(node.content, context);\n }\n\n if ('children' in node) {\n collectVisibleSearchSegments(node.children, context);\n }\n\n if ('rows' in node) {\n collectVisibleSearchSegments(node.rows, context);\n }\n\n if ('cells' in node) {\n collectVisibleSearchSegments(node.cells, context);\n }\n};\n\nexport const buildNoteSearchText = (note: Pick<SearchableNoteLike, 'title' | 'content'>) => {\n const normalizedTitle = normalizeSearchText(note.title);\n const normalizedContent = extractVisibleSearchTextFromContent(note.content);\n\n return normalizeSearchToken(`${normalizedTitle} ${normalizedContent}`);\n};\n\nexport const buildNoteSearchProjection = (\n note: Pick<SearchableNoteLike, 'title' | 'content'>,\n): NoteSearchProjection => {\n return {\n searchableText: buildNoteSearchText(note),\n searchableTextVersion: NOTE_SEARCH_TEXT_SCHEMA_VERSION,\n };\n};\n\nexport const parseNoteSearchQuery = (query: string): NoteSearchQuery => {\n const included: string[] = [];\n const excluded: string[] = [];\n\n for (const item of query\n .split(/\\s+/)\n .map((value) => value.trim())\n .filter(Boolean)) {\n if (item.startsWith('-') && item.length > 1) {\n excluded.push(normalizeSearchToken(item.slice(1)));\n continue;\n }\n\n included.push(normalizeSearchToken(item));\n }\n\n return {\n included,\n excluded,\n hasFilters: included.length > 0 || excluded.length > 0,\n };\n};\n\nexport const extractVisibleSearchTextFromContent = (content: string) => {\n try {\n const parsed = JSON.parse(content) as unknown;\n const context: SearchProjectionContext = { segments: [] };\n\n collectVisibleSearchSegments(parsed, context);\n\n return normalizeSearchText(context.segments.join(' '));\n } catch {\n return '';\n }\n};\n\nexport const matchesNoteSearchQuery = (note: SearchableNoteLike, query: string | NoteSearchQuery) => {\n const parsedQuery = typeof query === 'string' ? parseNoteSearchQuery(query) : query;\n\n if (!parsedQuery.hasFilters) {\n return true;\n }\n\n const haystack =\n note.searchableTextVersion === NOTE_SEARCH_TEXT_SCHEMA_VERSION && typeof note.searchableText === 'string'\n ? normalizeSearchToken(note.searchableText)\n : buildNoteSearchText(note);\n\n return (\n parsedQuery.included.every((term) => haystack.includes(term)) &&\n parsedQuery.excluded.every((term) => !haystack.includes(term))\n );\n};\n\nexport const filterNotesBySearchQuery = <T extends SearchableNoteLike>(notes: T[], query: string | NoteSearchQuery) => {\n const parsedQuery = typeof query === 'string' ? parseNoteSearchQuery(query) : query;\n\n if (!parsedQuery.hasFilters) {\n return notes;\n }\n\n return notes.filter((note) => matchesNoteSearchQuery(note, parsedQuery));\n};\n\nexport const getUnknownNoteSearchNodeTypeCounts = () => {\n return new Map(unknownNoteSearchNodeTypeCounts);\n};\n\nexport const resetUnknownNoteSearchNodeTypeCountsForTest = () => {\n unknownNoteSearchNodeTypeCounts.clear();\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/features/note/services/search.ts"],"sourcesContent":["import { isBlockNoteTreeNode, walkBlockNoteTree } from '~/modules/blocknote-tree.js';\n\nexport interface NoteSearchQuery {\n included: string[];\n excluded: string[];\n hasFilters: boolean;\n}\n\ninterface SearchableNoteLike {\n title: string;\n content: string;\n searchableText?: string;\n searchableTextVersion?: number;\n}\n\nexport interface NoteSearchProjection {\n searchableText: string;\n searchableTextVersion: number;\n}\n\ninterface SearchProjectionContext {\n segments: string[];\n}\n\ntype SearchableJsonNode = Record<string, unknown>;\ntype SearchNodeExtractor = (node: SearchableJsonNode, context: SearchProjectionContext) => void;\ntype SearchExtractorNodeType = (typeof NOTE_SEARCH_EXTRACTOR_NODE_TYPES)[number];\n\n// If we later materialize searchable text into a DB column, bump this when the\n// extraction meaning changes so stored projections can be rebuilt safely.\nexport const NOTE_SEARCH_TEXT_SCHEMA_VERSION = 1;\n\nconst FILE_BLOCK_VISIBLE_PROP_KEYS = ['name', 'caption'] as const;\n\nexport const NOTE_SEARCH_EXTRACTOR_NODE_TYPES = [\n 'text',\n 'tag',\n 'reference',\n 'image',\n 'video',\n 'audio',\n 'file',\n] as const;\n\nexport const NOTE_SEARCH_PASS_THROUGH_NODE_TYPES = [\n 'link',\n 'paragraph',\n 'heading',\n 'bulletListItem',\n 'numberedListItem',\n 'checkListItem',\n 'toggleListItem',\n 'quote',\n 'codeBlock',\n 'divider',\n 'table',\n 'tableContent',\n 'tableCell',\n] as const;\n\nexport const NOTE_SEARCH_IGNORED_NODE_TYPES = ['tableOfContents'] as const;\n\nconst KNOWN_NOTE_SEARCH_NODE_TYPES = new Set<string>([\n ...NOTE_SEARCH_EXTRACTOR_NODE_TYPES,\n ...NOTE_SEARCH_PASS_THROUGH_NODE_TYPES,\n ...NOTE_SEARCH_IGNORED_NODE_TYPES,\n]);\nconst SEARCH_NODE_EXTRACTOR_TYPE_SET = new Set<string>(NOTE_SEARCH_EXTRACTOR_NODE_TYPES);\n\nconst unknownNoteSearchNodeTypeCounts = new Map<string, number>();\n\nconst SEARCH_NODE_EXTRACTORS: Record<SearchExtractorNodeType, SearchNodeExtractor> = {\n text: (node, context) => {\n pushVisibleSegment(context, node.text);\n },\n tag: (node, context) => {\n pushVisibleProps(context, node.props, ['tag']);\n },\n reference: (node, context) => {\n pushVisibleProps(context, node.props, ['title']);\n },\n image: (node, context) => {\n pushVisibleProps(context, node.props, FILE_BLOCK_VISIBLE_PROP_KEYS);\n },\n video: (node, context) => {\n pushVisibleProps(context, node.props, FILE_BLOCK_VISIBLE_PROP_KEYS);\n },\n audio: (node, context) => {\n pushVisibleProps(context, node.props, FILE_BLOCK_VISIBLE_PROP_KEYS);\n },\n file: (node, context) => {\n pushVisibleProps(context, node.props, FILE_BLOCK_VISIBLE_PROP_KEYS);\n },\n};\n\nconst recordUnknownNoteSearchNodeType = (type: string) => {\n if (KNOWN_NOTE_SEARCH_NODE_TYPES.has(type)) {\n return;\n }\n\n const nextCount = (unknownNoteSearchNodeTypeCounts.get(type) ?? 0) + 1;\n unknownNoteSearchNodeTypeCounts.set(type, nextCount);\n\n if (nextCount === 1) {\n process.emitWarning(\n `note-search encountered unsupported BlockNote node type \"${type}\". Only nested text will be indexed until explicit support is added.`,\n { code: 'OCEAN_BRAIN_NOTE_SEARCH_UNKNOWN_NODE' },\n );\n }\n};\n\nconst isSearchExtractorNodeType = (type: string): type is SearchExtractorNodeType => {\n return SEARCH_NODE_EXTRACTOR_TYPE_SET.has(type);\n};\n\nconst normalizeSearchText = (value: string) => value.replace(/\\s+/g, ' ').trim();\n\nconst normalizeSearchToken = (value: string) => normalizeSearchText(value).toLowerCase();\n\nconst pushVisibleSegment = (context: SearchProjectionContext, value: unknown) => {\n if (typeof value !== 'string') {\n return;\n }\n\n const normalizedValue = normalizeSearchText(value);\n\n if (!normalizedValue) {\n return;\n }\n\n context.segments.push(normalizedValue);\n};\n\nconst pushVisibleProps = (context: SearchProjectionContext, props: unknown, keys: readonly string[]) => {\n if (!isBlockNoteTreeNode(props)) {\n return;\n }\n\n for (const key of keys) {\n pushVisibleSegment(context, props[key]);\n }\n};\n\nconst collectVisibleSearchSegments = (node: unknown, context: SearchProjectionContext) => {\n walkBlockNoteTree(node, (item) => {\n const type = typeof item.type === 'string' ? item.type : undefined;\n\n if (type) {\n if (isSearchExtractorNodeType(type)) {\n SEARCH_NODE_EXTRACTORS[type](item, context);\n }\n\n recordUnknownNoteSearchNodeType(type);\n }\n });\n};\n\nexport const buildNoteSearchText = (note: Pick<SearchableNoteLike, 'title' | 'content'>) => {\n const normalizedTitle = normalizeSearchText(note.title);\n const normalizedContent = extractVisibleSearchTextFromContent(note.content);\n\n return normalizeSearchToken(`${normalizedTitle} ${normalizedContent}`);\n};\n\nexport const buildNoteSearchProjection = (\n note: Pick<SearchableNoteLike, 'title' | 'content'>,\n): NoteSearchProjection => {\n return {\n searchableText: buildNoteSearchText(note),\n searchableTextVersion: NOTE_SEARCH_TEXT_SCHEMA_VERSION,\n };\n};\n\nexport const parseNoteSearchQuery = (query: string): NoteSearchQuery => {\n const included: string[] = [];\n const excluded: string[] = [];\n\n for (const item of query\n .split(/\\s+/)\n .map((value) => value.trim())\n .filter(Boolean)) {\n if (item.startsWith('-') && item.length > 1) {\n excluded.push(normalizeSearchToken(item.slice(1)));\n continue;\n }\n\n included.push(normalizeSearchToken(item));\n }\n\n return {\n included,\n excluded,\n hasFilters: included.length > 0 || excluded.length > 0,\n };\n};\n\nexport const extractVisibleSearchTextFromContent = (content: string) => {\n try {\n const parsed = JSON.parse(content) as unknown;\n const context: SearchProjectionContext = { segments: [] };\n\n collectVisibleSearchSegments(parsed, context);\n\n return normalizeSearchText(context.segments.join(' '));\n } catch {\n return '';\n }\n};\n\nexport const matchesNoteSearchQuery = (note: SearchableNoteLike, query: string | NoteSearchQuery) => {\n const parsedQuery = typeof query === 'string' ? parseNoteSearchQuery(query) : query;\n\n if (!parsedQuery.hasFilters) {\n return true;\n }\n\n const haystack =\n note.searchableTextVersion === NOTE_SEARCH_TEXT_SCHEMA_VERSION && typeof note.searchableText === 'string'\n ? normalizeSearchToken(note.searchableText)\n : buildNoteSearchText(note);\n\n return (\n parsedQuery.included.every((term) => haystack.includes(term)) &&\n parsedQuery.excluded.every((term) => !haystack.includes(term))\n );\n};\n\nexport const filterNotesBySearchQuery = <T extends SearchableNoteLike>(notes: T[], query: string | NoteSearchQuery) => {\n const parsedQuery = typeof query === 'string' ? parseNoteSearchQuery(query) : query;\n\n if (!parsedQuery.hasFilters) {\n return notes;\n }\n\n return notes.filter((note) => matchesNoteSearchQuery(note, parsedQuery));\n};\n\nexport const getUnknownNoteSearchNodeTypeCounts = () => {\n return new Map(unknownNoteSearchNodeTypeCounts);\n};\n\nexport const resetUnknownNoteSearchNodeTypeCountsForTest = () => {\n unknownNoteSearchNodeTypeCounts.clear();\n};\n"],"mappings":"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","names":[]}