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.
- package/LICENSE +190 -0
- package/README.md +99 -0
- package/admin-dist/nitro.json +15 -0
- package/admin-dist/public/assets/CmsEmptyState-CRswfTzk.js +5 -0
- package/admin-dist/public/assets/CmsPageHeader-CirpXndm.js +1 -0
- package/admin-dist/public/assets/CmsStatusBadge-CbEUpQu-.js +1 -0
- package/admin-dist/public/assets/CmsToolbar-BI2nZOXp.js +1 -0
- package/admin-dist/public/assets/ContentEntryEditor-CBeCyK_m.js +4 -0
- package/admin-dist/public/assets/ErrorState-BIVaWmom.js +1 -0
- package/admin-dist/public/assets/TaxonomyFilter-ChaY6Y_x.js +1 -0
- package/admin-dist/public/assets/_contentTypeId-DQ8k_Rvw.js +1 -0
- package/admin-dist/public/assets/_entryId-CKU_glsK.js +1 -0
- package/admin-dist/public/assets/alert-BXjTqrwQ.js +1 -0
- package/admin-dist/public/assets/badge-hvUOzpVZ.js +1 -0
- package/admin-dist/public/assets/circle-check-big-CF_pR17r.js +1 -0
- package/admin-dist/public/assets/command-DU82cJlt.js +1 -0
- package/admin-dist/public/assets/content-_LXl3pp7.js +1 -0
- package/admin-dist/public/assets/content-types-KjxaXGxY.js +2 -0
- package/admin-dist/public/assets/globals-CS6BZ0zp.css +1 -0
- package/admin-dist/public/assets/index-DNGIZHL-.js +1 -0
- package/admin-dist/public/assets/label-KNtpL71g.js +1 -0
- package/admin-dist/public/assets/link-2-Bw2aI4V4.js +1 -0
- package/admin-dist/public/assets/list-sYepHjt_.js +1 -0
- package/admin-dist/public/assets/main-CKj5yfEi.js +97 -0
- package/admin-dist/public/assets/media-Bkrkffm7.js +1 -0
- package/admin-dist/public/assets/new._contentTypeId-C3LstjNs.js +1 -0
- package/admin-dist/public/assets/plus-DUn8v_Xf.js +1 -0
- package/admin-dist/public/assets/rotate-ccw-DJEoHcRI.js +1 -0
- package/admin-dist/public/assets/scroll-area-DfIlT0in.js +1 -0
- package/admin-dist/public/assets/search-MuAUDJKR.js +1 -0
- package/admin-dist/public/assets/select-BD29IXCI.js +1 -0
- package/admin-dist/public/assets/settings-DmMyn_6A.js +1 -0
- package/admin-dist/public/assets/switch-h3Rrnl5i.js +1 -0
- package/admin-dist/public/assets/tabs-imc8h-Dp.js +1 -0
- package/admin-dist/public/assets/taxonomies-dAsrT65H.js +1 -0
- package/admin-dist/public/assets/textarea-BTy7nwzR.js +1 -0
- package/admin-dist/public/assets/trash-SAWKZZHv.js +1 -0
- package/admin-dist/public/assets/triangle-alert-E52Vfeuh.js +1 -0
- package/admin-dist/public/assets/useBreadcrumbLabel-BECBMCzM.js +1 -0
- package/admin-dist/public/assets/usePermissions-Basjs9BT.js +1 -0
- package/admin-dist/public/favicon.ico +0 -0
- package/admin-dist/server/_chunks/_libs/@date-fns/tz.mjs +217 -0
- package/admin-dist/server/_chunks/_libs/@floating-ui/core.mjs +719 -0
- package/admin-dist/server/_chunks/_libs/@floating-ui/dom.mjs +622 -0
- package/admin-dist/server/_chunks/_libs/@floating-ui/react-dom.mjs +292 -0
- package/admin-dist/server/_chunks/_libs/@floating-ui/utils.mjs +320 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/number.mjs +6 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/primitive.mjs +11 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-arrow.mjs +23 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-avatar.mjs +119 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-checkbox.mjs +270 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-collection.mjs +69 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-compose-refs.mjs +39 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-context.mjs +137 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-dialog.mjs +325 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-direction.mjs +9 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-dismissable-layer.mjs +210 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-dropdown-menu.mjs +253 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-guards.mjs +29 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-scope.mjs +206 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-id.mjs +14 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-label.mjs +23 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-menu.mjs +812 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-popover.mjs +300 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-popper.mjs +286 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-portal.mjs +16 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-presence.mjs +128 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-primitive.mjs +141 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-roving-focus.mjs +224 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-scroll-area.mjs +721 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-select.mjs +1163 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-separator.mjs +28 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-slot.mjs +601 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-switch.mjs +152 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-tabs.mjs +189 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-callback-ref.mjs +11 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-controllable-state.mjs +69 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-effect-event.mjs +1 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-escape-keydown.mjs +17 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-is-hydrated.mjs +15 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-layout-effect.mjs +6 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-previous.mjs +14 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-size.mjs +39 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-visually-hidden.mjs +33 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/history.mjs +409 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/react-router.mjs +1711 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/react-store.mjs +56 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/router-core.mjs +4829 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/store.mjs +134 -0
- package/admin-dist/server/_chunks/_libs/react-dom.mjs +10781 -0
- package/admin-dist/server/_chunks/_libs/react.mjs +513 -0
- package/admin-dist/server/_libs/aria-hidden.mjs +122 -0
- package/admin-dist/server/_libs/class-variance-authority.mjs +44 -0
- package/admin-dist/server/_libs/clsx.mjs +16 -0
- package/admin-dist/server/_libs/cmdk.mjs +315 -0
- package/admin-dist/server/_libs/convex.mjs +4841 -0
- package/admin-dist/server/_libs/cookie-es.mjs +58 -0
- package/admin-dist/server/_libs/croner.mjs +1 -0
- package/admin-dist/server/_libs/crossws.mjs +1 -0
- package/admin-dist/server/_libs/date-fns.mjs +1716 -0
- package/admin-dist/server/_libs/detect-node-es.mjs +1 -0
- package/admin-dist/server/_libs/get-nonce.mjs +9 -0
- package/admin-dist/server/_libs/h3-v2.mjs +277 -0
- package/admin-dist/server/_libs/h3.mjs +401 -0
- package/admin-dist/server/_libs/hookable.mjs +1 -0
- package/admin-dist/server/_libs/isbot.mjs +20 -0
- package/admin-dist/server/_libs/lucide-react.mjs +850 -0
- package/admin-dist/server/_libs/ohash.mjs +1 -0
- package/admin-dist/server/_libs/react-day-picker.mjs +2201 -0
- package/admin-dist/server/_libs/react-remove-scroll-bar.mjs +82 -0
- package/admin-dist/server/_libs/react-remove-scroll.mjs +328 -0
- package/admin-dist/server/_libs/react-style-singleton.mjs +69 -0
- package/admin-dist/server/_libs/rou3.mjs +8 -0
- package/admin-dist/server/_libs/seroval-plugins.mjs +58 -0
- package/admin-dist/server/_libs/seroval.mjs +1765 -0
- package/admin-dist/server/_libs/srvx.mjs +719 -0
- package/admin-dist/server/_libs/tailwind-merge.mjs +3010 -0
- package/admin-dist/server/_libs/tiny-invariant.mjs +12 -0
- package/admin-dist/server/_libs/tiny-warning.mjs +5 -0
- package/admin-dist/server/_libs/tslib.mjs +39 -0
- package/admin-dist/server/_libs/ufo.mjs +54 -0
- package/admin-dist/server/_libs/unctx.mjs +1 -0
- package/admin-dist/server/_libs/unstorage.mjs +1 -0
- package/admin-dist/server/_libs/use-callback-ref.mjs +66 -0
- package/admin-dist/server/_libs/use-sidecar.mjs +106 -0
- package/admin-dist/server/_libs/use-sync-external-store.mjs +139 -0
- package/admin-dist/server/_libs/zod.mjs +4223 -0
- package/admin-dist/server/_ssr/CmsEmptyState-DU7-7-mV.mjs +290 -0
- package/admin-dist/server/_ssr/CmsPageHeader-CseW0AHm.mjs +24 -0
- package/admin-dist/server/_ssr/CmsStatusBadge-B_pi4KCp.mjs +127 -0
- package/admin-dist/server/_ssr/CmsToolbar-X75ex6ek.mjs +49 -0
- package/admin-dist/server/_ssr/ContentEntryEditor-CepusRsA.mjs +3720 -0
- package/admin-dist/server/_ssr/ErrorState-cI-bKLez.mjs +89 -0
- package/admin-dist/server/_ssr/TaxonomyFilter-Bwrq0-cz.mjs +188 -0
- package/admin-dist/server/_ssr/_contentTypeId-BqYKEcLr.mjs +379 -0
- package/admin-dist/server/_ssr/_entryId-CRfnqeDf.mjs +161 -0
- package/admin-dist/server/_ssr/_tanstack-start-manifest_v-BwDlABVk.mjs +4 -0
- package/admin-dist/server/_ssr/alert-CVt45UUP.mjs +92 -0
- package/admin-dist/server/_ssr/badge-6BsP37vG.mjs +125 -0
- package/admin-dist/server/_ssr/command-fy8epIKf.mjs +128 -0
- package/admin-dist/server/_ssr/config.server-D7JHDcDv.mjs +117 -0
- package/admin-dist/server/_ssr/content-B5RhL7uW.mjs +532 -0
- package/admin-dist/server/_ssr/content-types-BIOqCQYN.mjs +1166 -0
- package/admin-dist/server/_ssr/index-DHSHDPt1.mjs +193 -0
- package/admin-dist/server/_ssr/index.mjs +1275 -0
- package/admin-dist/server/_ssr/label-C8Dko1j7.mjs +22 -0
- package/admin-dist/server/_ssr/media-CSx3XttC.mjs +1832 -0
- package/admin-dist/server/_ssr/new._contentTypeId-DzanEZQM.mjs +144 -0
- package/admin-dist/server/_ssr/router-DDWcF-kt.mjs +1556 -0
- package/admin-dist/server/_ssr/scroll-area-bjPYwhXN.mjs +59 -0
- package/admin-dist/server/_ssr/select-BUhDDf4T.mjs +142 -0
- package/admin-dist/server/_ssr/settings-DAsxnw2q.mjs +348 -0
- package/admin-dist/server/_ssr/start-HYkvq4Ni.mjs +4 -0
- package/admin-dist/server/_ssr/switch-BgyRtQ1Z.mjs +31 -0
- package/admin-dist/server/_ssr/tabs-DzMdRB1A.mjs +628 -0
- package/admin-dist/server/_ssr/taxonomies-C8j8g5Q5.mjs +915 -0
- package/admin-dist/server/_ssr/textarea-9jNeYJSc.mjs +18 -0
- package/admin-dist/server/_ssr/trash-DYMxwhZB.mjs +291 -0
- package/admin-dist/server/_ssr/useBreadcrumbLabel-FNSAr2Ha.mjs +16 -0
- package/admin-dist/server/_ssr/usePermissions-BJGGahrJ.mjs +68 -0
- package/admin-dist/server/favicon.ico +0 -0
- package/admin-dist/server/index.mjs +627 -0
- package/dist/cli/index.js +0 -0
- package/dist/client/admin-config.d.ts +0 -1
- package/dist/client/admin-config.d.ts.map +1 -1
- package/dist/client/admin-config.js +0 -1
- package/dist/client/admin-config.js.map +1 -1
- package/dist/client/adminApi.d.ts.map +1 -1
- package/dist/client/agentTools.d.ts +1237 -135
- package/dist/client/agentTools.d.ts.map +1 -1
- package/dist/client/agentTools.js +33 -9
- package/dist/client/agentTools.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +9 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/mediaAssets.d.ts +35 -0
- package/dist/component/mediaAssets.d.ts.map +1 -1
- package/dist/component/mediaAssets.js +81 -0
- package/dist/component/mediaAssets.js.map +1 -1
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +2 -1
- package/dist/test.js.map +1 -1
- package/package.json +24 -9
- package/dist/component/auditLog.d.ts +0 -410
- package/dist/component/auditLog.d.ts.map +0 -1
- package/dist/component/auditLog.js +0 -607
- package/dist/component/auditLog.js.map +0 -1
- package/dist/component/types.d.ts +0 -4
- package/dist/component/types.d.ts.map +0 -1
- package/dist/component/types.js +0 -2
- package/dist/component/types.js.map +0 -1
- package/src/cli/commands/admin.ts +0 -104
- package/src/cli/index.ts +0 -21
- package/src/cli/utils/detectConvexUrl.ts +0 -54
- package/src/cli/utils/openBrowser.ts +0 -16
- package/src/client/admin-config.ts +0 -138
- package/src/client/adminApi.ts +0 -942
- package/src/client/agentTools.ts +0 -1311
- package/src/client/argTypes.ts +0 -316
- package/src/client/field-types.ts +0 -187
- package/src/client/index.ts +0 -1301
- package/src/client/queryBuilder.ts +0 -1100
- package/src/client/schema/codegen.ts +0 -500
- package/src/client/schema/defineContentType.ts +0 -501
- package/src/client/schema/index.ts +0 -169
- package/src/client/schema/schemaDrift.ts +0 -574
- package/src/client/schema/typedClient.ts +0 -688
- package/src/client/schema/types.ts +0 -666
- package/src/client/types.ts +0 -723
- package/src/client/workflows.ts +0 -141
- package/src/client/wrapper.ts +0 -4304
- package/src/component/_generated/api.ts +0 -140
- package/src/component/_generated/component.ts +0 -5029
- package/src/component/_generated/dataModel.ts +0 -60
- package/src/component/_generated/server.ts +0 -156
- package/src/component/authorization.ts +0 -647
- package/src/component/authorizationHooks.ts +0 -668
- package/src/component/bulkOperations.ts +0 -687
- package/src/component/contentEntries.ts +0 -1976
- package/src/component/contentEntryMutations.ts +0 -1223
- package/src/component/contentEntryValidation.ts +0 -707
- package/src/component/contentLock.ts +0 -550
- package/src/component/contentTypeMigration.ts +0 -1064
- package/src/component/contentTypeMutations.ts +0 -969
- package/src/component/contentTypes.ts +0 -346
- package/src/component/convex.config.ts +0 -44
- package/src/component/documentTypes.ts +0 -240
- package/src/component/eventEmitter.ts +0 -485
- package/src/component/exportImport.ts +0 -1169
- package/src/component/index.ts +0 -491
- package/src/component/lib/deepReferenceResolver.ts +0 -999
- package/src/component/lib/errors.ts +0 -816
- package/src/component/lib/index.ts +0 -145
- package/src/component/lib/mediaReferenceResolver.ts +0 -495
- package/src/component/lib/metadataExtractor.ts +0 -792
- package/src/component/lib/mutationAuth.ts +0 -199
- package/src/component/lib/queries.ts +0 -79
- package/src/component/lib/ragContentChunker.ts +0 -1371
- package/src/component/lib/referenceResolver.ts +0 -430
- package/src/component/lib/slugGenerator.ts +0 -262
- package/src/component/lib/slugUniqueness.ts +0 -333
- package/src/component/lib/softDelete.ts +0 -44
- package/src/component/localeFallbackChain.ts +0 -673
- package/src/component/localeFields.ts +0 -896
- package/src/component/mediaAssetMutations.ts +0 -725
- package/src/component/mediaAssets.ts +0 -932
- package/src/component/mediaFolderMutations.ts +0 -1046
- package/src/component/mediaUploadMutations.ts +0 -224
- package/src/component/mediaVariantMutations.ts +0 -900
- package/src/component/mediaVariants.ts +0 -793
- package/src/component/ragContentIndexer.ts +0 -1067
- package/src/component/rateLimitHooks.ts +0 -572
- package/src/component/roles.ts +0 -1360
- package/src/component/scheduledPublish.ts +0 -358
- package/src/component/schema.ts +0 -617
- package/src/component/taxonomies.ts +0 -949
- package/src/component/taxonomyMutations.ts +0 -1210
- package/src/component/trash.ts +0 -724
- package/src/component/userContext.ts +0 -898
- package/src/component/validation.ts +0 -1388
- package/src/component/validators.ts +0 -949
- package/src/component/versionMutations.ts +0 -392
- package/src/component/webhookTrigger.ts +0 -1922
- package/src/react/index.ts +0 -898
- 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 };
|