convex-cms 0.0.1 → 0.0.3

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 (267) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +99 -0
  3. package/admin-dist/nitro.json +15 -0
  4. package/admin-dist/public/assets/CmsEmptyState-CRswfTzk.js +5 -0
  5. package/admin-dist/public/assets/CmsPageHeader-CirpXndm.js +1 -0
  6. package/admin-dist/public/assets/CmsStatusBadge-CbEUpQu-.js +1 -0
  7. package/admin-dist/public/assets/CmsToolbar-BI2nZOXp.js +1 -0
  8. package/admin-dist/public/assets/ContentEntryEditor-CBeCyK_m.js +4 -0
  9. package/admin-dist/public/assets/ErrorState-BIVaWmom.js +1 -0
  10. package/admin-dist/public/assets/TaxonomyFilter-ChaY6Y_x.js +1 -0
  11. package/admin-dist/public/assets/_contentTypeId-DQ8k_Rvw.js +1 -0
  12. package/admin-dist/public/assets/_entryId-CKU_glsK.js +1 -0
  13. package/admin-dist/public/assets/alert-BXjTqrwQ.js +1 -0
  14. package/admin-dist/public/assets/badge-hvUOzpVZ.js +1 -0
  15. package/admin-dist/public/assets/circle-check-big-CF_pR17r.js +1 -0
  16. package/admin-dist/public/assets/command-DU82cJlt.js +1 -0
  17. package/admin-dist/public/assets/content-_LXl3pp7.js +1 -0
  18. package/admin-dist/public/assets/content-types-KjxaXGxY.js +2 -0
  19. package/admin-dist/public/assets/globals-CS6BZ0zp.css +1 -0
  20. package/admin-dist/public/assets/index-DNGIZHL-.js +1 -0
  21. package/admin-dist/public/assets/label-KNtpL71g.js +1 -0
  22. package/admin-dist/public/assets/link-2-Bw2aI4V4.js +1 -0
  23. package/admin-dist/public/assets/list-sYepHjt_.js +1 -0
  24. package/admin-dist/public/assets/main-CKj5yfEi.js +97 -0
  25. package/admin-dist/public/assets/media-Bkrkffm7.js +1 -0
  26. package/admin-dist/public/assets/new._contentTypeId-C3LstjNs.js +1 -0
  27. package/admin-dist/public/assets/plus-DUn8v_Xf.js +1 -0
  28. package/admin-dist/public/assets/rotate-ccw-DJEoHcRI.js +1 -0
  29. package/admin-dist/public/assets/scroll-area-DfIlT0in.js +1 -0
  30. package/admin-dist/public/assets/search-MuAUDJKR.js +1 -0
  31. package/admin-dist/public/assets/select-BD29IXCI.js +1 -0
  32. package/admin-dist/public/assets/settings-DmMyn_6A.js +1 -0
  33. package/admin-dist/public/assets/switch-h3Rrnl5i.js +1 -0
  34. package/admin-dist/public/assets/tabs-imc8h-Dp.js +1 -0
  35. package/admin-dist/public/assets/taxonomies-dAsrT65H.js +1 -0
  36. package/admin-dist/public/assets/textarea-BTy7nwzR.js +1 -0
  37. package/admin-dist/public/assets/trash-SAWKZZHv.js +1 -0
  38. package/admin-dist/public/assets/triangle-alert-E52Vfeuh.js +1 -0
  39. package/admin-dist/public/assets/useBreadcrumbLabel-BECBMCzM.js +1 -0
  40. package/admin-dist/public/assets/usePermissions-Basjs9BT.js +1 -0
  41. package/admin-dist/public/favicon.ico +0 -0
  42. package/admin-dist/server/_chunks/_libs/@date-fns/tz.mjs +217 -0
  43. package/admin-dist/server/_chunks/_libs/@floating-ui/core.mjs +719 -0
  44. package/admin-dist/server/_chunks/_libs/@floating-ui/dom.mjs +622 -0
  45. package/admin-dist/server/_chunks/_libs/@floating-ui/react-dom.mjs +292 -0
  46. package/admin-dist/server/_chunks/_libs/@floating-ui/utils.mjs +320 -0
  47. package/admin-dist/server/_chunks/_libs/@radix-ui/number.mjs +6 -0
  48. package/admin-dist/server/_chunks/_libs/@radix-ui/primitive.mjs +11 -0
  49. package/admin-dist/server/_chunks/_libs/@radix-ui/react-arrow.mjs +23 -0
  50. package/admin-dist/server/_chunks/_libs/@radix-ui/react-avatar.mjs +119 -0
  51. package/admin-dist/server/_chunks/_libs/@radix-ui/react-checkbox.mjs +270 -0
  52. package/admin-dist/server/_chunks/_libs/@radix-ui/react-collection.mjs +69 -0
  53. package/admin-dist/server/_chunks/_libs/@radix-ui/react-compose-refs.mjs +39 -0
  54. package/admin-dist/server/_chunks/_libs/@radix-ui/react-context.mjs +137 -0
  55. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dialog.mjs +325 -0
  56. package/admin-dist/server/_chunks/_libs/@radix-ui/react-direction.mjs +9 -0
  57. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dismissable-layer.mjs +210 -0
  58. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dropdown-menu.mjs +253 -0
  59. package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-guards.mjs +29 -0
  60. package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-scope.mjs +206 -0
  61. package/admin-dist/server/_chunks/_libs/@radix-ui/react-id.mjs +14 -0
  62. package/admin-dist/server/_chunks/_libs/@radix-ui/react-label.mjs +23 -0
  63. package/admin-dist/server/_chunks/_libs/@radix-ui/react-menu.mjs +812 -0
  64. package/admin-dist/server/_chunks/_libs/@radix-ui/react-popover.mjs +300 -0
  65. package/admin-dist/server/_chunks/_libs/@radix-ui/react-popper.mjs +286 -0
  66. package/admin-dist/server/_chunks/_libs/@radix-ui/react-portal.mjs +16 -0
  67. package/admin-dist/server/_chunks/_libs/@radix-ui/react-presence.mjs +128 -0
  68. package/admin-dist/server/_chunks/_libs/@radix-ui/react-primitive.mjs +141 -0
  69. package/admin-dist/server/_chunks/_libs/@radix-ui/react-roving-focus.mjs +224 -0
  70. package/admin-dist/server/_chunks/_libs/@radix-ui/react-scroll-area.mjs +721 -0
  71. package/admin-dist/server/_chunks/_libs/@radix-ui/react-select.mjs +1163 -0
  72. package/admin-dist/server/_chunks/_libs/@radix-ui/react-separator.mjs +28 -0
  73. package/admin-dist/server/_chunks/_libs/@radix-ui/react-slot.mjs +601 -0
  74. package/admin-dist/server/_chunks/_libs/@radix-ui/react-switch.mjs +152 -0
  75. package/admin-dist/server/_chunks/_libs/@radix-ui/react-tabs.mjs +189 -0
  76. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-callback-ref.mjs +11 -0
  77. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-controllable-state.mjs +69 -0
  78. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-effect-event.mjs +1 -0
  79. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-escape-keydown.mjs +17 -0
  80. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-is-hydrated.mjs +15 -0
  81. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-layout-effect.mjs +6 -0
  82. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-previous.mjs +14 -0
  83. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-size.mjs +39 -0
  84. package/admin-dist/server/_chunks/_libs/@radix-ui/react-visually-hidden.mjs +33 -0
  85. package/admin-dist/server/_chunks/_libs/@tanstack/history.mjs +409 -0
  86. package/admin-dist/server/_chunks/_libs/@tanstack/react-router.mjs +1711 -0
  87. package/admin-dist/server/_chunks/_libs/@tanstack/react-store.mjs +56 -0
  88. package/admin-dist/server/_chunks/_libs/@tanstack/router-core.mjs +4829 -0
  89. package/admin-dist/server/_chunks/_libs/@tanstack/store.mjs +134 -0
  90. package/admin-dist/server/_chunks/_libs/react-dom.mjs +10781 -0
  91. package/admin-dist/server/_chunks/_libs/react.mjs +513 -0
  92. package/admin-dist/server/_libs/aria-hidden.mjs +122 -0
  93. package/admin-dist/server/_libs/class-variance-authority.mjs +44 -0
  94. package/admin-dist/server/_libs/clsx.mjs +16 -0
  95. package/admin-dist/server/_libs/cmdk.mjs +315 -0
  96. package/admin-dist/server/_libs/convex.mjs +4841 -0
  97. package/admin-dist/server/_libs/cookie-es.mjs +58 -0
  98. package/admin-dist/server/_libs/croner.mjs +1 -0
  99. package/admin-dist/server/_libs/crossws.mjs +1 -0
  100. package/admin-dist/server/_libs/date-fns.mjs +1716 -0
  101. package/admin-dist/server/_libs/detect-node-es.mjs +1 -0
  102. package/admin-dist/server/_libs/get-nonce.mjs +9 -0
  103. package/admin-dist/server/_libs/h3-v2.mjs +277 -0
  104. package/admin-dist/server/_libs/h3.mjs +401 -0
  105. package/admin-dist/server/_libs/hookable.mjs +1 -0
  106. package/admin-dist/server/_libs/isbot.mjs +20 -0
  107. package/admin-dist/server/_libs/lucide-react.mjs +850 -0
  108. package/admin-dist/server/_libs/ohash.mjs +1 -0
  109. package/admin-dist/server/_libs/react-day-picker.mjs +2201 -0
  110. package/admin-dist/server/_libs/react-remove-scroll-bar.mjs +82 -0
  111. package/admin-dist/server/_libs/react-remove-scroll.mjs +328 -0
  112. package/admin-dist/server/_libs/react-style-singleton.mjs +69 -0
  113. package/admin-dist/server/_libs/rou3.mjs +8 -0
  114. package/admin-dist/server/_libs/seroval-plugins.mjs +58 -0
  115. package/admin-dist/server/_libs/seroval.mjs +1765 -0
  116. package/admin-dist/server/_libs/srvx.mjs +719 -0
  117. package/admin-dist/server/_libs/tailwind-merge.mjs +3010 -0
  118. package/admin-dist/server/_libs/tiny-invariant.mjs +12 -0
  119. package/admin-dist/server/_libs/tiny-warning.mjs +5 -0
  120. package/admin-dist/server/_libs/tslib.mjs +39 -0
  121. package/admin-dist/server/_libs/ufo.mjs +54 -0
  122. package/admin-dist/server/_libs/unctx.mjs +1 -0
  123. package/admin-dist/server/_libs/unstorage.mjs +1 -0
  124. package/admin-dist/server/_libs/use-callback-ref.mjs +66 -0
  125. package/admin-dist/server/_libs/use-sidecar.mjs +106 -0
  126. package/admin-dist/server/_libs/use-sync-external-store.mjs +139 -0
  127. package/admin-dist/server/_libs/zod.mjs +4223 -0
  128. package/admin-dist/server/_ssr/CmsEmptyState-DU7-7-mV.mjs +290 -0
  129. package/admin-dist/server/_ssr/CmsPageHeader-CseW0AHm.mjs +24 -0
  130. package/admin-dist/server/_ssr/CmsStatusBadge-B_pi4KCp.mjs +127 -0
  131. package/admin-dist/server/_ssr/CmsToolbar-X75ex6ek.mjs +49 -0
  132. package/admin-dist/server/_ssr/ContentEntryEditor-CepusRsA.mjs +3720 -0
  133. package/admin-dist/server/_ssr/ErrorState-cI-bKLez.mjs +89 -0
  134. package/admin-dist/server/_ssr/TaxonomyFilter-Bwrq0-cz.mjs +188 -0
  135. package/admin-dist/server/_ssr/_contentTypeId-BqYKEcLr.mjs +379 -0
  136. package/admin-dist/server/_ssr/_entryId-CRfnqeDf.mjs +161 -0
  137. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-BwDlABVk.mjs +4 -0
  138. package/admin-dist/server/_ssr/alert-CVt45UUP.mjs +92 -0
  139. package/admin-dist/server/_ssr/badge-6BsP37vG.mjs +125 -0
  140. package/admin-dist/server/_ssr/command-fy8epIKf.mjs +128 -0
  141. package/admin-dist/server/_ssr/config.server-D7JHDcDv.mjs +117 -0
  142. package/admin-dist/server/_ssr/content-B5RhL7uW.mjs +532 -0
  143. package/admin-dist/server/_ssr/content-types-BIOqCQYN.mjs +1166 -0
  144. package/admin-dist/server/_ssr/index-DHSHDPt1.mjs +193 -0
  145. package/admin-dist/server/_ssr/index.mjs +1275 -0
  146. package/admin-dist/server/_ssr/label-C8Dko1j7.mjs +22 -0
  147. package/admin-dist/server/_ssr/media-CSx3XttC.mjs +1832 -0
  148. package/admin-dist/server/_ssr/new._contentTypeId-DzanEZQM.mjs +144 -0
  149. package/admin-dist/server/_ssr/router-DDWcF-kt.mjs +1556 -0
  150. package/admin-dist/server/_ssr/scroll-area-bjPYwhXN.mjs +59 -0
  151. package/admin-dist/server/_ssr/select-BUhDDf4T.mjs +142 -0
  152. package/admin-dist/server/_ssr/settings-DAsxnw2q.mjs +348 -0
  153. package/admin-dist/server/_ssr/start-HYkvq4Ni.mjs +4 -0
  154. package/admin-dist/server/_ssr/switch-BgyRtQ1Z.mjs +31 -0
  155. package/admin-dist/server/_ssr/tabs-DzMdRB1A.mjs +628 -0
  156. package/admin-dist/server/_ssr/taxonomies-C8j8g5Q5.mjs +915 -0
  157. package/admin-dist/server/_ssr/textarea-9jNeYJSc.mjs +18 -0
  158. package/admin-dist/server/_ssr/trash-DYMxwhZB.mjs +291 -0
  159. package/admin-dist/server/_ssr/useBreadcrumbLabel-FNSAr2Ha.mjs +16 -0
  160. package/admin-dist/server/_ssr/usePermissions-BJGGahrJ.mjs +68 -0
  161. package/admin-dist/server/favicon.ico +0 -0
  162. package/admin-dist/server/index.mjs +627 -0
  163. package/dist/cli/index.js +0 -0
  164. package/dist/client/admin-config.d.ts +0 -1
  165. package/dist/client/admin-config.d.ts.map +1 -1
  166. package/dist/client/admin-config.js +0 -1
  167. package/dist/client/admin-config.js.map +1 -1
  168. package/dist/client/adminApi.d.ts.map +1 -1
  169. package/dist/client/agentTools.d.ts +1237 -135
  170. package/dist/client/agentTools.d.ts.map +1 -1
  171. package/dist/client/agentTools.js +33 -9
  172. package/dist/client/agentTools.js.map +1 -1
  173. package/dist/client/index.d.ts +1 -1
  174. package/dist/client/index.d.ts.map +1 -1
  175. package/dist/client/index.js.map +1 -1
  176. package/dist/component/_generated/component.d.ts +9 -0
  177. package/dist/component/_generated/component.d.ts.map +1 -1
  178. package/dist/component/mediaAssets.d.ts +35 -0
  179. package/dist/component/mediaAssets.d.ts.map +1 -1
  180. package/dist/component/mediaAssets.js +81 -0
  181. package/dist/component/mediaAssets.js.map +1 -1
  182. package/dist/test.d.ts.map +1 -1
  183. package/dist/test.js +2 -1
  184. package/dist/test.js.map +1 -1
  185. package/package.json +24 -9
  186. package/dist/component/auditLog.d.ts +0 -410
  187. package/dist/component/auditLog.d.ts.map +0 -1
  188. package/dist/component/auditLog.js +0 -607
  189. package/dist/component/auditLog.js.map +0 -1
  190. package/dist/component/types.d.ts +0 -4
  191. package/dist/component/types.d.ts.map +0 -1
  192. package/dist/component/types.js +0 -2
  193. package/dist/component/types.js.map +0 -1
  194. package/src/cli/commands/admin.ts +0 -104
  195. package/src/cli/index.ts +0 -21
  196. package/src/cli/utils/detectConvexUrl.ts +0 -54
  197. package/src/cli/utils/openBrowser.ts +0 -16
  198. package/src/client/admin-config.ts +0 -138
  199. package/src/client/adminApi.ts +0 -942
  200. package/src/client/agentTools.ts +0 -1311
  201. package/src/client/argTypes.ts +0 -316
  202. package/src/client/field-types.ts +0 -187
  203. package/src/client/index.ts +0 -1301
  204. package/src/client/queryBuilder.ts +0 -1100
  205. package/src/client/schema/codegen.ts +0 -500
  206. package/src/client/schema/defineContentType.ts +0 -501
  207. package/src/client/schema/index.ts +0 -169
  208. package/src/client/schema/schemaDrift.ts +0 -574
  209. package/src/client/schema/typedClient.ts +0 -688
  210. package/src/client/schema/types.ts +0 -666
  211. package/src/client/types.ts +0 -723
  212. package/src/client/workflows.ts +0 -141
  213. package/src/client/wrapper.ts +0 -4304
  214. package/src/component/_generated/api.ts +0 -140
  215. package/src/component/_generated/component.ts +0 -5029
  216. package/src/component/_generated/dataModel.ts +0 -60
  217. package/src/component/_generated/server.ts +0 -156
  218. package/src/component/authorization.ts +0 -647
  219. package/src/component/authorizationHooks.ts +0 -668
  220. package/src/component/bulkOperations.ts +0 -687
  221. package/src/component/contentEntries.ts +0 -1976
  222. package/src/component/contentEntryMutations.ts +0 -1223
  223. package/src/component/contentEntryValidation.ts +0 -707
  224. package/src/component/contentLock.ts +0 -550
  225. package/src/component/contentTypeMigration.ts +0 -1064
  226. package/src/component/contentTypeMutations.ts +0 -969
  227. package/src/component/contentTypes.ts +0 -346
  228. package/src/component/convex.config.ts +0 -44
  229. package/src/component/documentTypes.ts +0 -240
  230. package/src/component/eventEmitter.ts +0 -485
  231. package/src/component/exportImport.ts +0 -1169
  232. package/src/component/index.ts +0 -491
  233. package/src/component/lib/deepReferenceResolver.ts +0 -999
  234. package/src/component/lib/errors.ts +0 -816
  235. package/src/component/lib/index.ts +0 -145
  236. package/src/component/lib/mediaReferenceResolver.ts +0 -495
  237. package/src/component/lib/metadataExtractor.ts +0 -792
  238. package/src/component/lib/mutationAuth.ts +0 -199
  239. package/src/component/lib/queries.ts +0 -79
  240. package/src/component/lib/ragContentChunker.ts +0 -1371
  241. package/src/component/lib/referenceResolver.ts +0 -430
  242. package/src/component/lib/slugGenerator.ts +0 -262
  243. package/src/component/lib/slugUniqueness.ts +0 -333
  244. package/src/component/lib/softDelete.ts +0 -44
  245. package/src/component/localeFallbackChain.ts +0 -673
  246. package/src/component/localeFields.ts +0 -896
  247. package/src/component/mediaAssetMutations.ts +0 -725
  248. package/src/component/mediaAssets.ts +0 -932
  249. package/src/component/mediaFolderMutations.ts +0 -1046
  250. package/src/component/mediaUploadMutations.ts +0 -224
  251. package/src/component/mediaVariantMutations.ts +0 -900
  252. package/src/component/mediaVariants.ts +0 -793
  253. package/src/component/ragContentIndexer.ts +0 -1067
  254. package/src/component/rateLimitHooks.ts +0 -572
  255. package/src/component/roles.ts +0 -1360
  256. package/src/component/scheduledPublish.ts +0 -358
  257. package/src/component/schema.ts +0 -617
  258. package/src/component/taxonomies.ts +0 -949
  259. package/src/component/taxonomyMutations.ts +0 -1210
  260. package/src/component/trash.ts +0 -724
  261. package/src/component/userContext.ts +0 -898
  262. package/src/component/validation.ts +0 -1388
  263. package/src/component/validators.ts +0 -949
  264. package/src/component/versionMutations.ts +0 -392
  265. package/src/component/webhookTrigger.ts +0 -1922
  266. package/src/react/index.ts +0 -898
  267. package/src/test.ts +0 -1580
