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,931 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Export/Import Functions
|
|
3
|
+
*
|
|
4
|
+
* Provides functions to export content entries to JSON format and import from JSON.
|
|
5
|
+
* Supports selective export by type or filter, handles reference resolution, and
|
|
6
|
+
* validates imports against schemas.
|
|
7
|
+
*
|
|
8
|
+
* ## Export Features
|
|
9
|
+
* - Export all entries or filter by content type
|
|
10
|
+
* - Optionally resolve references to include related content
|
|
11
|
+
* - Support for status filtering (export only published, etc.)
|
|
12
|
+
* - Include content type definitions for schema validation during import
|
|
13
|
+
*
|
|
14
|
+
* ## Import Features
|
|
15
|
+
* - Validate all entries against content type schemas before import
|
|
16
|
+
* - Handle reference ID mapping (old IDs to new IDs)
|
|
17
|
+
* - Support for skip, update, or error on duplicate slugs
|
|
18
|
+
* - Dry-run mode to validate without making changes
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* // Export all published blog posts
|
|
23
|
+
* const exportData = await ctx.runQuery(api.exportImport.exportEntries, {
|
|
24
|
+
* contentTypeName: "blog_post",
|
|
25
|
+
* status: "published",
|
|
26
|
+
* includeReferences: true,
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* // Import entries with conflict resolution
|
|
30
|
+
* const result = await ctx.runMutation(api.exportImport.importEntries, {
|
|
31
|
+
* data: exportData,
|
|
32
|
+
* onConflict: "skip",
|
|
33
|
+
* importedBy: currentUserId,
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
import { v } from "convex/values";
|
|
38
|
+
import { isDeleted } from "./lib/softDelete.js";
|
|
39
|
+
import { query, mutation } from "./_generated/server.js";
|
|
40
|
+
import { contentStatusValidator,
|
|
41
|
+
// contentEntryDoc,
|
|
42
|
+
// contentTypeDoc,
|
|
43
|
+
fieldTypeValidator, } from "./validators.js";
|
|
44
|
+
import { validateContentData, } from "./validation.js";
|
|
45
|
+
import { ensureUniqueSlug } from "./lib/slugUniqueness.js";
|
|
46
|
+
// =============================================================================
|
|
47
|
+
// Export Types and Validators
|
|
48
|
+
// =============================================================================
|
|
49
|
+
/**
|
|
50
|
+
* Field options validator for exported content types.
|
|
51
|
+
* This is a specialized version that uses string for taxonomyId instead of
|
|
52
|
+
* Id<"taxonomies"> since IDs are not portable across Convex deployments
|
|
53
|
+
* during export/import operations.
|
|
54
|
+
*/
|
|
55
|
+
const exportedFieldOptionsValidator = v.optional(v.object({
|
|
56
|
+
// Text fields
|
|
57
|
+
minLength: v.optional(v.number()),
|
|
58
|
+
maxLength: v.optional(v.number()),
|
|
59
|
+
pattern: v.optional(v.string()),
|
|
60
|
+
// Number fields
|
|
61
|
+
min: v.optional(v.number()),
|
|
62
|
+
max: v.optional(v.number()),
|
|
63
|
+
step: v.optional(v.number()),
|
|
64
|
+
precision: v.optional(v.number()),
|
|
65
|
+
// Reference fields
|
|
66
|
+
allowedContentTypes: v.optional(v.array(v.string())),
|
|
67
|
+
multiple: v.optional(v.boolean()),
|
|
68
|
+
minItems: v.optional(v.number()),
|
|
69
|
+
// Media fields
|
|
70
|
+
allowedMimeTypes: v.optional(v.array(v.string())),
|
|
71
|
+
maxFileSize: v.optional(v.number()),
|
|
72
|
+
// Select fields
|
|
73
|
+
options: v.optional(v.array(v.object({
|
|
74
|
+
value: v.string(),
|
|
75
|
+
label: v.string(),
|
|
76
|
+
}))),
|
|
77
|
+
// Rich text fields
|
|
78
|
+
allowedBlocks: v.optional(v.array(v.string())),
|
|
79
|
+
allowedMarks: v.optional(v.array(v.string())),
|
|
80
|
+
// Tag fields - taxonomyId as string for portability
|
|
81
|
+
taxonomyId: v.optional(v.string()),
|
|
82
|
+
allowCreate: v.optional(v.boolean()),
|
|
83
|
+
maxTags: v.optional(v.number()),
|
|
84
|
+
minTags: v.optional(v.number()),
|
|
85
|
+
// Category fields
|
|
86
|
+
allowMultiple: v.optional(v.boolean()),
|
|
87
|
+
}));
|
|
88
|
+
/**
|
|
89
|
+
* Field definition validator for exported content types.
|
|
90
|
+
* Reuses fieldTypeValidator from schema but uses exportedFieldOptionsValidator
|
|
91
|
+
* which has string taxonomyId for portability across deployments.
|
|
92
|
+
*/
|
|
93
|
+
const exportedFieldDefinitionValidator = v.object({
|
|
94
|
+
name: v.string(),
|
|
95
|
+
label: v.string(),
|
|
96
|
+
type: fieldTypeValidator,
|
|
97
|
+
required: v.boolean(),
|
|
98
|
+
searchable: v.optional(v.boolean()),
|
|
99
|
+
localized: v.optional(v.boolean()),
|
|
100
|
+
description: v.optional(v.string()),
|
|
101
|
+
defaultValue: v.optional(v.any()),
|
|
102
|
+
options: exportedFieldOptionsValidator,
|
|
103
|
+
});
|
|
104
|
+
/**
|
|
105
|
+
* Structure for a single exported content entry.
|
|
106
|
+
* Includes all data needed to recreate the entry on import.
|
|
107
|
+
*/
|
|
108
|
+
export const exportedEntryValidator = v.object({
|
|
109
|
+
/** Original entry ID (for reference mapping) */
|
|
110
|
+
_originalId: v.string(),
|
|
111
|
+
/** Content type name (machine-readable) */
|
|
112
|
+
contentTypeName: v.string(),
|
|
113
|
+
/** URL-friendly slug */
|
|
114
|
+
slug: v.string(),
|
|
115
|
+
/** Entry status at time of export */
|
|
116
|
+
status: contentStatusValidator,
|
|
117
|
+
/** Content data */
|
|
118
|
+
data: v.any(),
|
|
119
|
+
/** Locale code if localized */
|
|
120
|
+
locale: v.optional(v.string()),
|
|
121
|
+
/** Version number at time of export */
|
|
122
|
+
version: v.number(),
|
|
123
|
+
/** First published timestamp */
|
|
124
|
+
firstPublishedAt: v.optional(v.number()),
|
|
125
|
+
/** Last published timestamp */
|
|
126
|
+
lastPublishedAt: v.optional(v.number()),
|
|
127
|
+
/** Scheduled publish timestamp */
|
|
128
|
+
scheduledPublishAt: v.optional(v.number()),
|
|
129
|
+
/** User who created the entry */
|
|
130
|
+
createdBy: v.optional(v.string()),
|
|
131
|
+
/** Original creation timestamp */
|
|
132
|
+
createdAt: v.number(),
|
|
133
|
+
});
|
|
134
|
+
/**
|
|
135
|
+
* Structure for an exported content type definition.
|
|
136
|
+
* Allows importing schemas along with content.
|
|
137
|
+
* Uses exportedFieldDefinitionValidator which has string taxonomyId
|
|
138
|
+
* for portability across Convex deployments.
|
|
139
|
+
*/
|
|
140
|
+
export const exportedContentTypeValidator = v.object({
|
|
141
|
+
/** Content type name (machine-readable) */
|
|
142
|
+
name: v.string(),
|
|
143
|
+
/** Display name */
|
|
144
|
+
displayName: v.string(),
|
|
145
|
+
/** Description */
|
|
146
|
+
description: v.optional(v.string()),
|
|
147
|
+
/** Field definitions with portable types */
|
|
148
|
+
fields: v.array(exportedFieldDefinitionValidator),
|
|
149
|
+
/** Icon identifier */
|
|
150
|
+
icon: v.optional(v.string()),
|
|
151
|
+
/** Whether this is a singleton type */
|
|
152
|
+
singleton: v.optional(v.boolean()),
|
|
153
|
+
/** Field to generate slugs from */
|
|
154
|
+
slugField: v.optional(v.string()),
|
|
155
|
+
/** Field to use for display titles */
|
|
156
|
+
titleField: v.optional(v.string()),
|
|
157
|
+
});
|
|
158
|
+
/**
|
|
159
|
+
* Complete export package structure.
|
|
160
|
+
* Contains all information needed to import content into another instance.
|
|
161
|
+
*/
|
|
162
|
+
export const exportPackageValidator = v.object({
|
|
163
|
+
/** Export format version for compatibility checking */
|
|
164
|
+
version: v.literal("1.0"),
|
|
165
|
+
/** Timestamp when export was created */
|
|
166
|
+
exportedAt: v.number(),
|
|
167
|
+
/** Content type definitions (optional, for schema validation) */
|
|
168
|
+
contentTypes: v.optional(v.array(exportedContentTypeValidator)),
|
|
169
|
+
/** Exported entries */
|
|
170
|
+
entries: v.array(exportedEntryValidator),
|
|
171
|
+
/** Metadata about the export */
|
|
172
|
+
metadata: v.optional(v.object({
|
|
173
|
+
/** Source system identifier */
|
|
174
|
+
source: v.optional(v.string()),
|
|
175
|
+
/** Export description */
|
|
176
|
+
description: v.optional(v.string()),
|
|
177
|
+
/** Total count of entries */
|
|
178
|
+
totalEntries: v.number(),
|
|
179
|
+
/** Breakdown by content type */
|
|
180
|
+
entriesByType: v.optional(v.any()),
|
|
181
|
+
})),
|
|
182
|
+
});
|
|
183
|
+
// =============================================================================
|
|
184
|
+
// Import Types and Validators
|
|
185
|
+
// =============================================================================
|
|
186
|
+
/**
|
|
187
|
+
* Conflict resolution strategy for imports.
|
|
188
|
+
* - "skip": Skip entries with conflicting slugs
|
|
189
|
+
* - "update": Update existing entries with new data
|
|
190
|
+
* - "error": Fail the entire import if any conflicts exist
|
|
191
|
+
*/
|
|
192
|
+
export const conflictStrategyValidator = v.union(v.literal("skip"), v.literal("update"), v.literal("error"));
|
|
193
|
+
/**
|
|
194
|
+
* Result for a single imported entry.
|
|
195
|
+
*/
|
|
196
|
+
export const importEntryResultValidator = v.object({
|
|
197
|
+
/** Original ID from export */
|
|
198
|
+
originalId: v.string(),
|
|
199
|
+
/** New ID after import (if created/updated) */
|
|
200
|
+
newId: v.optional(v.id("contentEntries")),
|
|
201
|
+
/** Import action taken */
|
|
202
|
+
action: v.union(v.literal("created"), v.literal("updated"), v.literal("skipped"), v.literal("failed")),
|
|
203
|
+
/** Error message if failed */
|
|
204
|
+
error: v.optional(v.string()),
|
|
205
|
+
/** Slug of the entry */
|
|
206
|
+
slug: v.string(),
|
|
207
|
+
/** Content type name */
|
|
208
|
+
contentTypeName: v.string(),
|
|
209
|
+
});
|
|
210
|
+
/**
|
|
211
|
+
* Complete import result.
|
|
212
|
+
*/
|
|
213
|
+
export const importResultValidator = v.object({
|
|
214
|
+
/** Whether import was successful */
|
|
215
|
+
success: v.boolean(),
|
|
216
|
+
/** Total entries processed */
|
|
217
|
+
totalProcessed: v.number(),
|
|
218
|
+
/** Number of entries created */
|
|
219
|
+
created: v.number(),
|
|
220
|
+
/** Number of entries updated */
|
|
221
|
+
updated: v.number(),
|
|
222
|
+
/** Number of entries skipped */
|
|
223
|
+
skipped: v.number(),
|
|
224
|
+
/** Number of entries failed */
|
|
225
|
+
failed: v.number(),
|
|
226
|
+
/** Detailed results for each entry */
|
|
227
|
+
results: v.array(importEntryResultValidator),
|
|
228
|
+
/** ID mapping from old to new IDs (for reference updates) */
|
|
229
|
+
idMapping: v.any(),
|
|
230
|
+
/** Validation errors encountered */
|
|
231
|
+
validationErrors: v.optional(v.array(v.string())),
|
|
232
|
+
});
|
|
233
|
+
// =============================================================================
|
|
234
|
+
// Export Function
|
|
235
|
+
// =============================================================================
|
|
236
|
+
/**
|
|
237
|
+
* Arguments for the export function.
|
|
238
|
+
*/
|
|
239
|
+
const exportEntriesArgs = v.object({
|
|
240
|
+
/** Filter by content type ID */
|
|
241
|
+
contentTypeId: v.optional(v.id("contentTypes")),
|
|
242
|
+
/** Filter by content type name (alternative to contentTypeId) */
|
|
243
|
+
contentTypeName: v.optional(v.string()),
|
|
244
|
+
/** Filter by status */
|
|
245
|
+
status: v.optional(contentStatusValidator),
|
|
246
|
+
/** Filter by multiple statuses */
|
|
247
|
+
statusIn: v.optional(v.array(contentStatusValidator)),
|
|
248
|
+
/** Filter by locale */
|
|
249
|
+
locale: v.optional(v.string()),
|
|
250
|
+
/** Include content type definitions in export */
|
|
251
|
+
includeContentTypes: v.optional(v.boolean()),
|
|
252
|
+
/** Include soft-deleted entries */
|
|
253
|
+
includeDeleted: v.optional(v.boolean()),
|
|
254
|
+
/** Maximum number of entries to export (default: 1000) */
|
|
255
|
+
limit: v.optional(v.number()),
|
|
256
|
+
/** Export description for metadata */
|
|
257
|
+
description: v.optional(v.string()),
|
|
258
|
+
/** Source identifier for metadata */
|
|
259
|
+
source: v.optional(v.string()),
|
|
260
|
+
});
|
|
261
|
+
/**
|
|
262
|
+
* Export content entries to a JSON-serializable package.
|
|
263
|
+
*
|
|
264
|
+
* This query retrieves content entries matching the specified filters and
|
|
265
|
+
* packages them into a format suitable for import into another system.
|
|
266
|
+
*
|
|
267
|
+
* ## Features
|
|
268
|
+
* - Filter by content type, status, or locale
|
|
269
|
+
* - Optionally include content type definitions for schema validation
|
|
270
|
+
* - Preserves original IDs for reference mapping during import
|
|
271
|
+
* - Includes metadata about the export for traceability
|
|
272
|
+
*
|
|
273
|
+
* @param contentTypeId - Filter by content type ID
|
|
274
|
+
* @param contentTypeName - Filter by content type name (alternative to ID)
|
|
275
|
+
* @param status - Filter by single status
|
|
276
|
+
* @param statusIn - Filter by multiple statuses
|
|
277
|
+
* @param locale - Filter by locale code
|
|
278
|
+
* @param includeContentTypes - Include content type definitions (default: true)
|
|
279
|
+
* @param includeDeleted - Include soft-deleted entries (default: false)
|
|
280
|
+
* @param limit - Maximum entries to export (default: 1000)
|
|
281
|
+
* @param description - Description for export metadata
|
|
282
|
+
* @param source - Source identifier for export metadata
|
|
283
|
+
*
|
|
284
|
+
* @returns ExportPackage containing entries and optional content types
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* ```typescript
|
|
288
|
+
* // Export all published blog posts
|
|
289
|
+
* const exportData = await ctx.runQuery(api.exportImport.exportEntries, {
|
|
290
|
+
* contentTypeName: "blog_post",
|
|
291
|
+
* status: "published",
|
|
292
|
+
* includeContentTypes: true,
|
|
293
|
+
* });
|
|
294
|
+
*
|
|
295
|
+
* // Export all entries of all types
|
|
296
|
+
* const allData = await ctx.runQuery(api.exportImport.exportEntries, {
|
|
297
|
+
* limit: 5000,
|
|
298
|
+
* description: "Full site backup",
|
|
299
|
+
* });
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
export const exportEntries = query({
|
|
303
|
+
args: exportEntriesArgs.fields,
|
|
304
|
+
returns: exportPackageValidator,
|
|
305
|
+
handler: async (ctx, args) => {
|
|
306
|
+
const { contentTypeId, contentTypeName, status, statusIn, locale, includeContentTypes = true, includeDeleted = false, limit = 1000, description, source, } = args;
|
|
307
|
+
// Resolve status filter
|
|
308
|
+
const resolvedStatuses = statusIn?.length
|
|
309
|
+
? statusIn
|
|
310
|
+
: status
|
|
311
|
+
? [status]
|
|
312
|
+
: undefined;
|
|
313
|
+
// Resolve content type ID from name if needed
|
|
314
|
+
let resolvedContentTypeId = contentTypeId;
|
|
315
|
+
if (!resolvedContentTypeId && contentTypeName) {
|
|
316
|
+
const contentType = await ctx.db
|
|
317
|
+
.query("contentTypes")
|
|
318
|
+
.withIndex("by_name", (q) => q.eq("name", contentTypeName))
|
|
319
|
+
.first();
|
|
320
|
+
if (contentType) {
|
|
321
|
+
resolvedContentTypeId = contentType._id;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Build query for entries
|
|
325
|
+
let entriesQuery;
|
|
326
|
+
if (resolvedContentTypeId) {
|
|
327
|
+
entriesQuery = ctx.db
|
|
328
|
+
.query("contentEntries")
|
|
329
|
+
.withIndex("by_content_type", (q) => q.eq("contentTypeId", resolvedContentTypeId));
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
entriesQuery = ctx.db.query("contentEntries");
|
|
333
|
+
}
|
|
334
|
+
// Fetch entries (we'll filter in memory for complex conditions)
|
|
335
|
+
const allEntries = await entriesQuery.take(limit * 2);
|
|
336
|
+
// Apply filters
|
|
337
|
+
let filteredEntries = allEntries;
|
|
338
|
+
// Filter by deleted status
|
|
339
|
+
if (!includeDeleted) {
|
|
340
|
+
filteredEntries = filteredEntries.filter((e) => !isDeleted(e));
|
|
341
|
+
}
|
|
342
|
+
// Filter by status
|
|
343
|
+
if (resolvedStatuses && resolvedStatuses.length > 0) {
|
|
344
|
+
filteredEntries = filteredEntries.filter((e) => resolvedStatuses.includes(e.status));
|
|
345
|
+
}
|
|
346
|
+
// Filter by locale
|
|
347
|
+
if (locale) {
|
|
348
|
+
filteredEntries = filteredEntries.filter((e) => e.locale === locale);
|
|
349
|
+
}
|
|
350
|
+
// Limit results
|
|
351
|
+
filteredEntries = filteredEntries.slice(0, limit);
|
|
352
|
+
// Get unique content type IDs from entries
|
|
353
|
+
const contentTypeIdsSet = new Set();
|
|
354
|
+
for (const entry of filteredEntries) {
|
|
355
|
+
contentTypeIdsSet.add(entry.contentTypeId);
|
|
356
|
+
}
|
|
357
|
+
const contentTypeIds = Array.from(contentTypeIdsSet);
|
|
358
|
+
// Fetch content types
|
|
359
|
+
const contentTypesMap = new Map();
|
|
360
|
+
for (const typeId of contentTypeIds) {
|
|
361
|
+
const contentType = await ctx.db.get(typeId);
|
|
362
|
+
if (contentType) {
|
|
363
|
+
contentTypesMap.set(typeId, contentType);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Build exported entries
|
|
367
|
+
const exportedEntries = filteredEntries.map((entry) => {
|
|
368
|
+
const contentType = contentTypesMap.get(entry.contentTypeId);
|
|
369
|
+
return {
|
|
370
|
+
_originalId: entry._id,
|
|
371
|
+
contentTypeName: contentType?.name ?? "unknown",
|
|
372
|
+
slug: entry.slug,
|
|
373
|
+
status: entry.status,
|
|
374
|
+
data: entry.data,
|
|
375
|
+
locale: entry.locale,
|
|
376
|
+
version: entry.version,
|
|
377
|
+
firstPublishedAt: entry.firstPublishedAt,
|
|
378
|
+
lastPublishedAt: entry.lastPublishedAt,
|
|
379
|
+
scheduledPublishAt: entry.scheduledPublishAt,
|
|
380
|
+
createdBy: entry.createdBy,
|
|
381
|
+
createdAt: entry._creationTime,
|
|
382
|
+
};
|
|
383
|
+
});
|
|
384
|
+
// Build exported content types if requested
|
|
385
|
+
let exportedContentTypes;
|
|
386
|
+
if (includeContentTypes) {
|
|
387
|
+
exportedContentTypes = Array.from(contentTypesMap.values())
|
|
388
|
+
.filter((ct) => !ct.deletedAt)
|
|
389
|
+
.map((ct) => ({
|
|
390
|
+
name: ct.name,
|
|
391
|
+
displayName: ct.displayName,
|
|
392
|
+
description: ct.description,
|
|
393
|
+
fields: ct.fields,
|
|
394
|
+
icon: ct.icon,
|
|
395
|
+
singleton: ct.singleton,
|
|
396
|
+
slugField: ct.slugField,
|
|
397
|
+
titleField: ct.titleField,
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
// Build entries by type count
|
|
401
|
+
const entriesByType = {};
|
|
402
|
+
for (const entry of exportedEntries) {
|
|
403
|
+
entriesByType[entry.contentTypeName] =
|
|
404
|
+
(entriesByType[entry.contentTypeName] ?? 0) + 1;
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
version: "1.0",
|
|
408
|
+
exportedAt: Date.now(),
|
|
409
|
+
contentTypes: exportedContentTypes,
|
|
410
|
+
entries: exportedEntries,
|
|
411
|
+
metadata: {
|
|
412
|
+
source,
|
|
413
|
+
description,
|
|
414
|
+
totalEntries: exportedEntries.length,
|
|
415
|
+
entriesByType,
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
// =============================================================================
|
|
421
|
+
// Import Function
|
|
422
|
+
// =============================================================================
|
|
423
|
+
/**
|
|
424
|
+
* Arguments for the import function.
|
|
425
|
+
*/
|
|
426
|
+
const importEntriesArgs = v.object({
|
|
427
|
+
/** The export package to import */
|
|
428
|
+
data: exportPackageValidator,
|
|
429
|
+
/** How to handle conflicting slugs */
|
|
430
|
+
onConflict: v.optional(conflictStrategyValidator),
|
|
431
|
+
/** Whether to preserve original status or set all to draft */
|
|
432
|
+
preserveStatus: v.optional(v.boolean()),
|
|
433
|
+
/** Whether to run validation only without making changes */
|
|
434
|
+
dryRun: v.optional(v.boolean()),
|
|
435
|
+
/** User ID for audit trail */
|
|
436
|
+
importedBy: v.optional(v.string()),
|
|
437
|
+
/** Filter which content types to import (by name) */
|
|
438
|
+
contentTypeFilter: v.optional(v.array(v.string())),
|
|
439
|
+
});
|
|
440
|
+
/**
|
|
441
|
+
* Import content entries from an export package.
|
|
442
|
+
*
|
|
443
|
+
* This mutation validates and imports content entries from an export package.
|
|
444
|
+
* It supports conflict resolution, dry-run mode, and reference ID mapping.
|
|
445
|
+
*
|
|
446
|
+
* ## Features
|
|
447
|
+
* - Validates all entries against content type schemas before import
|
|
448
|
+
* - Maps old reference IDs to new IDs for reference fields
|
|
449
|
+
* - Supports skip, update, or error on slug conflicts
|
|
450
|
+
* - Dry-run mode validates without making changes
|
|
451
|
+
* - Preserves or resets entry status
|
|
452
|
+
*
|
|
453
|
+
* ## Import Process
|
|
454
|
+
* 1. Validate all entries against existing content type schemas
|
|
455
|
+
* 2. Check for slug conflicts based on onConflict strategy
|
|
456
|
+
* 3. Create or update entries, collecting ID mappings
|
|
457
|
+
* 4. Update reference fields with new IDs (second pass)
|
|
458
|
+
*
|
|
459
|
+
* @param data - The export package to import
|
|
460
|
+
* @param onConflict - How to handle slug conflicts (default: "skip")
|
|
461
|
+
* @param preserveStatus - Keep original status or set to draft (default: false)
|
|
462
|
+
* @param dryRun - Validate only without making changes (default: false)
|
|
463
|
+
* @param importedBy - User ID for audit trail
|
|
464
|
+
* @param contentTypeFilter - Only import entries of these content types
|
|
465
|
+
*
|
|
466
|
+
* @returns ImportResult with details of the import operation
|
|
467
|
+
*
|
|
468
|
+
* @example
|
|
469
|
+
* ```typescript
|
|
470
|
+
* // Dry run to validate import
|
|
471
|
+
* const validation = await ctx.runMutation(api.exportImport.importEntries, {
|
|
472
|
+
* data: exportPackage,
|
|
473
|
+
* dryRun: true,
|
|
474
|
+
* });
|
|
475
|
+
*
|
|
476
|
+
* // Import with skip on conflicts
|
|
477
|
+
* const result = await ctx.runMutation(api.exportImport.importEntries, {
|
|
478
|
+
* data: exportPackage,
|
|
479
|
+
* onConflict: "skip",
|
|
480
|
+
* preserveStatus: true,
|
|
481
|
+
* importedBy: currentUserId,
|
|
482
|
+
* });
|
|
483
|
+
* ```
|
|
484
|
+
*/
|
|
485
|
+
export const importEntries = mutation({
|
|
486
|
+
args: importEntriesArgs.fields,
|
|
487
|
+
returns: importResultValidator,
|
|
488
|
+
handler: async (ctx, args) => {
|
|
489
|
+
const { data, onConflict = "skip", preserveStatus = false, dryRun = false, importedBy, contentTypeFilter, } = args;
|
|
490
|
+
const results = [];
|
|
491
|
+
const idMapping = {};
|
|
492
|
+
const validationErrors = [];
|
|
493
|
+
let created = 0;
|
|
494
|
+
let updated = 0;
|
|
495
|
+
let skipped = 0;
|
|
496
|
+
let failed = 0;
|
|
497
|
+
// Filter entries by content type if specified
|
|
498
|
+
let entriesToImport = data.entries;
|
|
499
|
+
if (contentTypeFilter && contentTypeFilter.length > 0) {
|
|
500
|
+
entriesToImport = entriesToImport.filter((e) => contentTypeFilter.includes(e.contentTypeName));
|
|
501
|
+
}
|
|
502
|
+
// Build a map of content type name to content type document
|
|
503
|
+
const contentTypeMap = new Map();
|
|
504
|
+
const contentTypeNamesSet = new Set();
|
|
505
|
+
for (const entry of entriesToImport) {
|
|
506
|
+
contentTypeNamesSet.add(entry.contentTypeName);
|
|
507
|
+
}
|
|
508
|
+
const contentTypeNames = Array.from(contentTypeNamesSet);
|
|
509
|
+
for (const typeName of contentTypeNames) {
|
|
510
|
+
const contentType = await ctx.db
|
|
511
|
+
.query("contentTypes")
|
|
512
|
+
.withIndex("by_name", (q) => q.eq("name", typeName))
|
|
513
|
+
.first();
|
|
514
|
+
if (contentType && !contentType.deletedAt && contentType.isActive) {
|
|
515
|
+
contentTypeMap.set(typeName, contentType);
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
validationErrors.push(`Content type "${typeName}" not found or not active`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// Validate all entries first
|
|
522
|
+
for (const entry of entriesToImport) {
|
|
523
|
+
const contentType = contentTypeMap.get(entry.contentTypeName);
|
|
524
|
+
if (!contentType) {
|
|
525
|
+
results.push({
|
|
526
|
+
originalId: entry._originalId,
|
|
527
|
+
action: "failed",
|
|
528
|
+
error: `Content type "${entry.contentTypeName}" not found`,
|
|
529
|
+
slug: entry.slug,
|
|
530
|
+
contentTypeName: entry.contentTypeName,
|
|
531
|
+
});
|
|
532
|
+
failed++;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
// Build schema for validation
|
|
536
|
+
const schema = {
|
|
537
|
+
name: contentType.name,
|
|
538
|
+
displayName: contentType.displayName,
|
|
539
|
+
description: contentType.description,
|
|
540
|
+
fields: contentType.fields,
|
|
541
|
+
titleField: contentType.titleField,
|
|
542
|
+
slugField: contentType.slugField,
|
|
543
|
+
singleton: contentType.singleton,
|
|
544
|
+
};
|
|
545
|
+
// Validate content data
|
|
546
|
+
const validationResult = validateContentData(entry.data, schema);
|
|
547
|
+
if (!validationResult.valid) {
|
|
548
|
+
const errorMessages = validationResult.errors
|
|
549
|
+
.map((e) => `${e.field}: ${e.message}`)
|
|
550
|
+
.join("; ");
|
|
551
|
+
validationErrors.push(`Entry "${entry.slug}" (${entry.contentTypeName}): ${errorMessages}`);
|
|
552
|
+
results.push({
|
|
553
|
+
originalId: entry._originalId,
|
|
554
|
+
action: "failed",
|
|
555
|
+
error: `Validation failed: ${errorMessages}`,
|
|
556
|
+
slug: entry.slug,
|
|
557
|
+
contentTypeName: entry.contentTypeName,
|
|
558
|
+
});
|
|
559
|
+
failed++;
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
// Check for existing entry with same slug
|
|
563
|
+
const existingEntry = await ctx.db
|
|
564
|
+
.query("contentEntries")
|
|
565
|
+
.withIndex("by_content_type_and_slug", (q) => q.eq("contentTypeId", contentType._id).eq("slug", entry.slug))
|
|
566
|
+
.filter((q) => q.eq(q.field("deletedAt"), undefined))
|
|
567
|
+
.first();
|
|
568
|
+
if (existingEntry) {
|
|
569
|
+
// Handle conflict based on strategy
|
|
570
|
+
switch (onConflict) {
|
|
571
|
+
case "error":
|
|
572
|
+
validationErrors.push(`Slug conflict: "${entry.slug}" already exists for type "${entry.contentTypeName}"`);
|
|
573
|
+
results.push({
|
|
574
|
+
originalId: entry._originalId,
|
|
575
|
+
action: "failed",
|
|
576
|
+
error: `Slug "${entry.slug}" already exists`,
|
|
577
|
+
slug: entry.slug,
|
|
578
|
+
contentTypeName: entry.contentTypeName,
|
|
579
|
+
});
|
|
580
|
+
failed++;
|
|
581
|
+
continue;
|
|
582
|
+
case "skip":
|
|
583
|
+
results.push({
|
|
584
|
+
originalId: entry._originalId,
|
|
585
|
+
newId: existingEntry._id,
|
|
586
|
+
action: "skipped",
|
|
587
|
+
slug: entry.slug,
|
|
588
|
+
contentTypeName: entry.contentTypeName,
|
|
589
|
+
});
|
|
590
|
+
idMapping[entry._originalId] = existingEntry._id;
|
|
591
|
+
skipped++;
|
|
592
|
+
continue;
|
|
593
|
+
case "update":
|
|
594
|
+
if (!dryRun) {
|
|
595
|
+
// Update existing entry
|
|
596
|
+
const status = preserveStatus
|
|
597
|
+
? entry.status
|
|
598
|
+
: existingEntry.status;
|
|
599
|
+
await ctx.db.patch(existingEntry._id, {
|
|
600
|
+
data: entry.data,
|
|
601
|
+
status,
|
|
602
|
+
version: existingEntry.version + 1,
|
|
603
|
+
updatedBy: importedBy,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
results.push({
|
|
607
|
+
originalId: entry._originalId,
|
|
608
|
+
newId: existingEntry._id,
|
|
609
|
+
action: "updated",
|
|
610
|
+
slug: entry.slug,
|
|
611
|
+
contentTypeName: entry.contentTypeName,
|
|
612
|
+
});
|
|
613
|
+
idMapping[entry._originalId] = existingEntry._id;
|
|
614
|
+
updated++;
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// Create new entry
|
|
619
|
+
if (!dryRun) {
|
|
620
|
+
// Generate search text from searchable fields
|
|
621
|
+
let searchText = "";
|
|
622
|
+
for (const field of contentType.fields) {
|
|
623
|
+
const fieldData = entry.data;
|
|
624
|
+
if (field.searchable && fieldData[field.name]) {
|
|
625
|
+
const value = fieldData[field.name];
|
|
626
|
+
if (typeof value === "string") {
|
|
627
|
+
searchText += ` ${value}`;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// Ensure unique slug
|
|
632
|
+
const queryFn = async (candidateSlug) => {
|
|
633
|
+
return await ctx.db
|
|
634
|
+
.query("contentEntries")
|
|
635
|
+
.withIndex("by_content_type_and_slug", (q) => q.eq("contentTypeId", contentType._id).eq("slug", candidateSlug))
|
|
636
|
+
.filter((q) => q.eq(q.field("deletedAt"), undefined))
|
|
637
|
+
.first();
|
|
638
|
+
};
|
|
639
|
+
const uniqueSlug = await ensureUniqueSlug(entry.slug, queryFn);
|
|
640
|
+
const newEntryId = await ctx.db.insert("contentEntries", {
|
|
641
|
+
contentTypeId: contentType._id,
|
|
642
|
+
slug: uniqueSlug,
|
|
643
|
+
status: preserveStatus ? entry.status : "draft",
|
|
644
|
+
data: entry.data,
|
|
645
|
+
locale: entry.locale,
|
|
646
|
+
version: 1,
|
|
647
|
+
createdBy: importedBy ?? entry.createdBy,
|
|
648
|
+
updatedBy: importedBy ?? entry.createdBy,
|
|
649
|
+
searchText: searchText.trim() || undefined,
|
|
650
|
+
// Only preserve publication timestamps if preserving status
|
|
651
|
+
firstPublishedAt: preserveStatus ? entry.firstPublishedAt : undefined,
|
|
652
|
+
lastPublishedAt: preserveStatus ? entry.lastPublishedAt : undefined,
|
|
653
|
+
scheduledPublishAt: preserveStatus
|
|
654
|
+
? entry.scheduledPublishAt
|
|
655
|
+
: undefined,
|
|
656
|
+
});
|
|
657
|
+
results.push({
|
|
658
|
+
originalId: entry._originalId,
|
|
659
|
+
newId: newEntryId,
|
|
660
|
+
action: "created",
|
|
661
|
+
slug: uniqueSlug,
|
|
662
|
+
contentTypeName: entry.contentTypeName,
|
|
663
|
+
});
|
|
664
|
+
idMapping[entry._originalId] = newEntryId;
|
|
665
|
+
created++;
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
// Dry run - simulate creation
|
|
669
|
+
results.push({
|
|
670
|
+
originalId: entry._originalId,
|
|
671
|
+
action: "created",
|
|
672
|
+
slug: entry.slug,
|
|
673
|
+
contentTypeName: entry.contentTypeName,
|
|
674
|
+
});
|
|
675
|
+
created++;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// Second pass: Update reference fields with new IDs
|
|
679
|
+
if (!dryRun && Object.keys(idMapping).length > 0) {
|
|
680
|
+
for (const result of results) {
|
|
681
|
+
if ((result.action === "created" || result.action === "updated") &&
|
|
682
|
+
result.newId) {
|
|
683
|
+
const entry = await ctx.db.get(result.newId);
|
|
684
|
+
if (!entry)
|
|
685
|
+
continue;
|
|
686
|
+
const contentType = contentTypeMap.get(result.contentTypeName);
|
|
687
|
+
if (!contentType)
|
|
688
|
+
continue;
|
|
689
|
+
const entryData = entry.data;
|
|
690
|
+
let dataChanged = false;
|
|
691
|
+
const updatedData = { ...entryData };
|
|
692
|
+
// Find reference fields and update IDs
|
|
693
|
+
for (const field of contentType.fields) {
|
|
694
|
+
if (field.type === "reference") {
|
|
695
|
+
const value = entryData[field.name];
|
|
696
|
+
if (field.options?.multiple && Array.isArray(value)) {
|
|
697
|
+
const newRefs = value.map((refId) => idMapping[refId] ?? refId);
|
|
698
|
+
if (JSON.stringify(newRefs) !== JSON.stringify(value)) {
|
|
699
|
+
updatedData[field.name] = newRefs;
|
|
700
|
+
dataChanged = true;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
else if (typeof value === "string" && idMapping[value]) {
|
|
704
|
+
updatedData[field.name] = idMapping[value];
|
|
705
|
+
dataChanged = true;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (dataChanged) {
|
|
710
|
+
await ctx.db.patch(result.newId, {
|
|
711
|
+
data: updatedData,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
const success = failed === 0 &&
|
|
718
|
+
validationErrors.filter((e) => !e.includes("Slug conflict")).length === 0;
|
|
719
|
+
return {
|
|
720
|
+
success,
|
|
721
|
+
totalProcessed: entriesToImport.length,
|
|
722
|
+
created,
|
|
723
|
+
updated,
|
|
724
|
+
skipped,
|
|
725
|
+
failed,
|
|
726
|
+
results,
|
|
727
|
+
idMapping,
|
|
728
|
+
validationErrors: validationErrors.length > 0 ? validationErrors : undefined,
|
|
729
|
+
};
|
|
730
|
+
},
|
|
731
|
+
});
|
|
732
|
+
// =============================================================================
|
|
733
|
+
// Utility Functions
|
|
734
|
+
// =============================================================================
|
|
735
|
+
/**
|
|
736
|
+
* Get a summary of content that would be exported without actually exporting.
|
|
737
|
+
*
|
|
738
|
+
* This is useful for previewing what an export would contain before
|
|
739
|
+
* running the full export operation.
|
|
740
|
+
*/
|
|
741
|
+
export const getExportPreview = query({
|
|
742
|
+
args: {
|
|
743
|
+
contentTypeId: v.optional(v.id("contentTypes")),
|
|
744
|
+
contentTypeName: v.optional(v.string()),
|
|
745
|
+
status: v.optional(contentStatusValidator),
|
|
746
|
+
statusIn: v.optional(v.array(contentStatusValidator)),
|
|
747
|
+
locale: v.optional(v.string()),
|
|
748
|
+
includeDeleted: v.optional(v.boolean()),
|
|
749
|
+
},
|
|
750
|
+
returns: v.object({
|
|
751
|
+
totalEntries: v.number(),
|
|
752
|
+
entriesByType: v.any(),
|
|
753
|
+
entriesByStatus: v.any(),
|
|
754
|
+
contentTypes: v.array(v.string()),
|
|
755
|
+
}),
|
|
756
|
+
handler: async (ctx, args) => {
|
|
757
|
+
const { contentTypeId, contentTypeName, status, statusIn, locale, includeDeleted = false, } = args;
|
|
758
|
+
// Resolve status filter
|
|
759
|
+
const resolvedStatuses = statusIn?.length
|
|
760
|
+
? statusIn
|
|
761
|
+
: status
|
|
762
|
+
? [status]
|
|
763
|
+
: undefined;
|
|
764
|
+
// Resolve content type ID
|
|
765
|
+
let resolvedContentTypeId = contentTypeId;
|
|
766
|
+
if (!resolvedContentTypeId && contentTypeName) {
|
|
767
|
+
const contentType = await ctx.db
|
|
768
|
+
.query("contentTypes")
|
|
769
|
+
.withIndex("by_name", (q) => q.eq("name", contentTypeName))
|
|
770
|
+
.first();
|
|
771
|
+
if (contentType) {
|
|
772
|
+
resolvedContentTypeId = contentType._id;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
// Build query
|
|
776
|
+
let entriesQuery;
|
|
777
|
+
if (resolvedContentTypeId) {
|
|
778
|
+
entriesQuery = ctx.db
|
|
779
|
+
.query("contentEntries")
|
|
780
|
+
.withIndex("by_content_type", (q) => q.eq("contentTypeId", resolvedContentTypeId));
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
entriesQuery = ctx.db.query("contentEntries");
|
|
784
|
+
}
|
|
785
|
+
// Fetch all matching entries
|
|
786
|
+
const allEntries = await entriesQuery.collect();
|
|
787
|
+
// Apply filters
|
|
788
|
+
let filteredEntries = allEntries;
|
|
789
|
+
if (!includeDeleted) {
|
|
790
|
+
filteredEntries = filteredEntries.filter((e) => !isDeleted(e));
|
|
791
|
+
}
|
|
792
|
+
if (resolvedStatuses && resolvedStatuses.length > 0) {
|
|
793
|
+
filteredEntries = filteredEntries.filter((e) => resolvedStatuses.includes(e.status));
|
|
794
|
+
}
|
|
795
|
+
if (locale) {
|
|
796
|
+
filteredEntries = filteredEntries.filter((e) => e.locale === locale);
|
|
797
|
+
}
|
|
798
|
+
// Get content types
|
|
799
|
+
const contentTypeIdsSet = new Set();
|
|
800
|
+
for (const entry of filteredEntries) {
|
|
801
|
+
contentTypeIdsSet.add(entry.contentTypeId);
|
|
802
|
+
}
|
|
803
|
+
const contentTypeIds = Array.from(contentTypeIdsSet);
|
|
804
|
+
const contentTypeNames = [];
|
|
805
|
+
const contentTypeNameMap = new Map();
|
|
806
|
+
for (const typeId of contentTypeIds) {
|
|
807
|
+
const contentType = await ctx.db.get(typeId);
|
|
808
|
+
if (contentType) {
|
|
809
|
+
contentTypeNames.push(contentType.name);
|
|
810
|
+
contentTypeNameMap.set(typeId, contentType.name);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
// Count by type
|
|
814
|
+
const entriesByType = {};
|
|
815
|
+
for (const entry of filteredEntries) {
|
|
816
|
+
const typeName = contentTypeNameMap.get(entry.contentTypeId) ?? "unknown";
|
|
817
|
+
entriesByType[typeName] = (entriesByType[typeName] ?? 0) + 1;
|
|
818
|
+
}
|
|
819
|
+
// Count by status
|
|
820
|
+
const entriesByStatus = {};
|
|
821
|
+
for (const entry of filteredEntries) {
|
|
822
|
+
entriesByStatus[entry.status] = (entriesByStatus[entry.status] ?? 0) + 1;
|
|
823
|
+
}
|
|
824
|
+
return {
|
|
825
|
+
totalEntries: filteredEntries.length,
|
|
826
|
+
entriesByType,
|
|
827
|
+
entriesByStatus,
|
|
828
|
+
contentTypes: contentTypeNames,
|
|
829
|
+
};
|
|
830
|
+
},
|
|
831
|
+
});
|
|
832
|
+
/**
|
|
833
|
+
* Validate an export package without importing.
|
|
834
|
+
*
|
|
835
|
+
* Checks that all entries can be validated against existing content type
|
|
836
|
+
* schemas and reports any issues that would occur during import.
|
|
837
|
+
*/
|
|
838
|
+
export const validateImportPackage = query({
|
|
839
|
+
args: {
|
|
840
|
+
data: exportPackageValidator,
|
|
841
|
+
contentTypeFilter: v.optional(v.array(v.string())),
|
|
842
|
+
},
|
|
843
|
+
returns: v.object({
|
|
844
|
+
valid: v.boolean(),
|
|
845
|
+
totalEntries: v.number(),
|
|
846
|
+
validEntries: v.number(),
|
|
847
|
+
invalidEntries: v.number(),
|
|
848
|
+
missingContentTypes: v.array(v.string()),
|
|
849
|
+
validationErrors: v.array(v.object({
|
|
850
|
+
slug: v.string(),
|
|
851
|
+
contentTypeName: v.string(),
|
|
852
|
+
errors: v.array(v.string()),
|
|
853
|
+
})),
|
|
854
|
+
}),
|
|
855
|
+
handler: async (ctx, args) => {
|
|
856
|
+
const { data, contentTypeFilter } = args;
|
|
857
|
+
const missingContentTypes = [];
|
|
858
|
+
const validationErrors = [];
|
|
859
|
+
// Filter entries
|
|
860
|
+
let entriesToValidate = data.entries;
|
|
861
|
+
if (contentTypeFilter && contentTypeFilter.length > 0) {
|
|
862
|
+
entriesToValidate = entriesToValidate.filter((e) => contentTypeFilter.includes(e.contentTypeName));
|
|
863
|
+
}
|
|
864
|
+
// Build content type map
|
|
865
|
+
const contentTypeMap = new Map();
|
|
866
|
+
const contentTypeNamesSet = new Set();
|
|
867
|
+
for (const entry of entriesToValidate) {
|
|
868
|
+
contentTypeNamesSet.add(entry.contentTypeName);
|
|
869
|
+
}
|
|
870
|
+
const contentTypeNames = Array.from(contentTypeNamesSet);
|
|
871
|
+
for (const typeName of contentTypeNames) {
|
|
872
|
+
const contentType = await ctx.db
|
|
873
|
+
.query("contentTypes")
|
|
874
|
+
.withIndex("by_name", (q) => q.eq("name", typeName))
|
|
875
|
+
.first();
|
|
876
|
+
if (contentType && !contentType.deletedAt && contentType.isActive) {
|
|
877
|
+
contentTypeMap.set(typeName, contentType);
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
missingContentTypes.push(typeName);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
let validEntries = 0;
|
|
884
|
+
let invalidEntries = 0;
|
|
885
|
+
// Validate each entry
|
|
886
|
+
for (const entry of entriesToValidate) {
|
|
887
|
+
const contentType = contentTypeMap.get(entry.contentTypeName);
|
|
888
|
+
if (!contentType) {
|
|
889
|
+
invalidEntries++;
|
|
890
|
+
validationErrors.push({
|
|
891
|
+
slug: entry.slug,
|
|
892
|
+
contentTypeName: entry.contentTypeName,
|
|
893
|
+
errors: [`Content type "${entry.contentTypeName}" not found`],
|
|
894
|
+
});
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
// Build schema
|
|
898
|
+
const schema = {
|
|
899
|
+
name: contentType.name,
|
|
900
|
+
displayName: contentType.displayName,
|
|
901
|
+
description: contentType.description,
|
|
902
|
+
fields: contentType.fields,
|
|
903
|
+
titleField: contentType.titleField,
|
|
904
|
+
slugField: contentType.slugField,
|
|
905
|
+
singleton: contentType.singleton,
|
|
906
|
+
};
|
|
907
|
+
// Validate
|
|
908
|
+
const result = validateContentData(entry.data, schema);
|
|
909
|
+
if (result.valid) {
|
|
910
|
+
validEntries++;
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
invalidEntries++;
|
|
914
|
+
validationErrors.push({
|
|
915
|
+
slug: entry.slug,
|
|
916
|
+
contentTypeName: entry.contentTypeName,
|
|
917
|
+
errors: result.errors.map((e) => `${e.field}: ${e.message}`),
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
valid: invalidEntries === 0 && missingContentTypes.length === 0,
|
|
923
|
+
totalEntries: entriesToValidate.length,
|
|
924
|
+
validEntries,
|
|
925
|
+
invalidEntries,
|
|
926
|
+
missingContentTypes,
|
|
927
|
+
validationErrors,
|
|
928
|
+
};
|
|
929
|
+
},
|
|
930
|
+
});
|
|
931
|
+
//# sourceMappingURL=exportImport.js.map
|