convex-cms 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/admin.d.ts +16 -0
- package/dist/cli/commands/admin.d.ts.map +1 -0
- package/dist/cli/commands/admin.js +88 -0
- package/dist/cli/commands/admin.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +18 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/utils/detectConvexUrl.d.ts +13 -0
- package/dist/cli/utils/detectConvexUrl.d.ts.map +1 -0
- package/dist/cli/utils/detectConvexUrl.js +48 -0
- package/dist/cli/utils/detectConvexUrl.js.map +1 -0
- package/dist/cli/utils/openBrowser.d.ts +7 -0
- package/dist/cli/utils/openBrowser.d.ts.map +1 -0
- package/dist/cli/utils/openBrowser.js +17 -0
- package/dist/cli/utils/openBrowser.js.map +1 -0
- package/dist/client/admin-config.d.ts +126 -0
- package/dist/client/admin-config.d.ts.map +1 -0
- package/dist/client/admin-config.js +117 -0
- package/dist/client/admin-config.js.map +1 -0
- package/dist/client/adminApi.d.ts +2273 -0
- package/dist/client/adminApi.d.ts.map +1 -0
- package/dist/client/adminApi.js +716 -0
- package/dist/client/adminApi.js.map +1 -0
- package/dist/client/agentTools.d.ts +933 -0
- package/dist/client/agentTools.d.ts.map +1 -0
- package/dist/client/agentTools.js +1004 -0
- package/dist/client/agentTools.js.map +1 -0
- package/dist/client/argTypes.d.ts +212 -0
- package/dist/client/argTypes.d.ts.map +1 -0
- package/dist/client/argTypes.js +5 -0
- package/dist/client/argTypes.js.map +1 -0
- package/dist/client/field-types.d.ts +55 -0
- package/dist/client/field-types.d.ts.map +1 -0
- package/dist/client/field-types.js +152 -0
- package/dist/client/field-types.js.map +1 -0
- package/dist/client/index.d.ts +189 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +668 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/queryBuilder.d.ts +765 -0
- package/dist/client/queryBuilder.d.ts.map +1 -0
- package/dist/client/queryBuilder.js +970 -0
- package/dist/client/queryBuilder.js.map +1 -0
- package/dist/client/schema/codegen.d.ts +128 -0
- package/dist/client/schema/codegen.d.ts.map +1 -0
- package/dist/client/schema/codegen.js +318 -0
- package/dist/client/schema/codegen.js.map +1 -0
- package/dist/client/schema/defineContentType.d.ts +221 -0
- package/dist/client/schema/defineContentType.d.ts.map +1 -0
- package/dist/client/schema/defineContentType.js +380 -0
- package/dist/client/schema/defineContentType.js.map +1 -0
- package/dist/client/schema/index.d.ts +85 -0
- package/dist/client/schema/index.d.ts.map +1 -0
- package/dist/client/schema/index.js +92 -0
- package/dist/client/schema/index.js.map +1 -0
- package/dist/client/schema/schemaDrift.d.ts +199 -0
- package/dist/client/schema/schemaDrift.d.ts.map +1 -0
- package/dist/client/schema/schemaDrift.js +340 -0
- package/dist/client/schema/schemaDrift.js.map +1 -0
- package/dist/client/schema/typedClient.d.ts +401 -0
- package/dist/client/schema/typedClient.d.ts.map +1 -0
- package/dist/client/schema/typedClient.js +269 -0
- package/dist/client/schema/typedClient.js.map +1 -0
- package/dist/client/schema/types.d.ts +477 -0
- package/dist/client/schema/types.d.ts.map +1 -0
- package/dist/client/schema/types.js +39 -0
- package/dist/client/schema/types.js.map +1 -0
- package/dist/client/types.d.ts +449 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +149 -0
- package/dist/client/types.js.map +1 -0
- package/dist/client/workflows.d.ts +51 -0
- package/dist/client/workflows.d.ts.map +1 -0
- package/dist/client/workflows.js +103 -0
- package/dist/client/workflows.js.map +1 -0
- package/dist/client/wrapper.d.ts +2198 -0
- package/dist/client/wrapper.d.ts.map +1 -0
- package/dist/client/wrapper.js +2651 -0
- package/dist/client/wrapper.js.map +1 -0
- package/dist/component/_generated/api.d.ts +124 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +4321 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/auditLog.d.ts +410 -0
- package/dist/component/auditLog.d.ts.map +1 -0
- package/dist/component/auditLog.js +607 -0
- package/dist/component/auditLog.js.map +1 -0
- package/dist/component/authorization.d.ts +323 -0
- package/dist/component/authorization.d.ts.map +1 -0
- package/dist/component/authorization.js +464 -0
- package/dist/component/authorization.js.map +1 -0
- package/dist/component/authorizationHooks.d.ts +184 -0
- package/dist/component/authorizationHooks.d.ts.map +1 -0
- package/dist/component/authorizationHooks.js +521 -0
- package/dist/component/authorizationHooks.js.map +1 -0
- package/dist/component/bulkOperations.d.ts +200 -0
- package/dist/component/bulkOperations.d.ts.map +1 -0
- package/dist/component/bulkOperations.js +568 -0
- package/dist/component/bulkOperations.js.map +1 -0
- package/dist/component/contentEntries.d.ts +719 -0
- package/dist/component/contentEntries.d.ts.map +1 -0
- package/dist/component/contentEntries.js +1617 -0
- package/dist/component/contentEntries.js.map +1 -0
- package/dist/component/contentEntryMutations.d.ts +505 -0
- package/dist/component/contentEntryMutations.d.ts.map +1 -0
- package/dist/component/contentEntryMutations.js +1009 -0
- package/dist/component/contentEntryMutations.js.map +1 -0
- package/dist/component/contentEntryValidation.d.ts +115 -0
- package/dist/component/contentEntryValidation.d.ts.map +1 -0
- package/dist/component/contentEntryValidation.js +546 -0
- package/dist/component/contentEntryValidation.js.map +1 -0
- package/dist/component/contentLock.d.ts +328 -0
- package/dist/component/contentLock.d.ts.map +1 -0
- package/dist/component/contentLock.js +471 -0
- package/dist/component/contentLock.js.map +1 -0
- package/dist/component/contentTypeMigration.d.ts +411 -0
- package/dist/component/contentTypeMigration.d.ts.map +1 -0
- package/dist/component/contentTypeMigration.js +805 -0
- package/dist/component/contentTypeMigration.js.map +1 -0
- package/dist/component/contentTypeMutations.d.ts +975 -0
- package/dist/component/contentTypeMutations.d.ts.map +1 -0
- package/dist/component/contentTypeMutations.js +768 -0
- package/dist/component/contentTypeMutations.js.map +1 -0
- package/dist/component/contentTypes.d.ts +538 -0
- package/dist/component/contentTypes.d.ts.map +1 -0
- package/dist/component/contentTypes.js +304 -0
- package/dist/component/contentTypes.js.map +1 -0
- package/dist/component/convex.config.d.ts +42 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +43 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/documentTypes.d.ts +186 -0
- package/dist/component/documentTypes.d.ts.map +1 -0
- package/dist/component/documentTypes.js +23 -0
- package/dist/component/documentTypes.js.map +1 -0
- package/dist/component/eventEmitter.d.ts +281 -0
- package/dist/component/eventEmitter.d.ts.map +1 -0
- package/dist/component/eventEmitter.js +300 -0
- package/dist/component/eventEmitter.js.map +1 -0
- package/dist/component/exportImport.d.ts +1120 -0
- package/dist/component/exportImport.d.ts.map +1 -0
- package/dist/component/exportImport.js +931 -0
- package/dist/component/exportImport.js.map +1 -0
- package/dist/component/index.d.ts +28 -0
- package/dist/component/index.d.ts.map +1 -0
- package/dist/component/index.js +142 -0
- package/dist/component/index.js.map +1 -0
- package/dist/component/lib/deepReferenceResolver.d.ts +252 -0
- package/dist/component/lib/deepReferenceResolver.d.ts.map +1 -0
- package/dist/component/lib/deepReferenceResolver.js +601 -0
- package/dist/component/lib/deepReferenceResolver.js.map +1 -0
- package/dist/component/lib/errors.d.ts +306 -0
- package/dist/component/lib/errors.d.ts.map +1 -0
- package/dist/component/lib/errors.js +407 -0
- package/dist/component/lib/errors.js.map +1 -0
- package/dist/component/lib/index.d.ts +10 -0
- package/dist/component/lib/index.d.ts.map +1 -0
- package/dist/component/lib/index.js +33 -0
- package/dist/component/lib/index.js.map +1 -0
- package/dist/component/lib/mediaReferenceResolver.d.ts +217 -0
- package/dist/component/lib/mediaReferenceResolver.d.ts.map +1 -0
- package/dist/component/lib/mediaReferenceResolver.js +326 -0
- package/dist/component/lib/mediaReferenceResolver.js.map +1 -0
- package/dist/component/lib/metadataExtractor.d.ts +245 -0
- package/dist/component/lib/metadataExtractor.d.ts.map +1 -0
- package/dist/component/lib/metadataExtractor.js +548 -0
- package/dist/component/lib/metadataExtractor.js.map +1 -0
- package/dist/component/lib/mutationAuth.d.ts +95 -0
- package/dist/component/lib/mutationAuth.d.ts.map +1 -0
- package/dist/component/lib/mutationAuth.js +146 -0
- package/dist/component/lib/mutationAuth.js.map +1 -0
- package/dist/component/lib/queries.d.ts +17 -0
- package/dist/component/lib/queries.d.ts.map +1 -0
- package/dist/component/lib/queries.js +49 -0
- package/dist/component/lib/queries.js.map +1 -0
- package/dist/component/lib/ragContentChunker.d.ts +423 -0
- package/dist/component/lib/ragContentChunker.d.ts.map +1 -0
- package/dist/component/lib/ragContentChunker.js +897 -0
- package/dist/component/lib/ragContentChunker.js.map +1 -0
- package/dist/component/lib/referenceResolver.d.ts +175 -0
- package/dist/component/lib/referenceResolver.d.ts.map +1 -0
- package/dist/component/lib/referenceResolver.js +293 -0
- package/dist/component/lib/referenceResolver.js.map +1 -0
- package/dist/component/lib/slugGenerator.d.ts +71 -0
- package/dist/component/lib/slugGenerator.d.ts.map +1 -0
- package/dist/component/lib/slugGenerator.js +207 -0
- package/dist/component/lib/slugGenerator.js.map +1 -0
- package/dist/component/lib/slugUniqueness.d.ts +131 -0
- package/dist/component/lib/slugUniqueness.d.ts.map +1 -0
- package/dist/component/lib/slugUniqueness.js +229 -0
- package/dist/component/lib/slugUniqueness.js.map +1 -0
- package/dist/component/lib/softDelete.d.ts +18 -0
- package/dist/component/lib/softDelete.d.ts.map +1 -0
- package/dist/component/lib/softDelete.js +29 -0
- package/dist/component/lib/softDelete.js.map +1 -0
- package/dist/component/localeFallbackChain.d.ts +410 -0
- package/dist/component/localeFallbackChain.d.ts.map +1 -0
- package/dist/component/localeFallbackChain.js +467 -0
- package/dist/component/localeFallbackChain.js.map +1 -0
- package/dist/component/localeFields.d.ts +508 -0
- package/dist/component/localeFields.d.ts.map +1 -0
- package/dist/component/localeFields.js +592 -0
- package/dist/component/localeFields.js.map +1 -0
- package/dist/component/mediaAssetMutations.d.ts +235 -0
- package/dist/component/mediaAssetMutations.d.ts.map +1 -0
- package/dist/component/mediaAssetMutations.js +558 -0
- package/dist/component/mediaAssetMutations.js.map +1 -0
- package/dist/component/mediaAssets.d.ts +168 -0
- package/dist/component/mediaAssets.d.ts.map +1 -0
- package/dist/component/mediaAssets.js +618 -0
- package/dist/component/mediaAssets.js.map +1 -0
- package/dist/component/mediaFolderMutations.d.ts +642 -0
- package/dist/component/mediaFolderMutations.d.ts.map +1 -0
- package/dist/component/mediaFolderMutations.js +849 -0
- package/dist/component/mediaFolderMutations.js.map +1 -0
- package/dist/component/mediaUploadMutations.d.ts +136 -0
- package/dist/component/mediaUploadMutations.d.ts.map +1 -0
- package/dist/component/mediaUploadMutations.js +205 -0
- package/dist/component/mediaUploadMutations.js.map +1 -0
- package/dist/component/mediaVariantMutations.d.ts +468 -0
- package/dist/component/mediaVariantMutations.d.ts.map +1 -0
- package/dist/component/mediaVariantMutations.js +737 -0
- package/dist/component/mediaVariantMutations.js.map +1 -0
- package/dist/component/mediaVariants.d.ts +525 -0
- package/dist/component/mediaVariants.d.ts.map +1 -0
- package/dist/component/mediaVariants.js +661 -0
- package/dist/component/mediaVariants.js.map +1 -0
- package/dist/component/ragContentIndexer.d.ts +595 -0
- package/dist/component/ragContentIndexer.d.ts.map +1 -0
- package/dist/component/ragContentIndexer.js +794 -0
- package/dist/component/ragContentIndexer.js.map +1 -0
- package/dist/component/rateLimitHooks.d.ts +266 -0
- package/dist/component/rateLimitHooks.d.ts.map +1 -0
- package/dist/component/rateLimitHooks.js +412 -0
- package/dist/component/rateLimitHooks.js.map +1 -0
- package/dist/component/roles.d.ts +649 -0
- package/dist/component/roles.d.ts.map +1 -0
- package/dist/component/roles.js +884 -0
- package/dist/component/roles.js.map +1 -0
- package/dist/component/scheduledPublish.d.ts +182 -0
- package/dist/component/scheduledPublish.d.ts.map +1 -0
- package/dist/component/scheduledPublish.js +304 -0
- package/dist/component/scheduledPublish.js.map +1 -0
- package/dist/component/schema.d.ts +4114 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +469 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/taxonomies.d.ts +476 -0
- package/dist/component/taxonomies.d.ts.map +1 -0
- package/dist/component/taxonomies.js +785 -0
- package/dist/component/taxonomies.js.map +1 -0
- package/dist/component/taxonomyMutations.d.ts +206 -0
- package/dist/component/taxonomyMutations.d.ts.map +1 -0
- package/dist/component/taxonomyMutations.js +1001 -0
- package/dist/component/taxonomyMutations.js.map +1 -0
- package/dist/component/trash.d.ts +265 -0
- package/dist/component/trash.d.ts.map +1 -0
- package/dist/component/trash.js +621 -0
- package/dist/component/trash.js.map +1 -0
- package/dist/component/types.d.ts +4 -0
- package/dist/component/types.d.ts.map +1 -0
- package/dist/component/types.js +2 -0
- package/dist/component/types.js.map +1 -0
- package/dist/component/userContext.d.ts +508 -0
- package/dist/component/userContext.d.ts.map +1 -0
- package/dist/component/userContext.js +615 -0
- package/dist/component/userContext.js.map +1 -0
- package/dist/component/validation.d.ts +387 -0
- package/dist/component/validation.d.ts.map +1 -0
- package/dist/component/validation.js +1052 -0
- package/dist/component/validation.js.map +1 -0
- package/dist/component/validators.d.ts +4645 -0
- package/dist/component/validators.d.ts.map +1 -0
- package/dist/component/validators.js +641 -0
- package/dist/component/validators.js.map +1 -0
- package/dist/component/versionMutations.d.ts +216 -0
- package/dist/component/versionMutations.d.ts.map +1 -0
- package/dist/component/versionMutations.js +321 -0
- package/dist/component/versionMutations.js.map +1 -0
- package/dist/component/webhookTrigger.d.ts +770 -0
- package/dist/component/webhookTrigger.d.ts.map +1 -0
- package/dist/component/webhookTrigger.js +1413 -0
- package/dist/component/webhookTrigger.js.map +1 -0
- package/dist/react/index.d.ts +316 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +558 -0
- package/dist/react/index.js.map +1 -0
- package/dist/test.d.ts +2230 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +1107 -0
- package/dist/test.js.map +1 -0
- package/package.json +95 -0
- package/src/cli/commands/admin.ts +104 -0
- package/src/cli/index.ts +21 -0
- package/src/cli/utils/detectConvexUrl.ts +54 -0
- package/src/cli/utils/openBrowser.ts +16 -0
- package/src/client/admin-config.ts +138 -0
- package/src/client/adminApi.ts +942 -0
- package/src/client/agentTools.ts +1311 -0
- package/src/client/argTypes.ts +316 -0
- package/src/client/field-types.ts +187 -0
- package/src/client/index.ts +1301 -0
- package/src/client/queryBuilder.ts +1100 -0
- package/src/client/schema/codegen.ts +500 -0
- package/src/client/schema/defineContentType.ts +501 -0
- package/src/client/schema/index.ts +169 -0
- package/src/client/schema/schemaDrift.ts +574 -0
- package/src/client/schema/typedClient.ts +688 -0
- package/src/client/schema/types.ts +666 -0
- package/src/client/types.ts +723 -0
- package/src/client/workflows.ts +141 -0
- package/src/client/wrapper.ts +4304 -0
- package/src/component/_generated/api.ts +140 -0
- package/src/component/_generated/component.ts +5029 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/authorization.ts +647 -0
- package/src/component/authorizationHooks.ts +668 -0
- package/src/component/bulkOperations.ts +687 -0
- package/src/component/contentEntries.ts +1976 -0
- package/src/component/contentEntryMutations.ts +1223 -0
- package/src/component/contentEntryValidation.ts +707 -0
- package/src/component/contentLock.ts +550 -0
- package/src/component/contentTypeMigration.ts +1064 -0
- package/src/component/contentTypeMutations.ts +969 -0
- package/src/component/contentTypes.ts +346 -0
- package/src/component/convex.config.ts +44 -0
- package/src/component/documentTypes.ts +240 -0
- package/src/component/eventEmitter.ts +485 -0
- package/src/component/exportImport.ts +1169 -0
- package/src/component/index.ts +491 -0
- package/src/component/lib/deepReferenceResolver.ts +999 -0
- package/src/component/lib/errors.ts +816 -0
- package/src/component/lib/index.ts +145 -0
- package/src/component/lib/mediaReferenceResolver.ts +495 -0
- package/src/component/lib/metadataExtractor.ts +792 -0
- package/src/component/lib/mutationAuth.ts +199 -0
- package/src/component/lib/queries.ts +79 -0
- package/src/component/lib/ragContentChunker.ts +1371 -0
- package/src/component/lib/referenceResolver.ts +430 -0
- package/src/component/lib/slugGenerator.ts +262 -0
- package/src/component/lib/slugUniqueness.ts +333 -0
- package/src/component/lib/softDelete.ts +44 -0
- package/src/component/localeFallbackChain.ts +673 -0
- package/src/component/localeFields.ts +896 -0
- package/src/component/mediaAssetMutations.ts +725 -0
- package/src/component/mediaAssets.ts +932 -0
- package/src/component/mediaFolderMutations.ts +1046 -0
- package/src/component/mediaUploadMutations.ts +224 -0
- package/src/component/mediaVariantMutations.ts +900 -0
- package/src/component/mediaVariants.ts +793 -0
- package/src/component/ragContentIndexer.ts +1067 -0
- package/src/component/rateLimitHooks.ts +572 -0
- package/src/component/roles.ts +1360 -0
- package/src/component/scheduledPublish.ts +358 -0
- package/src/component/schema.ts +617 -0
- package/src/component/taxonomies.ts +949 -0
- package/src/component/taxonomyMutations.ts +1210 -0
- package/src/component/trash.ts +724 -0
- package/src/component/userContext.ts +898 -0
- package/src/component/validation.ts +1388 -0
- package/src/component/validators.ts +949 -0
- package/src/component/versionMutations.ts +392 -0
- package/src/component/webhookTrigger.ts +1922 -0
- package/src/react/index.ts +898 -0
- package/src/test.ts +1580 -0
|
@@ -0,0 +1,999 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep Reference Resolution Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides functions for recursively resolving content and media references
|
|
5
|
+
* within content entries. Supports depth limiting and circular reference
|
|
6
|
+
* prevention to avoid infinite loops.
|
|
7
|
+
*
|
|
8
|
+
* This module extends the basic reference resolution with:
|
|
9
|
+
* - Recursive resolution of nested references
|
|
10
|
+
* - Configurable maximum depth
|
|
11
|
+
* - Circular reference detection and prevention
|
|
12
|
+
* - Combined content and media reference resolution
|
|
13
|
+
* - Selective field resolution
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* // Resolve a blog post with author and related posts
|
|
18
|
+
* const resolvedEntry = await resolveEntryReferences(ctx, entry, contentType.fields, {
|
|
19
|
+
* maxDepth: 2,
|
|
20
|
+
* resolveMedia: true,
|
|
21
|
+
* publishedOnly: true,
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // The resolved entry will have:
|
|
25
|
+
* // - entry.data.author resolved to full author entry
|
|
26
|
+
* // - entry.data.author.data.profileImage resolved to media URL
|
|
27
|
+
* // - entry.data.relatedPosts resolved to array of entries (depth 1)
|
|
28
|
+
* // - entry.data.relatedPosts[].author NOT resolved (depth limit reached)
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
// Doc,
|
|
34
|
+
Id,
|
|
35
|
+
} from "../_generated/dataModel.js";
|
|
36
|
+
import { QueryCtx } from "../_generated/server.js";
|
|
37
|
+
import { isDeleted } from "./softDelete.js";
|
|
38
|
+
import {
|
|
39
|
+
resolveReference,
|
|
40
|
+
// resolveReferences,
|
|
41
|
+
// ResolvedReference,
|
|
42
|
+
// ResolveOptions,
|
|
43
|
+
} from "./referenceResolver.js";
|
|
44
|
+
import {
|
|
45
|
+
resolveMediaReference,
|
|
46
|
+
// resolveMediaReferences,
|
|
47
|
+
ResolvedMediaReference,
|
|
48
|
+
MediaResolveOptions,
|
|
49
|
+
} from "./mediaReferenceResolver.js";
|
|
50
|
+
|
|
51
|
+
// =============================================================================
|
|
52
|
+
// Types
|
|
53
|
+
// =============================================================================
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Field definition subset needed for reference resolution.
|
|
57
|
+
* This type matches the fields array in content types.
|
|
58
|
+
*/
|
|
59
|
+
export interface FieldDefinitionForResolver {
|
|
60
|
+
/** Field name in the data object */
|
|
61
|
+
name: string;
|
|
62
|
+
/** Field type identifier */
|
|
63
|
+
type: string;
|
|
64
|
+
/** Field-specific options */
|
|
65
|
+
options?: {
|
|
66
|
+
/** For reference fields: allowed content type names */
|
|
67
|
+
allowedContentTypes?: string[];
|
|
68
|
+
/** For reference/media fields: whether multiple values are allowed */
|
|
69
|
+
multiple?: boolean;
|
|
70
|
+
/** For media fields: allowed MIME types */
|
|
71
|
+
allowedMimeTypes?: string[];
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Options for deep reference resolution.
|
|
77
|
+
*/
|
|
78
|
+
export interface DeepResolveOptions {
|
|
79
|
+
/**
|
|
80
|
+
* Maximum depth to resolve nested references.
|
|
81
|
+
* - 0: Don't resolve any references (just return IDs)
|
|
82
|
+
* - 1: Resolve immediate references only
|
|
83
|
+
* - 2: Resolve references and their references
|
|
84
|
+
* - etc.
|
|
85
|
+
*
|
|
86
|
+
* @default 1
|
|
87
|
+
*/
|
|
88
|
+
maxDepth?: number;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Whether to resolve media references.
|
|
92
|
+
* When true, media IDs are replaced with full asset data including URLs.
|
|
93
|
+
*
|
|
94
|
+
* @default true
|
|
95
|
+
*/
|
|
96
|
+
resolveMedia?: boolean;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Whether to resolve content references.
|
|
100
|
+
* When true, content entry IDs are replaced with full entry data.
|
|
101
|
+
*
|
|
102
|
+
* @default true
|
|
103
|
+
*/
|
|
104
|
+
resolveContent?: boolean;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Only resolve references to published entries.
|
|
108
|
+
* Useful for frontend/public API usage.
|
|
109
|
+
*
|
|
110
|
+
* @default false
|
|
111
|
+
*/
|
|
112
|
+
publishedOnly?: boolean;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Include soft-deleted entries when resolving.
|
|
116
|
+
*
|
|
117
|
+
* @default false
|
|
118
|
+
*/
|
|
119
|
+
includeDeleted?: boolean;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Specific fields to include from resolved entries.
|
|
123
|
+
* If not specified, all fields are included.
|
|
124
|
+
* Only applies to content references.
|
|
125
|
+
*/
|
|
126
|
+
fields?: string[];
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Specific field names to resolve references for.
|
|
130
|
+
* If not specified, all reference/media fields are resolved.
|
|
131
|
+
* Useful for selective resolution of expensive operations.
|
|
132
|
+
*/
|
|
133
|
+
onlyFields?: string[];
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Field names to skip when resolving references.
|
|
137
|
+
* Useful for excluding specific fields from resolution.
|
|
138
|
+
*/
|
|
139
|
+
excludeFields?: string[];
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Whether to preserve the original reference ID alongside resolved data.
|
|
143
|
+
* When true, resolved objects include an `_originalId` field.
|
|
144
|
+
*
|
|
145
|
+
* @default false
|
|
146
|
+
*/
|
|
147
|
+
preserveOriginalIds?: boolean;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* A content entry with resolved references.
|
|
152
|
+
* The data object will have reference fields replaced with resolved content.
|
|
153
|
+
*/
|
|
154
|
+
export interface ResolvedContentEntry {
|
|
155
|
+
/** The content entry ID */
|
|
156
|
+
id: string;
|
|
157
|
+
/** The content type name */
|
|
158
|
+
contentTypeName: string;
|
|
159
|
+
/** The content type display name */
|
|
160
|
+
contentTypeDisplayName: string;
|
|
161
|
+
/** The entry's URL slug */
|
|
162
|
+
slug: string;
|
|
163
|
+
/** The entry's publishing status (supports custom workflow states) */
|
|
164
|
+
status: string;
|
|
165
|
+
/** The entry's data with resolved references */
|
|
166
|
+
data: Record<string, unknown>;
|
|
167
|
+
/** Whether the entry exists */
|
|
168
|
+
exists: boolean;
|
|
169
|
+
/** Locale code if localized */
|
|
170
|
+
locale?: string;
|
|
171
|
+
/** Version number */
|
|
172
|
+
version?: number;
|
|
173
|
+
/** Fields that had circular references (were not resolved) */
|
|
174
|
+
_circularReferences?: string[];
|
|
175
|
+
/** Fields that had unresolved references (not found) */
|
|
176
|
+
_unresolvedReferences?: Record<string, string[]>;
|
|
177
|
+
/** Original entry ID (only if preserveOriginalIds is true) */
|
|
178
|
+
_originalId?: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Context for tracking resolution state during recursive resolution.
|
|
183
|
+
* Used internally to prevent circular references.
|
|
184
|
+
*/
|
|
185
|
+
interface ResolutionContext {
|
|
186
|
+
/** Set of entry IDs currently being resolved (for circular detection) */
|
|
187
|
+
visitedEntries: Set<string>;
|
|
188
|
+
/** Set of media IDs currently being resolved */
|
|
189
|
+
visitedMedia: Set<string>;
|
|
190
|
+
/** Current resolution depth */
|
|
191
|
+
currentDepth: number;
|
|
192
|
+
/** Maximum allowed depth */
|
|
193
|
+
maxDepth: number;
|
|
194
|
+
/** Cache of already-resolved entries at this depth */
|
|
195
|
+
resolvedCache: Map<string, ResolvedContentEntry | null>;
|
|
196
|
+
/** Cache of already-resolved media assets */
|
|
197
|
+
mediaCache: Map<string, ResolvedMediaReference | null>;
|
|
198
|
+
/** Fields with detected circular references */
|
|
199
|
+
circularReferences: string[];
|
|
200
|
+
/** Fields with unresolved references */
|
|
201
|
+
unresolvedReferences: Record<string, string[]>;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Result of resolving references for multiple entries.
|
|
206
|
+
*/
|
|
207
|
+
export interface BatchResolveResult {
|
|
208
|
+
/** Successfully resolved entries */
|
|
209
|
+
resolved: ResolvedContentEntry[];
|
|
210
|
+
/** Entry IDs that could not be resolved */
|
|
211
|
+
unresolved: string[];
|
|
212
|
+
/** Summary of circular references detected */
|
|
213
|
+
circularReferencesDetected: number;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// =============================================================================
|
|
217
|
+
// Core Resolution Functions
|
|
218
|
+
// =============================================================================
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Resolve all references within a content entry's data.
|
|
222
|
+
*
|
|
223
|
+
* This function recursively resolves reference and media fields up to
|
|
224
|
+
* the specified depth, while preventing circular references.
|
|
225
|
+
*
|
|
226
|
+
* @param ctx - Convex query context
|
|
227
|
+
* @param entry - The content entry to resolve references for
|
|
228
|
+
* @param fields - Field definitions from the content type
|
|
229
|
+
* @param options - Resolution options
|
|
230
|
+
* @returns The entry with resolved references
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```typescript
|
|
234
|
+
* // Basic usage - resolve one level deep
|
|
235
|
+
* const resolved = await resolveEntryReferences(ctx, blogPost, contentType.fields);
|
|
236
|
+
*
|
|
237
|
+
* // Resolve two levels deep with only published entries
|
|
238
|
+
* const resolved = await resolveEntryReferences(ctx, blogPost, contentType.fields, {
|
|
239
|
+
* maxDepth: 2,
|
|
240
|
+
* publishedOnly: true,
|
|
241
|
+
* });
|
|
242
|
+
*
|
|
243
|
+
* // Resolve only specific fields
|
|
244
|
+
* const resolved = await resolveEntryReferences(ctx, blogPost, contentType.fields, {
|
|
245
|
+
* onlyFields: ["author", "featuredImage"],
|
|
246
|
+
* });
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
export async function resolveEntryReferences(
|
|
250
|
+
ctx: QueryCtx,
|
|
251
|
+
entry: {
|
|
252
|
+
_id: string;
|
|
253
|
+
slug: string;
|
|
254
|
+
status: string;
|
|
255
|
+
data: Record<string, unknown>;
|
|
256
|
+
contentTypeId?: string;
|
|
257
|
+
locale?: string;
|
|
258
|
+
version?: number;
|
|
259
|
+
},
|
|
260
|
+
fields: FieldDefinitionForResolver[],
|
|
261
|
+
options: DeepResolveOptions = {},
|
|
262
|
+
): Promise<ResolvedContentEntry> {
|
|
263
|
+
const {
|
|
264
|
+
maxDepth = 1,
|
|
265
|
+
resolveMedia = true,
|
|
266
|
+
resolveContent = true,
|
|
267
|
+
publishedOnly = false,
|
|
268
|
+
includeDeleted = false,
|
|
269
|
+
fields: selectFields,
|
|
270
|
+
onlyFields,
|
|
271
|
+
excludeFields,
|
|
272
|
+
preserveOriginalIds = false,
|
|
273
|
+
} = options;
|
|
274
|
+
|
|
275
|
+
// Get content type info
|
|
276
|
+
let contentTypeName = "";
|
|
277
|
+
let contentTypeDisplayName = "";
|
|
278
|
+
|
|
279
|
+
if (entry.contentTypeId) {
|
|
280
|
+
try {
|
|
281
|
+
const contentType = await ctx.db.get(
|
|
282
|
+
entry.contentTypeId as Id<"contentTypes">,
|
|
283
|
+
);
|
|
284
|
+
if (contentType) {
|
|
285
|
+
contentTypeName = contentType.name;
|
|
286
|
+
contentTypeDisplayName = contentType.displayName;
|
|
287
|
+
}
|
|
288
|
+
} catch {
|
|
289
|
+
// Content type not found, continue with empty names
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// If maxDepth is 0, return without resolving
|
|
294
|
+
if (maxDepth === 0) {
|
|
295
|
+
return {
|
|
296
|
+
id: entry._id,
|
|
297
|
+
contentTypeName,
|
|
298
|
+
contentTypeDisplayName,
|
|
299
|
+
slug: entry.slug,
|
|
300
|
+
status: entry.status,
|
|
301
|
+
data: entry.data,
|
|
302
|
+
exists: true,
|
|
303
|
+
locale: entry.locale,
|
|
304
|
+
version: entry.version,
|
|
305
|
+
...(preserveOriginalIds && { _originalId: entry._id }),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Initialize resolution context
|
|
310
|
+
const resolutionCtx: ResolutionContext = {
|
|
311
|
+
visitedEntries: new Set([entry._id]),
|
|
312
|
+
visitedMedia: new Set(),
|
|
313
|
+
currentDepth: 0,
|
|
314
|
+
maxDepth,
|
|
315
|
+
resolvedCache: new Map(),
|
|
316
|
+
mediaCache: new Map(),
|
|
317
|
+
circularReferences: [],
|
|
318
|
+
unresolvedReferences: {},
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// Filter fields to resolve based on options
|
|
322
|
+
const fieldsToResolve = filterFieldsToResolve(
|
|
323
|
+
fields,
|
|
324
|
+
onlyFields,
|
|
325
|
+
excludeFields,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// Resolve the entry data
|
|
329
|
+
const resolvedData = await resolveDataFields(
|
|
330
|
+
ctx,
|
|
331
|
+
entry.data,
|
|
332
|
+
fieldsToResolve,
|
|
333
|
+
resolutionCtx,
|
|
334
|
+
{
|
|
335
|
+
resolveMedia,
|
|
336
|
+
resolveContent,
|
|
337
|
+
publishedOnly,
|
|
338
|
+
includeDeleted,
|
|
339
|
+
selectFields,
|
|
340
|
+
preserveOriginalIds,
|
|
341
|
+
},
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
const result: ResolvedContentEntry = {
|
|
345
|
+
id: entry._id,
|
|
346
|
+
contentTypeName,
|
|
347
|
+
contentTypeDisplayName,
|
|
348
|
+
slug: entry.slug,
|
|
349
|
+
status: entry.status,
|
|
350
|
+
data: resolvedData,
|
|
351
|
+
exists: true,
|
|
352
|
+
locale: entry.locale,
|
|
353
|
+
version: entry.version,
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// Add metadata about resolution issues
|
|
357
|
+
if (resolutionCtx.circularReferences.length > 0) {
|
|
358
|
+
result._circularReferences = resolutionCtx.circularReferences;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (Object.keys(resolutionCtx.unresolvedReferences).length > 0) {
|
|
362
|
+
result._unresolvedReferences = resolutionCtx.unresolvedReferences;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (preserveOriginalIds) {
|
|
366
|
+
result._originalId = entry._id;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Resolve references for multiple content entries in batch.
|
|
374
|
+
*
|
|
375
|
+
* More efficient than calling resolveEntryReferences multiple times
|
|
376
|
+
* as it shares caches across entries.
|
|
377
|
+
*
|
|
378
|
+
* @param ctx - Convex query context
|
|
379
|
+
* @param entries - Array of content entries to resolve
|
|
380
|
+
* @param fields - Field definitions from the content type
|
|
381
|
+
* @param options - Resolution options
|
|
382
|
+
* @returns Batch result with resolved entries and unresolved IDs
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* ```typescript
|
|
386
|
+
* const { page } = await cms.contentEntries.list(ctx, { ... });
|
|
387
|
+
* const result = await resolveEntryReferencesBatch(ctx, page, contentType.fields, {
|
|
388
|
+
* maxDepth: 1,
|
|
389
|
+
* publishedOnly: true,
|
|
390
|
+
* });
|
|
391
|
+
* ```
|
|
392
|
+
*/
|
|
393
|
+
export async function resolveEntryReferencesBatch(
|
|
394
|
+
ctx: QueryCtx,
|
|
395
|
+
entries: Array<{
|
|
396
|
+
_id: string;
|
|
397
|
+
slug: string;
|
|
398
|
+
status: string;
|
|
399
|
+
data: Record<string, unknown>;
|
|
400
|
+
contentTypeId?: string;
|
|
401
|
+
locale?: string;
|
|
402
|
+
version?: number;
|
|
403
|
+
}>,
|
|
404
|
+
fields: FieldDefinitionForResolver[],
|
|
405
|
+
options: DeepResolveOptions = {},
|
|
406
|
+
): Promise<BatchResolveResult> {
|
|
407
|
+
const resolved: ResolvedContentEntry[] = [];
|
|
408
|
+
const unresolved: string[] = [];
|
|
409
|
+
let circularReferencesDetected = 0;
|
|
410
|
+
|
|
411
|
+
// Resolve each entry in parallel
|
|
412
|
+
const promises = entries.map(async (entry) => {
|
|
413
|
+
try {
|
|
414
|
+
const result = await resolveEntryReferences(ctx, entry, fields, options);
|
|
415
|
+
if (result._circularReferences) {
|
|
416
|
+
circularReferencesDetected += result._circularReferences.length;
|
|
417
|
+
}
|
|
418
|
+
return { success: true, result, id: entry._id };
|
|
419
|
+
} catch {
|
|
420
|
+
return { success: false, result: null, id: entry._id };
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const results = await Promise.all(promises);
|
|
425
|
+
|
|
426
|
+
for (const { success, result, id } of results) {
|
|
427
|
+
if (success && result) {
|
|
428
|
+
resolved.push(result);
|
|
429
|
+
} else {
|
|
430
|
+
unresolved.push(id);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
resolved,
|
|
436
|
+
unresolved,
|
|
437
|
+
circularReferencesDetected,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// =============================================================================
|
|
442
|
+
// Internal Resolution Functions
|
|
443
|
+
// =============================================================================
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Filter fields based on onlyFields and excludeFields options.
|
|
447
|
+
*/
|
|
448
|
+
function filterFieldsToResolve(
|
|
449
|
+
fields: FieldDefinitionForResolver[],
|
|
450
|
+
onlyFields?: string[],
|
|
451
|
+
excludeFields?: string[],
|
|
452
|
+
): FieldDefinitionForResolver[] {
|
|
453
|
+
let filtered = fields.filter(
|
|
454
|
+
(f) => f.type === "reference" || f.type === "media",
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
if (onlyFields && onlyFields.length > 0) {
|
|
458
|
+
filtered = filtered.filter((f) => onlyFields.includes(f.name));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (excludeFields && excludeFields.length > 0) {
|
|
462
|
+
filtered = filtered.filter((f) => !excludeFields.includes(f.name));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return filtered;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Resolve all reference and media fields in a data object.
|
|
470
|
+
*/
|
|
471
|
+
async function resolveDataFields(
|
|
472
|
+
ctx: QueryCtx,
|
|
473
|
+
data: Record<string, unknown>,
|
|
474
|
+
fields: FieldDefinitionForResolver[],
|
|
475
|
+
resolutionCtx: ResolutionContext,
|
|
476
|
+
options: {
|
|
477
|
+
resolveMedia: boolean;
|
|
478
|
+
resolveContent: boolean;
|
|
479
|
+
publishedOnly: boolean;
|
|
480
|
+
includeDeleted: boolean;
|
|
481
|
+
selectFields?: string[];
|
|
482
|
+
preserveOriginalIds: boolean;
|
|
483
|
+
},
|
|
484
|
+
): Promise<Record<string, unknown>> {
|
|
485
|
+
const resolvedData = { ...data };
|
|
486
|
+
|
|
487
|
+
// Process each resolvable field
|
|
488
|
+
for (const field of fields) {
|
|
489
|
+
const value = data[field.name];
|
|
490
|
+
|
|
491
|
+
if (value === null || value === undefined) {
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (field.type === "reference" && options.resolveContent) {
|
|
496
|
+
const resolved = await resolveReferenceField(
|
|
497
|
+
ctx,
|
|
498
|
+
field,
|
|
499
|
+
value,
|
|
500
|
+
resolutionCtx,
|
|
501
|
+
options,
|
|
502
|
+
);
|
|
503
|
+
resolvedData[field.name] = resolved;
|
|
504
|
+
} else if (field.type === "media" && options.resolveMedia) {
|
|
505
|
+
const resolved = await resolveMediaField(
|
|
506
|
+
ctx,
|
|
507
|
+
field,
|
|
508
|
+
value,
|
|
509
|
+
resolutionCtx,
|
|
510
|
+
options,
|
|
511
|
+
);
|
|
512
|
+
resolvedData[field.name] = resolved;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return resolvedData;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Resolve a reference field value (single or multiple).
|
|
521
|
+
*/
|
|
522
|
+
async function resolveReferenceField(
|
|
523
|
+
ctx: QueryCtx,
|
|
524
|
+
field: FieldDefinitionForResolver,
|
|
525
|
+
value: unknown,
|
|
526
|
+
resolutionCtx: ResolutionContext,
|
|
527
|
+
options: {
|
|
528
|
+
publishedOnly: boolean;
|
|
529
|
+
includeDeleted: boolean;
|
|
530
|
+
selectFields?: string[];
|
|
531
|
+
preserveOriginalIds: boolean;
|
|
532
|
+
},
|
|
533
|
+
): Promise<unknown> {
|
|
534
|
+
const isMultiple = field.options?.multiple ?? false;
|
|
535
|
+
|
|
536
|
+
if (isMultiple) {
|
|
537
|
+
// Resolve array of references
|
|
538
|
+
if (!Array.isArray(value)) {
|
|
539
|
+
return value; // Invalid, return as-is
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const resolvedArray: unknown[] = [];
|
|
543
|
+
const unresolvedIds: string[] = [];
|
|
544
|
+
|
|
545
|
+
for (const refId of value) {
|
|
546
|
+
if (typeof refId !== "string") {
|
|
547
|
+
resolvedArray.push(refId);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const resolved = await resolveNestedReference(
|
|
552
|
+
ctx,
|
|
553
|
+
refId,
|
|
554
|
+
field.name,
|
|
555
|
+
resolutionCtx,
|
|
556
|
+
options,
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
if (resolved) {
|
|
560
|
+
resolvedArray.push(resolved);
|
|
561
|
+
} else {
|
|
562
|
+
unresolvedIds.push(refId);
|
|
563
|
+
// Keep the original ID for unresolved references
|
|
564
|
+
if (options.preserveOriginalIds) {
|
|
565
|
+
resolvedArray.push({ _unresolvedId: refId });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (unresolvedIds.length > 0) {
|
|
571
|
+
resolutionCtx.unresolvedReferences[field.name] = unresolvedIds;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return resolvedArray;
|
|
575
|
+
} else {
|
|
576
|
+
// Resolve single reference
|
|
577
|
+
if (typeof value !== "string") {
|
|
578
|
+
return value; // Invalid, return as-is
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const resolved = await resolveNestedReference(
|
|
582
|
+
ctx,
|
|
583
|
+
value,
|
|
584
|
+
field.name,
|
|
585
|
+
resolutionCtx,
|
|
586
|
+
options,
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
if (!resolved) {
|
|
590
|
+
resolutionCtx.unresolvedReferences[field.name] = [value];
|
|
591
|
+
if (options.preserveOriginalIds) {
|
|
592
|
+
return { _unresolvedId: value };
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return resolved ?? value;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Resolve a nested content reference with circular detection.
|
|
602
|
+
*/
|
|
603
|
+
async function resolveNestedReference(
|
|
604
|
+
ctx: QueryCtx,
|
|
605
|
+
refId: string,
|
|
606
|
+
fieldName: string,
|
|
607
|
+
resolutionCtx: ResolutionContext,
|
|
608
|
+
options: {
|
|
609
|
+
publishedOnly: boolean;
|
|
610
|
+
includeDeleted: boolean;
|
|
611
|
+
selectFields?: string[];
|
|
612
|
+
preserveOriginalIds: boolean;
|
|
613
|
+
},
|
|
614
|
+
): Promise<ResolvedContentEntry | null> {
|
|
615
|
+
// Check cache first
|
|
616
|
+
if (resolutionCtx.resolvedCache.has(refId)) {
|
|
617
|
+
return resolutionCtx.resolvedCache.get(refId) ?? null;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Check for circular reference
|
|
621
|
+
if (resolutionCtx.visitedEntries.has(refId)) {
|
|
622
|
+
resolutionCtx.circularReferences.push(`${fieldName}:${refId}`);
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Check depth limit
|
|
627
|
+
if (resolutionCtx.currentDepth >= resolutionCtx.maxDepth) {
|
|
628
|
+
// At max depth, just return the basic resolved reference without recursing
|
|
629
|
+
const basicRef = await resolveReference(ctx, refId, {
|
|
630
|
+
publishedOnly: options.publishedOnly,
|
|
631
|
+
includeDeleted: options.includeDeleted,
|
|
632
|
+
fields: options.selectFields,
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
if (!basicRef) {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const result: ResolvedContentEntry = {
|
|
640
|
+
id: basicRef.id,
|
|
641
|
+
contentTypeName: basicRef.contentTypeName,
|
|
642
|
+
contentTypeDisplayName: basicRef.contentTypeDisplayName,
|
|
643
|
+
slug: basicRef.slug,
|
|
644
|
+
status: basicRef.status,
|
|
645
|
+
data: basicRef.data,
|
|
646
|
+
exists: basicRef.exists,
|
|
647
|
+
...(options.preserveOriginalIds && { _originalId: refId }),
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
resolutionCtx.resolvedCache.set(refId, result);
|
|
651
|
+
return result;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Mark as visiting
|
|
655
|
+
resolutionCtx.visitedEntries.add(refId);
|
|
656
|
+
resolutionCtx.currentDepth++;
|
|
657
|
+
|
|
658
|
+
try {
|
|
659
|
+
// Get the referenced entry
|
|
660
|
+
const entry = await ctx.db.get(refId as Id<"contentEntries">);
|
|
661
|
+
|
|
662
|
+
if (!entry) {
|
|
663
|
+
resolutionCtx.resolvedCache.set(refId, null);
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Check soft-delete
|
|
668
|
+
if (!options.includeDeleted && isDeleted(entry)) {
|
|
669
|
+
resolutionCtx.resolvedCache.set(refId, null);
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Check published status
|
|
674
|
+
if (options.publishedOnly && entry.status !== "published") {
|
|
675
|
+
resolutionCtx.resolvedCache.set(refId, null);
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Get content type for field definitions
|
|
680
|
+
const contentType = await ctx.db.get(entry.contentTypeId);
|
|
681
|
+
|
|
682
|
+
if (!contentType || isDeleted(contentType)) {
|
|
683
|
+
resolutionCtx.resolvedCache.set(refId, null);
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Recursively resolve this entry's references
|
|
688
|
+
const nestedFields = (contentType.fields as FieldDefinitionForResolver[]).filter(
|
|
689
|
+
(f) => f.type === "reference" || f.type === "media",
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
const resolvedData = await resolveDataFields(
|
|
693
|
+
ctx,
|
|
694
|
+
entry.data as Record<string, unknown>,
|
|
695
|
+
nestedFields,
|
|
696
|
+
resolutionCtx,
|
|
697
|
+
{
|
|
698
|
+
resolveMedia: true,
|
|
699
|
+
resolveContent: true,
|
|
700
|
+
publishedOnly: options.publishedOnly,
|
|
701
|
+
includeDeleted: options.includeDeleted,
|
|
702
|
+
selectFields: options.selectFields,
|
|
703
|
+
preserveOriginalIds: options.preserveOriginalIds,
|
|
704
|
+
},
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
// Filter fields if specified
|
|
708
|
+
let finalData = resolvedData;
|
|
709
|
+
if (options.selectFields && options.selectFields.length > 0) {
|
|
710
|
+
finalData = {};
|
|
711
|
+
for (const field of options.selectFields) {
|
|
712
|
+
if (field in resolvedData) {
|
|
713
|
+
finalData[field] = resolvedData[field];
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const result: ResolvedContentEntry = {
|
|
719
|
+
id: refId,
|
|
720
|
+
contentTypeName: contentType.name,
|
|
721
|
+
contentTypeDisplayName: contentType.displayName,
|
|
722
|
+
slug: entry.slug,
|
|
723
|
+
status: entry.status,
|
|
724
|
+
data: finalData,
|
|
725
|
+
exists: true,
|
|
726
|
+
locale: entry.locale,
|
|
727
|
+
version: entry.version,
|
|
728
|
+
...(options.preserveOriginalIds && { _originalId: refId }),
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
resolutionCtx.resolvedCache.set(refId, result);
|
|
732
|
+
return result;
|
|
733
|
+
} finally {
|
|
734
|
+
// Unmark as visiting (allow visiting again from different paths)
|
|
735
|
+
resolutionCtx.visitedEntries.delete(refId);
|
|
736
|
+
resolutionCtx.currentDepth--;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Resolve a media field value (single or multiple).
|
|
742
|
+
*/
|
|
743
|
+
async function resolveMediaField(
|
|
744
|
+
ctx: QueryCtx,
|
|
745
|
+
field: FieldDefinitionForResolver,
|
|
746
|
+
value: unknown,
|
|
747
|
+
resolutionCtx: ResolutionContext,
|
|
748
|
+
options: {
|
|
749
|
+
preserveOriginalIds: boolean;
|
|
750
|
+
includeDeleted: boolean;
|
|
751
|
+
},
|
|
752
|
+
): Promise<unknown> {
|
|
753
|
+
const isMultiple = field.options?.multiple ?? false;
|
|
754
|
+
const mediaOptions: MediaResolveOptions = {
|
|
755
|
+
includeDeleted: options.includeDeleted,
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
if (isMultiple) {
|
|
759
|
+
// Resolve array of media references
|
|
760
|
+
if (!Array.isArray(value)) {
|
|
761
|
+
return value;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const resolvedArray: unknown[] = [];
|
|
765
|
+
const unresolvedIds: string[] = [];
|
|
766
|
+
|
|
767
|
+
for (const mediaId of value) {
|
|
768
|
+
if (typeof mediaId !== "string") {
|
|
769
|
+
resolvedArray.push(mediaId);
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Check cache
|
|
774
|
+
if (resolutionCtx.mediaCache.has(mediaId)) {
|
|
775
|
+
const cached = resolutionCtx.mediaCache.get(mediaId);
|
|
776
|
+
if (cached) {
|
|
777
|
+
resolvedArray.push(
|
|
778
|
+
options.preserveOriginalIds
|
|
779
|
+
? { ...cached, _originalId: mediaId }
|
|
780
|
+
: cached,
|
|
781
|
+
);
|
|
782
|
+
} else {
|
|
783
|
+
unresolvedIds.push(mediaId);
|
|
784
|
+
}
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const resolved = await resolveMediaReference(ctx, mediaId, mediaOptions);
|
|
789
|
+
|
|
790
|
+
if (resolved) {
|
|
791
|
+
resolutionCtx.mediaCache.set(mediaId, resolved);
|
|
792
|
+
resolvedArray.push(
|
|
793
|
+
options.preserveOriginalIds
|
|
794
|
+
? { ...resolved, _originalId: mediaId }
|
|
795
|
+
: resolved,
|
|
796
|
+
);
|
|
797
|
+
} else {
|
|
798
|
+
resolutionCtx.mediaCache.set(mediaId, null);
|
|
799
|
+
unresolvedIds.push(mediaId);
|
|
800
|
+
if (options.preserveOriginalIds) {
|
|
801
|
+
resolvedArray.push({ _unresolvedId: mediaId });
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (unresolvedIds.length > 0) {
|
|
807
|
+
resolutionCtx.unresolvedReferences[field.name] = unresolvedIds;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return resolvedArray;
|
|
811
|
+
} else {
|
|
812
|
+
// Resolve single media reference
|
|
813
|
+
if (typeof value !== "string") {
|
|
814
|
+
return value;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Check cache
|
|
818
|
+
if (resolutionCtx.mediaCache.has(value)) {
|
|
819
|
+
const cached = resolutionCtx.mediaCache.get(value);
|
|
820
|
+
if (cached) {
|
|
821
|
+
return options.preserveOriginalIds
|
|
822
|
+
? { ...cached, _originalId: value }
|
|
823
|
+
: cached;
|
|
824
|
+
}
|
|
825
|
+
resolutionCtx.unresolvedReferences[field.name] = [value];
|
|
826
|
+
return options.preserveOriginalIds ? { _unresolvedId: value } : value;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const resolved = await resolveMediaReference(ctx, value, mediaOptions);
|
|
830
|
+
|
|
831
|
+
if (resolved) {
|
|
832
|
+
resolutionCtx.mediaCache.set(value, resolved);
|
|
833
|
+
return options.preserveOriginalIds
|
|
834
|
+
? { ...resolved, _originalId: value }
|
|
835
|
+
: resolved;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
resolutionCtx.mediaCache.set(value, null);
|
|
839
|
+
resolutionCtx.unresolvedReferences[field.name] = [value];
|
|
840
|
+
return options.preserveOriginalIds ? { _unresolvedId: value } : value;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// =============================================================================
|
|
845
|
+
// Utility Functions
|
|
846
|
+
// =============================================================================
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Check if a value contains circular reference markers.
|
|
850
|
+
*
|
|
851
|
+
* @param data - Data object to check
|
|
852
|
+
* @returns Array of field paths with circular references
|
|
853
|
+
*/
|
|
854
|
+
export function findCircularReferenceMarkers(
|
|
855
|
+
data: Record<string, unknown>,
|
|
856
|
+
): string[] {
|
|
857
|
+
const markers: string[] = [];
|
|
858
|
+
|
|
859
|
+
function traverse(obj: unknown, path: string): void {
|
|
860
|
+
if (obj === null || obj === undefined) {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (typeof obj === "object") {
|
|
865
|
+
if (Array.isArray(obj)) {
|
|
866
|
+
obj.forEach((item, index) => traverse(item, `${path}[${index}]`));
|
|
867
|
+
} else {
|
|
868
|
+
const record = obj as Record<string, unknown>;
|
|
869
|
+
if ("_circularReferences" in record) {
|
|
870
|
+
markers.push(path);
|
|
871
|
+
}
|
|
872
|
+
for (const [key, value] of Object.entries(record)) {
|
|
873
|
+
traverse(value, path ? `${path}.${key}` : key);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
traverse(data, "");
|
|
880
|
+
return markers;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Flatten resolved references to a simple lookup map.
|
|
885
|
+
* Useful for deduplicating references across multiple entries.
|
|
886
|
+
*
|
|
887
|
+
* @param entries - Array of resolved entries
|
|
888
|
+
* @returns Map of entry ID to resolved entry
|
|
889
|
+
*/
|
|
890
|
+
export function flattenResolvedReferences(
|
|
891
|
+
entries: ResolvedContentEntry[],
|
|
892
|
+
): Map<string, ResolvedContentEntry> {
|
|
893
|
+
const map = new Map<string, ResolvedContentEntry>();
|
|
894
|
+
|
|
895
|
+
function extractReferences(data: Record<string, unknown>): void {
|
|
896
|
+
for (const value of Object.values(data)) {
|
|
897
|
+
if (value === null || value === undefined) {
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (typeof value === "object") {
|
|
902
|
+
if (Array.isArray(value)) {
|
|
903
|
+
for (const item of value) {
|
|
904
|
+
if (isResolvedContentEntry(item)) {
|
|
905
|
+
map.set(item.id, item);
|
|
906
|
+
extractReferences(item.data);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
} else if (isResolvedContentEntry(value as Record<string, unknown>)) {
|
|
910
|
+
const entry = value as ResolvedContentEntry;
|
|
911
|
+
map.set(entry.id, entry);
|
|
912
|
+
extractReferences(entry.data);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
for (const entry of entries) {
|
|
919
|
+
map.set(entry.id, entry);
|
|
920
|
+
extractReferences(entry.data);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
return map;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Type guard to check if a value is a resolved content entry.
|
|
928
|
+
*/
|
|
929
|
+
function isResolvedContentEntry(value: unknown): value is ResolvedContentEntry {
|
|
930
|
+
if (typeof value !== "object" || value === null) {
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
const obj = value as Record<string, unknown>;
|
|
934
|
+
return (
|
|
935
|
+
"id" in obj &&
|
|
936
|
+
"contentTypeName" in obj &&
|
|
937
|
+
"slug" in obj &&
|
|
938
|
+
"status" in obj &&
|
|
939
|
+
"data" in obj &&
|
|
940
|
+
"exists" in obj
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Count the total number of references resolved in an entry.
|
|
946
|
+
*
|
|
947
|
+
* @param entry - Resolved entry to count
|
|
948
|
+
* @returns Object with counts of content and media references
|
|
949
|
+
*/
|
|
950
|
+
export function countResolvedReferences(
|
|
951
|
+
entry: ResolvedContentEntry,
|
|
952
|
+
): {
|
|
953
|
+
content: number;
|
|
954
|
+
media: number;
|
|
955
|
+
total: number;
|
|
956
|
+
} {
|
|
957
|
+
let content = 0;
|
|
958
|
+
let media = 0;
|
|
959
|
+
|
|
960
|
+
function count(value: unknown): void {
|
|
961
|
+
if (value === null || value === undefined) {
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (typeof value === "object") {
|
|
966
|
+
if (Array.isArray(value)) {
|
|
967
|
+
for (const item of value) {
|
|
968
|
+
count(item);
|
|
969
|
+
}
|
|
970
|
+
} else {
|
|
971
|
+
const record = value as Record<string, unknown>;
|
|
972
|
+
|
|
973
|
+
// Check if it's a resolved content entry
|
|
974
|
+
if (isResolvedContentEntry(record)) {
|
|
975
|
+
content++;
|
|
976
|
+
count(record.data);
|
|
977
|
+
}
|
|
978
|
+
// Check if it's a resolved media reference
|
|
979
|
+
else if (
|
|
980
|
+
"storageId" in record &&
|
|
981
|
+
"url" in record &&
|
|
982
|
+
"mimeType" in record
|
|
983
|
+
) {
|
|
984
|
+
media++;
|
|
985
|
+
}
|
|
986
|
+
// Otherwise recurse into nested objects
|
|
987
|
+
else {
|
|
988
|
+
for (const val of Object.values(record)) {
|
|
989
|
+
count(val);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
count(entry.data);
|
|
997
|
+
|
|
998
|
+
return { content, media, total: content + media };
|
|
999
|
+
}
|