@@ -1,1067 +0,0 @@
1
- /**
2
- * RAG Content Indexer
3
- *
4
- * Background job system to automatically index published content for RAG pipelines.
5
- * Triggers on content publish events and maintains sync between CMS content and vector indexes.
6
- *
7
- * Architecture:
8
- * 1. Content publish events are captured via the event emitter system (cmsEvents table)
9
- * 2. A background processor polls for unprocessed "contentEntry.published" events
10
- * 3. For each event, content is extracted and chunked using ragContentChunker
11
- * 4. Chunks are passed to a user-provided indexing callback (e.g., @convex-dev/rag)
12
- * 5. Events are marked as processed after successful indexing
13
- *
14
- * The indexer supports:
15
- * - Automatic indexing on publish events
16
- * - Manual reindexing of specific entries
17
- * - Bulk reindexing of all published content
18
- * - Configurable chunking options
19
- * - Index removal on unpublish/delete events
20
- *
21
- * @example
22
- * ```typescript
23
- * // In your Convex action, process pending indexing jobs:
24
- * import { processPublishEvents } from "./ragContentIndexer";
25
- *
26
- * export const runIndexer = action({
27
- * handler: async (ctx) => {
28
- * return await processPublishEventsAction(ctx, {
29
- * onIndex: async (entryId, chunks, metadata) => {
30
- * // Add to your vector index (e.g., @convex-dev/rag)
31
- * await rag.add(ctx, {
32
- * namespace: `cms:${metadata.contentType}`,
33
- * key: entryId,
34
- * chunks: chunks.map(c => c.text),
35
- * });
36
- * },
37
- * onRemove: async (entryId) => {
38
- * // Remove from your vector index
39
- * await rag.remove(ctx, { key: entryId });
40
- * },
41
- * });
42
- * },
43
- * });
44
- * ```
45
- *
46
- * @module
47
- */
48
-
49
- import { v } from "convex/values";
50
- import { mutation, query, internalMutation, internalQuery } from "./_generated/server.js";
51
- import { internal } from "./_generated/api.js";
52
- import type { Doc } from "./_generated/dataModel.js";
53
- import {
54
- chunkContentEntry,
55
- type ContentEntryInfo,
56
- type ContentTypeInfo,
57
- type RagExtractionOptions,
58
- } from "./lib/ragContentChunker.js";
59
-
60
- // =============================================================================
61
- // Types
62
- // =============================================================================
63
-
64
- /**
65
- * Configuration for the RAG indexer.
66
- */
67
- export interface RagIndexerConfig {
68
- /**
69
- * Whether automatic indexing on publish events is enabled.
70
- * @default true
71
- */
72
- autoIndexOnPublish?: boolean;
73
-
74
- /**
75
- * Whether to automatically remove from index on unpublish.
76
- * @default true
77
- */
78
- autoRemoveOnUnpublish?: boolean;
79
-
80
- /**
81
- * Whether to automatically remove from index on delete.
82
- * @default true
83
- */
84
- autoRemoveOnDelete?: boolean;
85
-
86
- /**
87
- * Maximum number of events to process in a single batch.
88
- * @default 50
89
- */
90
- batchSize?: number;
91
-
92
- /**
93
- * Interval in milliseconds for the background polling job.
94
- * @default 60000 (1 minute)
95
- */
96
- pollingIntervalMs?: number;
97
-
98
- /**
99
- * Options for content extraction and chunking.
100
- */
101
- extractionOptions?: Partial<RagExtractionOptions>;
102
-
103
- /**
104
- * Content types to include in indexing.
105
- * If not specified, all content types are indexed.
106
- */
107
- includeContentTypes?: string[];
108
-
109
- /**
110
- * Content types to exclude from indexing.
111
- */
112
- excludeContentTypes?: string[];
113
-
114
- /**
115
- * Namespace prefix for organizing indexed content.
116
- * @default "cms"
117
- */
118
- namespacePrefix?: string;
119
- }
120
-
121
- /**
122
- * Result of processing a single entry for indexing.
123
- */
124
- export interface IndexEntryResult {
125
- entryId: string;
126
- success: boolean;
127
- chunksCreated: number;
128
- error?: string;
129
- }
130
-
131
- /**
132
- * Result of processing multiple events.
133
- */
134
- export interface ProcessEventsResult {
135
- processed: number;
136
- indexed: number;
137
- removed: number;
138
- errors: Array<{
139
- eventId: string;
140
- entryId: string;
141
- error: string;
142
- }>;
143
- hasMore: boolean;
144
- }
145
-
146
- /**
147
- * Metadata about an indexed entry for callback consumers.
148
- */
149
- export interface IndexedEntryMetadata {
150
- entryId: string;
151
- contentType: string;
152
- contentTypeDisplayName: string;
153
- slug: string;
154
- locale?: string;
155
- version: number;
156
- title?: string;
157
- publishedAt?: number;
158
- namespace: string;
159
- }
160
-
161
- /**
162
- * Statistics about the indexing state.
163
- */
164
- export interface IndexingStats {
165
- /** Total number of published entries */
166
- totalPublished: number;
167
- /** Number of entries pending indexing (unprocessed publish events) */
168
- pendingIndexing: number;
169
- /** Number of entries pending removal (unprocessed unpublish/delete events) */
170
- pendingRemoval: number;
171
- /** Breakdown by content type */
172
- byContentType: Record<string, {
173
- published: number;
174
- pending: number;
175
- }>;
176
- }
177
-
178
- // =============================================================================
179
- // Default Configuration
180
- // =============================================================================
181
-
182
- const DEFAULT_CONFIG: Required<RagIndexerConfig> = {
183
- autoIndexOnPublish: true,
184
- autoRemoveOnUnpublish: true,
185
- autoRemoveOnDelete: true,
186
- batchSize: 50,
187
- pollingIntervalMs: 60000,
188
- extractionOptions: {},
189
- includeContentTypes: [],
190
- excludeContentTypes: [],
191
- namespacePrefix: "cms",
192
- };
193
-
194
- // =============================================================================
195
- // Internal Queries
196
- // =============================================================================
197
-
198
- /**
199
- * Internal query to get unprocessed publish-related events.
200
- * Returns events for indexing (published) and removal (unpublished, deleted).
201
- */
202
- export const getUnprocessedIndexingEvents = internalQuery({
203
- args: {
204
- limit: v.optional(v.number()),
205
- includeContentTypes: v.optional(v.array(v.string())),
206
- excludeContentTypes: v.optional(v.array(v.string())),
207
- },
208
- handler: async (ctx, args) => {
209
- const { limit = 50, includeContentTypes = [], excludeContentTypes = [] } = args;
210
-
211
- // Get unprocessed events
212
- const events = await ctx.db
213
- .query("cmsEvents")
214
- .withIndex("by_processed", (q) => q.eq("processed", false))
215
- .order("asc")
216
- .take(limit * 2); // Over-fetch to account for filtering
217
-
218
- // Filter to only content entry events that affect indexing
219
- const indexingActions = ["published", "unpublished", "deleted", "restored"];
220
-
221
- const filteredEvents = events.filter((event) => {
222
- // Must be a content entry event
223
- if (event.resourceType !== "contentEntry") return false;
224
-
225
- // Must be an indexing-related action
226
- if (!indexingActions.includes(event.action)) return false;
227
-
228
- // Apply content type filters if specified
229
- const payload = event.payload as { contentTypeName?: string } | undefined;
230
- const contentTypeName = payload?.contentTypeName;
231
-
232
- if (contentTypeName) {
233
- if (includeContentTypes.length > 0 && !includeContentTypes.includes(contentTypeName)) {
234
- return false;
235
- }
236
- if (excludeContentTypes.length > 0 && excludeContentTypes.includes(contentTypeName)) {
237
- return false;
238
- }
239
- }
240
-
241
- return true;
242
- });
243
-
244
- return filteredEvents.slice(0, limit);
245
- },
246
- });
247
-
248
- /**
249
- * Internal query to get entry data for indexing.
250
- */
251
- export const getEntryForIndexing = internalQuery({
252
- args: {
253
- entryId: v.id("contentEntries"),
254
- },
255
- handler: async (ctx, args) => {
256
- const entry = await ctx.db.get(args.entryId);
257
- if (!entry) return null;
258
-
259
- // Get the content type
260
- const contentType = await ctx.db.get(entry.contentTypeId);
261
- if (!contentType) return null;
262
-
263
- return {
264
- entry,
265
- contentType,
266
- };
267
- },
268
- });
269
-
270
- /**
271
- * Internal query to get multiple entries for batch indexing.
272
- */
273
- export const getEntriesForIndexing = internalQuery({
274
- args: {
275
- entryIds: v.array(v.id("contentEntries")),
276
- },
277
- handler: async (ctx, args) => {
278
- const results: Array<{
279
- entry: Doc<"contentEntries">;
280
- contentType: Doc<"contentTypes">;
281
- } | null> = [];
282
-
283
- // Fetch all entries and their content types
284
- const contentTypeCache = new Map<string, Doc<"contentTypes">>();
285
-
286
- for (const entryId of args.entryIds) {
287
- const entry = await ctx.db.get(entryId);
288
- if (!entry) {
289
- results.push(null);
290
- continue;
291
- }
292
-
293
- let contentType = contentTypeCache.get(entry.contentTypeId);
294
- if (!contentType) {
295
- const fetchedContentType = await ctx.db.get(entry.contentTypeId);
296
- if (fetchedContentType) {
297
- contentType = fetchedContentType;
298
- contentTypeCache.set(entry.contentTypeId, fetchedContentType);
299
- }
300
- }
301
-
302
- if (!contentType) {
303
- results.push(null);
304
- continue;
305
- }
306
-
307
- results.push({ entry, contentType });
308
- }
309
-
310
- return results;
311
- },
312
- });
313
-
314
- // =============================================================================
315
- // Public Queries
316
- // =============================================================================
317
-
318
- /**
319
- * Query to get statistics about the indexing state.
320
- *
321
- * Returns counts of published entries, pending indexing events,
322
- * and breakdown by content type.
323
- */
324
- export const getIndexingStats = query({
325
- args: {},
326
- returns: v.object({
327
- totalPublished: v.number(),
328
- pendingIndexing: v.number(),
329
- pendingRemoval: v.number(),
330
- byContentType: v.any(),
331
- }),
332
- handler: async (ctx) => {
333
- // Count published entries
334
- const publishedEntries = await ctx.db
335
- .query("contentEntries")
336
- .withIndex("by_status", (q) => q.eq("status", "published"))
337
- .filter((q) => q.eq(q.field("deletedAt"), undefined))
338
- .collect();
339
-
340
- // Count unprocessed publish events
341
- const unprocessedEvents = await ctx.db
342
- .query("cmsEvents")
343
- .withIndex("by_processed", (q) => q.eq("processed", false))
344
- .filter((q) => q.eq(q.field("resourceType"), "contentEntry"))
345
- .collect();
346
-
347
- const pendingIndexing = unprocessedEvents.filter((e) => e.action === "published").length;
348
- const pendingRemoval = unprocessedEvents.filter((e) =>
349
- ["unpublished", "deleted"].includes(e.action)
350
- ).length;
351
-
352
- // Get content types for breakdown
353
- const contentTypes = await ctx.db.query("contentTypes").collect();
354
- const contentTypeMap = new Map(contentTypes.map((ct) => [ct._id, ct.name]));
355
-
356
- // Build breakdown by content type
357
- const byContentType: Record<string, { published: number; pending: number }> = {};
358
-
359
- for (const entry of publishedEntries) {
360
- const typeName = contentTypeMap.get(entry.contentTypeId) || "unknown";
361
- if (!byContentType[typeName]) {
362
- byContentType[typeName] = { published: 0, pending: 0 };
363
- }
364
- byContentType[typeName].published++;
365
- }
366
-
367
- for (const event of unprocessedEvents) {
368
- if (event.action !== "published") continue;
369
- const payload = event.payload as { contentTypeName?: string } | undefined;
370
- const typeName = payload?.contentTypeName || "unknown";
371
- if (!byContentType[typeName]) {
372
- byContentType[typeName] = { published: 0, pending: 0 };
373
- }
374
- byContentType[typeName].pending++;
375
- }
376
-
377
- return {
378
- totalPublished: publishedEntries.length,
379
- pendingIndexing,
380
- pendingRemoval,
381
- byContentType,
382
- };
383
- },
384
- });
385
-
386
- /**
387
- * Query to check if an entry needs reindexing.
388
- *
389
- * Returns true if there are unprocessed events for the entry,
390
- * or if the entry has been updated since last indexing.
391
- */
392
- export const needsReindexing = query({
393
- args: {
394
- entryId: v.id("contentEntries"),
395
- },
396
- returns: v.boolean(),
397
- handler: async (ctx, args) => {
398
- // Check for any unprocessed events for this entry
399
- const events = await ctx.db
400
- .query("cmsEvents")
401
- .withIndex("by_resource", (q) =>
402
- q.eq("resourceType", "contentEntry").eq("resourceId", args.entryId)
403
- )
404
- .filter((q) => q.eq(q.field("processed"), false))
405
- .first();
406
-
407
- return events !== null;
408
- },
409
- });
410
-
411
- // =============================================================================
412
- // Mutations
413
- // =============================================================================
414
-
415
- /**
416
- * Mutation to prepare content for indexing.
417
- *
418
- * This extracts and chunks content from an entry, returning the chunks
419
- * and metadata for the caller to pass to their vector index.
420
- *
421
- * @param entryId - The content entry ID to prepare for indexing
422
- * @param options - Optional extraction options
423
- *
424
- * @returns Chunks and metadata for indexing, or null if entry not found/not published
425
- */
426
- export const prepareEntryForIndexing = query({
427
- args: {
428
- entryId: v.id("contentEntries"),
429
- options: v.optional(
430
- v.object({
431
- includeFields: v.optional(v.array(v.string())),
432
- excludeFields: v.optional(v.array(v.string())),
433
- maxCharsSoftLimit: v.optional(v.number()),
434
- namespacePrefix: v.optional(v.string()),
435
- })
436
- ),
437
- },
438
- returns: v.union(
439
- v.object({
440
- entryId: v.string(),
441
- chunks: v.array(
442
- v.object({
443
- text: v.string(),
444
- metadata: v.any(),
445
- })
446
- ),
447
- metadata: v.object({
448
- entryId: v.string(),
449
- contentType: v.string(),
450
- contentTypeDisplayName: v.string(),
451
- slug: v.string(),
452
- locale: v.optional(v.string()),
453
- version: v.number(),
454
- title: v.optional(v.string()),
455
- publishedAt: v.optional(v.number()),
456
- namespace: v.string(),
457
- }),
458
- }),
459
- v.null()
460
- ),
461
- handler: async (ctx, args) => {
462
- const { entryId, options = {} } = args;
463
-
464
- // Get entry and content type
465
- const entry = await ctx.db.get(entryId);
466
- if (!entry) return null;
467
-
468
- // Only index published content
469
- if (entry.status !== "published") return null;
470
-
471
- const contentType = await ctx.db.get(entry.contentTypeId);
472
- if (!contentType) return null;
473
-
474
- // Build extraction options
475
- const extractionOptions: Partial<RagExtractionOptions> = {
476
- includeMetadata: true,
477
- includeFields: options.includeFields,
478
- excludeFields: options.excludeFields,
479
- chunkOptions: {
480
- maxCharsSoftLimit: options.maxCharsSoftLimit ?? 1000,
481
- },
482
- };
483
-
484
- // Convert to the expected types
485
- const entryInfo: ContentEntryInfo = {
486
- _id: entry._id,
487
- contentTypeId: entry.contentTypeId,
488
- slug: entry.slug,
489
- status: entry.status,
490
- data: entry.data as Record<string, unknown>,
491
- locale: entry.locale,
492
- version: entry.version,
493
- _creationTime: entry._creationTime,
494
- firstPublishedAt: entry.firstPublishedAt,
495
- lastPublishedAt: entry.lastPublishedAt,
496
- };
497
-
498
- const contentTypeInfo: ContentTypeInfo = {
499
- _id: contentType._id,
500
- name: contentType.name,
501
- displayName: contentType.displayName,
502
- fields: contentType.fields as ContentTypeInfo["fields"],
503
- titleField: contentType.titleField,
504
- slugField: contentType.slugField,
505
- };
506
-
507
- // Extract and chunk content
508
- const chunks = chunkContentEntry(entryInfo, contentTypeInfo, extractionOptions);
509
-
510
- // Build namespace
511
- const namespacePrefix = options.namespacePrefix ?? "cms";
512
- const namespace = entry.locale
513
- ? `${namespacePrefix}:${contentType.name}:${entry.locale}`
514
- : `${namespacePrefix}:${contentType.name}`;
515
-
516
- // Get title from chunks metadata or entry data
517
- const title = chunks[0]?.metadata?.title || (entry.data as Record<string, unknown>)?.title as string | undefined;
518
-
519
- return {
520
- entryId: entry._id,
521
- chunks: chunks.map((c) => ({
522
- text: c.text,
523
- metadata: c.metadata,
524
- })),
525
- metadata: {
526
- entryId: entry._id,
527
- contentType: contentType.name,
528
- contentTypeDisplayName: contentType.displayName,
529
- slug: entry.slug,
530
- locale: entry.locale,
531
- version: entry.version,
532
- title,
533
- publishedAt: entry.lastPublishedAt,
534
- namespace,
535
- },
536
- };
537
- },
538
- });
539
-
540
- /**
541
- * Mutation to mark indexing events as processed.
542
- *
543
- * Call this after successfully indexing content to prevent reprocessing.
544
- *
545
- * @param eventIds - Array of event IDs to mark as processed
546
- */
547
- export const markIndexingEventsProcessed = mutation({
548
- args: {
549
- eventIds: v.array(v.id("cmsEvents")),
550
- },
551
- returns: v.object({
552
- processedCount: v.number(),
553
- }),
554
- handler: async (ctx, args) => {
555
- const { eventIds } = args;
556
- const now = Date.now();
557
- let processedCount = 0;
558
-
559
- for (const eventId of eventIds) {
560
- const event = await ctx.db.get(eventId);
561
- if (event && !event.processed) {
562
- await ctx.db.patch(eventId, {
563
- processed: true,
564
- processedAt: now,
565
- });
566
- processedCount++;
567
- }
568
- }
569
-
570
- return { processedCount };
571
- },
572
- });
573
-
574
- /**
575
- * Internal mutation to request reindexing of an entry.
576
- *
577
- * Creates a synthetic "published" event to trigger reindexing.
578
- */
579
- export const requestReindex = internalMutation({
580
- args: {
581
- entryId: v.id("contentEntries"),
582
- userId: v.optional(v.string()),
583
- },
584
- handler: async (ctx, args) => {
585
- const { entryId, userId } = args;
586
-
587
- // Get entry details
588
- const entry = await ctx.db.get(entryId);
589
- if (!entry) {
590
- throw new Error(`Entry not found: ${entryId}`);
591
- }
592
-
593
- if (entry.status !== "published") {
594
- throw new Error(`Entry is not published: ${entryId}`);
595
- }
596
-
597
- // Get content type for payload
598
- const contentType = await ctx.db.get(entry.contentTypeId);
599
- if (!contentType) {
600
- throw new Error(`Content type not found: ${entry.contentTypeId}`);
601
- }
602
-
603
- // Create a reindex event
604
- await ctx.db.insert("cmsEvents", {
605
- eventType: "contentEntry.published",
606
- resourceType: "contentEntry",
607
- resourceId: entryId,
608
- action: "published",
609
- payload: {
610
- slug: entry.slug,
611
- contentTypeName: contentType.name,
612
- contentTypeId: contentType._id,
613
- status: entry.status,
614
- version: entry.version,
615
- locale: entry.locale,
616
- changeDescription: "Reindex requested",
617
- },
618
- userId,
619
- processed: false,
620
- metadata: { reindexRequest: true },
621
- });
622
-
623
- return { success: true };
624
- },
625
- });
626
-
627
- /**
628
- * Public mutation to request reindexing of a specific entry.
629
- */
630
- export const requestEntryReindex = mutation({
631
- args: {
632
- entryId: v.id("contentEntries"),
633
- userId: v.optional(v.string()),
634
- },
635
- returns: v.object({
636
- success: v.boolean(),
637
- message: v.string(),
638
- }),
639
- handler: async (ctx, args) => {
640
- const { entryId, userId } = args;
641
-
642
- // Get entry details
643
- const entry = await ctx.db.get(entryId);
644
- if (!entry) {
645
- return { success: false, message: "Entry not found" };
646
- }
647
-
648
- if (entry.status !== "published") {
649
- return { success: false, message: "Entry is not published" };
650
- }
651
-
652
- // Get content type for payload
653
- const contentType = await ctx.db.get(entry.contentTypeId);
654
- if (!contentType) {
655
- return { success: false, message: "Content type not found" };
656
- }
657
-
658
- // Create a reindex event
659
- await ctx.db.insert("cmsEvents", {
660
- eventType: "contentEntry.published",
661
- resourceType: "contentEntry",
662
- resourceId: entryId,
663
- action: "published",
664
- payload: {
665
- slug: entry.slug,
666
- contentTypeName: contentType.name,
667
- contentTypeId: contentType._id,
668
- status: entry.status,
669
- version: entry.version,
670
- locale: entry.locale,
671
- changeDescription: "Reindex requested",
672
- },
673
- userId,
674
- processed: false,
675
- metadata: { reindexRequest: true },
676
- });
677
-
678
- return { success: true, message: "Reindex event created" };
679
- },
680
- });
681
-
682
- /**
683
- * Mutation to request reindexing of all published content.
684
- *
685
- * Creates publish events for all currently published entries,
686
- * which will be processed by the background indexer.
687
- *
688
- * @param contentTypeId - Optional content type to filter by
689
- * @param batchSize - Number of entries to process per batch
690
- * @param cursor - Pagination cursor for large datasets
691
- */
692
- export const requestBulkReindex = mutation({
693
- args: {
694
- contentTypeId: v.optional(v.id("contentTypes")),
695
- batchSize: v.optional(v.number()),
696
- cursor: v.optional(v.string()),
697
- userId: v.optional(v.string()),
698
- },
699
- returns: v.object({
700
- eventsCreated: v.number(),
701
- hasMore: v.boolean(),
702
- nextCursor: v.optional(v.string()),
703
- }),
704
- handler: async (ctx, args) => {
705
- const { contentTypeId, batchSize = 100, userId } = args;
706
-
707
- // Build query for published entries
708
- const entriesQuery = ctx.db
709
- .query("contentEntries")
710
- .withIndex("by_status", (q) => q.eq("status", "published"))
711
- .filter((q) => q.eq(q.field("deletedAt"), undefined));
712
-
713
- // Apply content type filter if specified
714
- const entries = await entriesQuery.take(batchSize + 1);
715
- const hasMore = entries.length > batchSize;
716
- const entriesToProcess = entries.slice(0, batchSize);
717
-
718
- // Filter by content type if specified
719
- const filteredEntries = contentTypeId
720
- ? entriesToProcess.filter((e) => e.contentTypeId === contentTypeId)
721
- : entriesToProcess;
722
-
723
- // Get content types for payloads
724
- const contentTypeIds = [...new Set(filteredEntries.map((e) => e.contentTypeId))];
725
- const contentTypes = await Promise.all(contentTypeIds.map((id) => ctx.db.get(id)));
726
- const contentTypeMap = new Map(
727
- contentTypes.filter(Boolean).map((ct) => [ct!._id, ct!])
728
- );
729
-
730
- // Create reindex events
731
- let eventsCreated = 0;
732
- for (const entry of filteredEntries) {
733
- const contentType = contentTypeMap.get(entry.contentTypeId);
734
- if (!contentType) continue;
735
-
736
- await ctx.db.insert("cmsEvents", {
737
- eventType: "contentEntry.published",
738
- resourceType: "contentEntry",
739
- resourceId: entry._id,
740
- action: "published",
741
- payload: {
742
- slug: entry.slug,
743
- contentTypeName: contentType.name,
744
- contentTypeId: contentType._id,
745
- status: entry.status,
746
- version: entry.version,
747
- locale: entry.locale,
748
- changeDescription: "Bulk reindex requested",
749
- },
750
- userId,
751
- processed: false,
752
- metadata: { bulkReindex: true },
753
- });
754
- eventsCreated++;
755
- }
756
-
757
- // Calculate next cursor
758
- const nextCursor = hasMore ? entriesToProcess[entriesToProcess.length - 1]?._id : undefined;
759
-
760
- return {
761
- eventsCreated,
762
- hasMore,
763
- nextCursor,
764
- };
765
- },
766
- });
767
-
768
- // =============================================================================
769
- // Background Job Scheduling
770
- // =============================================================================
771
-
772
- /**
773
- * Internal mutation to process pending indexing events.
774
- *
775
- * This is called by the background scheduler to process events in batches.
776
- * Returns information about what was processed so the action can perform indexing.
777
- */
778
- export const getIndexingBatch = internalQuery({
779
- args: {
780
- config: v.optional(
781
- v.object({
782
- batchSize: v.optional(v.number()),
783
- includeContentTypes: v.optional(v.array(v.string())),
784
- excludeContentTypes: v.optional(v.array(v.string())),
785
- namespacePrefix: v.optional(v.string()),
786
- })
787
- ),
788
- },
789
- handler: async (ctx, args) => {
790
- const config = { ...DEFAULT_CONFIG, ...(args.config || {}) };
791
-
792
- // Get unprocessed events
793
- const events = await ctx.db
794
- .query("cmsEvents")
795
- .withIndex("by_processed", (q) => q.eq("processed", false))
796
- .order("asc")
797
- .take(config.batchSize * 2);
798
-
799
- // Filter to indexing-related content entry events
800
- const indexingActions = ["published", "unpublished", "deleted", "restored"];
801
-
802
- const filteredEvents = events.filter((event) => {
803
- if (event.resourceType !== "contentEntry") return false;
804
- if (!indexingActions.includes(event.action)) return false;
805
-
806
- const payload = event.payload as { contentTypeName?: string } | undefined;
807
- const contentTypeName = payload?.contentTypeName;
808
-
809
- if (contentTypeName) {
810
- if (
811
- config.includeContentTypes.length > 0 &&
812
- !config.includeContentTypes.includes(contentTypeName)
813
- ) {
814
- return false;
815
- }
816
- if (
817
- config.excludeContentTypes.length > 0 &&
818
- config.excludeContentTypes.includes(contentTypeName)
819
- ) {
820
- return false;
821
- }
822
- }
823
-
824
- return true;
825
- }).slice(0, config.batchSize);
826
-
827
- // Categorize events
828
- const toIndex: Array<{ eventId: string; entryId: string }> = [];
829
- const toRemove: Array<{ eventId: string; entryId: string }> = [];
830
-
831
- for (const event of filteredEvents) {
832
- const item = { eventId: event._id, entryId: event.resourceId };
833
-
834
- if (event.action === "published" || event.action === "restored") {
835
- toIndex.push(item);
836
- } else if (event.action === "unpublished" || event.action === "deleted") {
837
- toRemove.push(item);
838
- }
839
- }
840
-
841
- return {
842
- toIndex,
843
- toRemove,
844
- hasMore: events.length > config.batchSize,
845
- };
846
- },
847
- });
848
-
849
- /**
850
- * Schedules the next background indexing run.
851
- *
852
- * Call this to set up recurring background processing.
853
- *
854
- * @param delayMs - Delay before next run in milliseconds
855
- */
856
- export const scheduleNextIndexingRun = mutation({
857
- args: {
858
- delayMs: v.optional(v.number()),
859
- },
860
- returns: v.object({
861
- scheduledAt: v.number(),
862
- }),
863
- handler: async (ctx, args) => {
864
- const delayMs = args.delayMs ?? DEFAULT_CONFIG.pollingIntervalMs;
865
- const runAt = Date.now() + delayMs;
866
-
867
- await ctx.scheduler.runAt(runAt, internal.ragContentIndexer.triggerIndexingCheck, {});
868
-
869
- return { scheduledAt: runAt };
870
- },
871
- });
872
-
873
- /**
874
- * Internal mutation triggered by scheduler to check for pending events.
875
- *
876
- * This checks if there are pending events and signals that processing is needed.
877
- */
878
- export const triggerIndexingCheck = internalMutation({
879
- args: {},
880
- handler: async (ctx) => {
881
- // Check if there are any unprocessed indexing events
882
- const pendingEvent = await ctx.db
883
- .query("cmsEvents")
884
- .withIndex("by_processed", (q) => q.eq("processed", false))
885
- .filter((q) =>
886
- q.and(
887
- q.eq(q.field("resourceType"), "contentEntry"),
888
- q.or(
889
- q.eq(q.field("action"), "published"),
890
- q.eq(q.field("action"), "unpublished"),
891
- q.eq(q.field("action"), "deleted"),
892
- q.eq(q.field("action"), "restored")
893
- )
894
- )
895
- )
896
- .first();
897
-
898
- const hasPendingEvents = pendingEvent !== null;
899
-
900
- // Log for monitoring
901
- if (hasPendingEvents) {
902
- console.log("RAG Indexer: Pending events detected, processing needed");
903
- }
904
-
905
- return { hasPendingEvents };
906
- },
907
- });
908
-
909
- // =============================================================================
910
- // Utility Functions
911
- // =============================================================================
912
-
913
- /**
914
- * Prepares multiple entries for indexing in a single call.
915
- * Useful for batch operations.
916
- */
917
- export const prepareEntriesForIndexing = query({
918
- args: {
919
- entryIds: v.array(v.id("contentEntries")),
920
- options: v.optional(
921
- v.object({
922
- includeFields: v.optional(v.array(v.string())),
923
- excludeFields: v.optional(v.array(v.string())),
924
- maxCharsSoftLimit: v.optional(v.number()),
925
- namespacePrefix: v.optional(v.string()),
926
- })
927
- ),
928
- },
929
- returns: v.array(
930
- v.union(
931
- v.object({
932
- entryId: v.string(),
933
- chunks: v.array(
934
- v.object({
935
- text: v.string(),
936
- metadata: v.any(),
937
- })
938
- ),
939
- metadata: v.object({
940
- entryId: v.string(),
941
- contentType: v.string(),
942
- contentTypeDisplayName: v.string(),
943
- slug: v.string(),
944
- locale: v.optional(v.string()),
945
- version: v.number(),
946
- title: v.optional(v.string()),
947
- publishedAt: v.optional(v.number()),
948
- namespace: v.string(),
949
- }),
950
- }),
951
- v.null()
952
- )
953
- ),
954
- handler: async (ctx, args) => {
955
- const { entryIds, options = {} } = args;
956
- // Use explicit type for results to avoid typeof issues with registered queries
957
- type PrepareResult = {
958
- entryId: string;
959
- chunks: Array<{ text: string; metadata: unknown }>;
960
- metadata: {
961
- entryId: string;
962
- contentType: string;
963
- contentTypeDisplayName: string;
964
- slug: string;
965
- locale?: string;
966
- version: number;
967
- title?: string;
968
- publishedAt?: number;
969
- namespace: string;
970
- };
971
- } | null;
972
- const results: PrepareResult[] = [];
973
-
974
- // Cache content types to avoid repeated lookups
975
- const contentTypeCache = new Map<string, Doc<"contentTypes">>();
976
-
977
- for (const entryId of entryIds) {
978
- const entry = await ctx.db.get(entryId);
979
- if (!entry || entry.status !== "published") {
980
- results.push(null);
981
- continue;
982
- }
983
-
984
- let contentType = contentTypeCache.get(entry.contentTypeId);
985
- if (!contentType) {
986
- const fetchedContentType = await ctx.db.get(entry.contentTypeId);
987
- if (fetchedContentType) {
988
- contentType = fetchedContentType;
989
- contentTypeCache.set(entry.contentTypeId, fetchedContentType);
990
- }
991
- }
992
-
993
- if (!contentType) {
994
- results.push(null);
995
- continue;
996
- }
997
-
998
- // Build extraction options
999
- const extractionOptions: Partial<RagExtractionOptions> = {
1000
- includeMetadata: true,
1001
- includeFields: options.includeFields,
1002
- excludeFields: options.excludeFields,
1003
- chunkOptions: {
1004
- maxCharsSoftLimit: options.maxCharsSoftLimit ?? 1000,
1005
- },
1006
- };
1007
-
1008
- const entryInfo: ContentEntryInfo = {
1009
- _id: entry._id,
1010
- contentTypeId: entry.contentTypeId,
1011
- slug: entry.slug,
1012
- status: entry.status,
1013
- data: entry.data as Record<string, unknown>,
1014
- locale: entry.locale,
1015
- version: entry.version,
1016
- _creationTime: entry._creationTime,
1017
- firstPublishedAt: entry.firstPublishedAt,
1018
- lastPublishedAt: entry.lastPublishedAt,
1019
- };
1020
-
1021
- const contentTypeInfo: ContentTypeInfo = {
1022
- _id: contentType._id,
1023
- name: contentType.name,
1024
- displayName: contentType.displayName,
1025
- fields: contentType.fields as ContentTypeInfo["fields"],
1026
- titleField: contentType.titleField,
1027
- slugField: contentType.slugField,
1028
- };
1029
-
1030
- const chunks = chunkContentEntry(entryInfo, contentTypeInfo, extractionOptions);
1031
-
1032
- const namespacePrefix = options.namespacePrefix ?? "cms";
1033
- const namespace = entry.locale
1034
- ? `${namespacePrefix}:${contentType.name}:${entry.locale}`
1035
- : `${namespacePrefix}:${contentType.name}`;
1036
-
1037
- const title = chunks[0]?.metadata?.title || (entry.data as Record<string, unknown>)?.title as string | undefined;
1038
-
1039
- results.push({
1040
- entryId: entry._id,
1041
- chunks: chunks.map((c) => ({
1042
- text: c.text,
1043
- metadata: c.metadata,
1044
- })),
1045
- metadata: {
1046
- entryId: entry._id,
1047
- contentType: contentType.name,
1048
- contentTypeDisplayName: contentType.displayName,
1049
- slug: entry.slug,
1050
- locale: entry.locale,
1051
- version: entry.version,
1052
- title,
1053
- publishedAt: entry.lastPublishedAt,
1054
- namespace,
1055
- },
1056
- });
1057
- }
1058
-
1059
- return results;
1060
- },
1061
- });
1062
-
1063
- // =============================================================================
1064
- // Exports
1065
- // =============================================================================
1066
-
1067
- export { DEFAULT_CONFIG as DEFAULT_INDEXER_CONFIG };