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,969 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Type Mutation Functions
|
|
3
|
+
*
|
|
4
|
+
* Provides mutation functions for creating, updating, and managing content types.
|
|
5
|
+
* Content types define the schema/blueprint for content entries in the CMS.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { v } from "convex/values";
|
|
9
|
+
import { isDeleted } from "./lib/softDelete.js";
|
|
10
|
+
import { mutation } from "./_generated/server.js";
|
|
11
|
+
import {
|
|
12
|
+
createContentTypeArgs,
|
|
13
|
+
updateContentTypeArgs,
|
|
14
|
+
deleteContentTypeArgs,
|
|
15
|
+
contentTypeDoc,
|
|
16
|
+
type FieldType,
|
|
17
|
+
mutationAuthContext,
|
|
18
|
+
} from "./validators.js";
|
|
19
|
+
import type { FieldDefinition } from "./validation.js";
|
|
20
|
+
import {
|
|
21
|
+
emitEvent,
|
|
22
|
+
contentTypeEventType,
|
|
23
|
+
ContentTypeEventPayload,
|
|
24
|
+
} from "./eventEmitter.js";
|
|
25
|
+
import { fieldTypes } from "./schema.js";
|
|
26
|
+
import {
|
|
27
|
+
contentTypeNotFound,
|
|
28
|
+
contentTypeDeleted,
|
|
29
|
+
contentTypeNameInvalid,
|
|
30
|
+
contentTypeNameDuplicate,
|
|
31
|
+
contentTypeFieldValidationFailed,
|
|
32
|
+
contentTypeSlugFieldInvalid,
|
|
33
|
+
contentTypeTitleFieldInvalid,
|
|
34
|
+
contentTypeHasEntries,
|
|
35
|
+
contentTypeBreakingChange,
|
|
36
|
+
// batchSizeExceeded,
|
|
37
|
+
internalError,
|
|
38
|
+
} from "./lib/errors.js";
|
|
39
|
+
import { requireMutationAuth } from "./lib/mutationAuth.js";
|
|
40
|
+
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// Breaking Change Detection Types
|
|
43
|
+
// =============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Describes a potential breaking change when updating a content type.
|
|
47
|
+
*/
|
|
48
|
+
interface BreakingChange {
|
|
49
|
+
/** Type of breaking change detected */
|
|
50
|
+
type:
|
|
51
|
+
| "FIELD_REMOVED"
|
|
52
|
+
| "FIELD_TYPE_CHANGED"
|
|
53
|
+
| "FIELD_MADE_REQUIRED"
|
|
54
|
+
| "SELECT_OPTIONS_REMOVED"
|
|
55
|
+
| "REFERENCE_TYPES_RESTRICTED"
|
|
56
|
+
| "VALIDATION_TIGHTENED";
|
|
57
|
+
/** The field name affected */
|
|
58
|
+
fieldName: string;
|
|
59
|
+
/** Human-readable description of the breaking change */
|
|
60
|
+
message: string;
|
|
61
|
+
/** Number of entries affected by this change */
|
|
62
|
+
affectedEntriesCount: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validation error for content type field definitions.
|
|
67
|
+
*/
|
|
68
|
+
interface FieldValidationError {
|
|
69
|
+
/** The field name that has the error */
|
|
70
|
+
fieldName: string;
|
|
71
|
+
/** Human-readable error message */
|
|
72
|
+
message: string;
|
|
73
|
+
/** Error code for programmatic handling */
|
|
74
|
+
code:
|
|
75
|
+
| "DUPLICATE_FIELD_NAME"
|
|
76
|
+
| "INVALID_FIELD_TYPE"
|
|
77
|
+
| "MISSING_REQUIRED_PROPERTY"
|
|
78
|
+
| "INVALID_FIELD_NAME"
|
|
79
|
+
| "INVALID_SELECT_OPTIONS";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validates the name format for content types and fields.
|
|
84
|
+
* Names must be valid identifiers: lowercase letters, numbers, and underscores.
|
|
85
|
+
* Must start with a letter and be 1-64 characters.
|
|
86
|
+
*/
|
|
87
|
+
function isValidName(name: string): boolean {
|
|
88
|
+
const namePattern = /^[a-z][a-z0-9_]{0,63}$/;
|
|
89
|
+
return namePattern.test(name);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Detects breaking changes between old and new field definitions.
|
|
94
|
+
* Returns an array of breaking changes that would affect existing content entries.
|
|
95
|
+
*
|
|
96
|
+
* @param oldFields - Current field definitions
|
|
97
|
+
* @param newFields - Proposed new field definitions
|
|
98
|
+
* @param existingEntries - Existing content entries to check for impact
|
|
99
|
+
* @returns Array of detected breaking changes with affected entry counts
|
|
100
|
+
*/
|
|
101
|
+
function detectBreakingChanges(
|
|
102
|
+
oldFields: FieldDefinition[],
|
|
103
|
+
newFields: FieldDefinition[],
|
|
104
|
+
existingEntries: Array<{ data: Record<string, unknown> }>
|
|
105
|
+
): BreakingChange[] {
|
|
106
|
+
const breakingChanges: BreakingChange[] = [];
|
|
107
|
+
const oldFieldMap = new Map(oldFields.map((f) => [f.name, f]));
|
|
108
|
+
const newFieldMap = new Map(newFields.map((f) => [f.name, f]));
|
|
109
|
+
|
|
110
|
+
// Check for removed fields that have data in existing entries
|
|
111
|
+
for (const oldField of oldFields) {
|
|
112
|
+
if (!newFieldMap.has(oldField.name)) {
|
|
113
|
+
// Field is being removed - count entries with data in this field
|
|
114
|
+
const affectedCount = existingEntries.filter((entry) => {
|
|
115
|
+
const value = entry.data[oldField.name];
|
|
116
|
+
return value !== undefined && value !== null && value !== "";
|
|
117
|
+
}).length;
|
|
118
|
+
|
|
119
|
+
if (affectedCount > 0) {
|
|
120
|
+
breakingChanges.push({
|
|
121
|
+
type: "FIELD_REMOVED",
|
|
122
|
+
fieldName: oldField.name,
|
|
123
|
+
message: `Removing field "${oldField.name}" will delete data from ${affectedCount} existing entries`,
|
|
124
|
+
affectedEntriesCount: affectedCount,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check for changes to existing fields
|
|
131
|
+
for (const newField of newFields) {
|
|
132
|
+
const oldField = oldFieldMap.get(newField.name);
|
|
133
|
+
if (!oldField) continue; // New field, no breaking change possible
|
|
134
|
+
|
|
135
|
+
// Check for type changes
|
|
136
|
+
if (oldField.type !== newField.type) {
|
|
137
|
+
const affectedCount = existingEntries.filter((entry) => {
|
|
138
|
+
const value = entry.data[newField.name];
|
|
139
|
+
return value !== undefined && value !== null;
|
|
140
|
+
}).length;
|
|
141
|
+
|
|
142
|
+
if (affectedCount > 0) {
|
|
143
|
+
breakingChanges.push({
|
|
144
|
+
type: "FIELD_TYPE_CHANGED",
|
|
145
|
+
fieldName: newField.name,
|
|
146
|
+
message: `Changing field "${newField.name}" type from "${oldField.type}" to "${newField.type}" may invalidate ${affectedCount} existing entries`,
|
|
147
|
+
affectedEntriesCount: affectedCount,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check for optional -> required changes
|
|
153
|
+
if (!oldField.required && newField.required) {
|
|
154
|
+
const affectedCount = existingEntries.filter((entry) => {
|
|
155
|
+
const value = entry.data[newField.name];
|
|
156
|
+
return value === undefined || value === null || value === "";
|
|
157
|
+
}).length;
|
|
158
|
+
|
|
159
|
+
if (affectedCount > 0) {
|
|
160
|
+
breakingChanges.push({
|
|
161
|
+
type: "FIELD_MADE_REQUIRED",
|
|
162
|
+
fieldName: newField.name,
|
|
163
|
+
message: `Making field "${newField.name}" required will invalidate ${affectedCount} entries with missing values`,
|
|
164
|
+
affectedEntriesCount: affectedCount,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check for removed select/multiSelect options
|
|
170
|
+
if (
|
|
171
|
+
(oldField.type === "select" || oldField.type === "multiSelect") &&
|
|
172
|
+
oldField.options?.options &&
|
|
173
|
+
newField.options?.options
|
|
174
|
+
) {
|
|
175
|
+
const oldOptions = new Set(oldField.options.options.map((o) => o.value));
|
|
176
|
+
const newOptions = new Set(newField.options.options.map((o) => o.value));
|
|
177
|
+
const removedOptions = [...oldOptions].filter((o) => !newOptions.has(o));
|
|
178
|
+
|
|
179
|
+
if (removedOptions.length > 0) {
|
|
180
|
+
const affectedCount = existingEntries.filter((entry) => {
|
|
181
|
+
const value = entry.data[newField.name];
|
|
182
|
+
if (oldField.type === "select") {
|
|
183
|
+
return removedOptions.includes(value as string);
|
|
184
|
+
} else {
|
|
185
|
+
// multiSelect - check if any values are in removed options
|
|
186
|
+
const values = value as string[] | undefined;
|
|
187
|
+
return values?.some((v) => removedOptions.includes(v));
|
|
188
|
+
}
|
|
189
|
+
}).length;
|
|
190
|
+
|
|
191
|
+
if (affectedCount > 0) {
|
|
192
|
+
breakingChanges.push({
|
|
193
|
+
type: "SELECT_OPTIONS_REMOVED",
|
|
194
|
+
fieldName: newField.name,
|
|
195
|
+
message: `Removing options [${removedOptions.join(", ")}] from "${newField.name}" will invalidate ${affectedCount} entries using those values`,
|
|
196
|
+
affectedEntriesCount: affectedCount,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check for restricted reference content types
|
|
203
|
+
if (
|
|
204
|
+
oldField.type === "reference" &&
|
|
205
|
+
newField.type === "reference" &&
|
|
206
|
+
oldField.options?.allowedContentTypes &&
|
|
207
|
+
newField.options?.allowedContentTypes
|
|
208
|
+
) {
|
|
209
|
+
const oldAllowed = new Set(oldField.options.allowedContentTypes);
|
|
210
|
+
const newAllowed = new Set(newField.options.allowedContentTypes);
|
|
211
|
+
const removedTypes = [...oldAllowed].filter((t) => !newAllowed.has(t));
|
|
212
|
+
|
|
213
|
+
// Note: We can't easily check if existing references point to removed types
|
|
214
|
+
// without resolving references. This is a warning-level change.
|
|
215
|
+
if (removedTypes.length > 0) {
|
|
216
|
+
breakingChanges.push({
|
|
217
|
+
type: "REFERENCE_TYPES_RESTRICTED",
|
|
218
|
+
fieldName: newField.name,
|
|
219
|
+
message: `Restricting allowed content types for "${newField.name}" by removing [${removedTypes.join(", ")}] may invalidate existing references`,
|
|
220
|
+
affectedEntriesCount: existingEntries.length, // Potentially all entries
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check for tightened validation (minLength increased, maxLength decreased, etc.)
|
|
226
|
+
if (
|
|
227
|
+
oldField.type === "text" &&
|
|
228
|
+
newField.type === "text" &&
|
|
229
|
+
oldField.options &&
|
|
230
|
+
newField.options
|
|
231
|
+
) {
|
|
232
|
+
const violations: string[] = [];
|
|
233
|
+
|
|
234
|
+
// Check if minLength was increased
|
|
235
|
+
if (
|
|
236
|
+
newField.options.minLength !== undefined &&
|
|
237
|
+
(oldField.options.minLength === undefined ||
|
|
238
|
+
newField.options.minLength > oldField.options.minLength)
|
|
239
|
+
) {
|
|
240
|
+
violations.push(
|
|
241
|
+
`minLength increased to ${newField.options.minLength}`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Check if maxLength was decreased
|
|
246
|
+
if (
|
|
247
|
+
newField.options.maxLength !== undefined &&
|
|
248
|
+
oldField.options.maxLength !== undefined &&
|
|
249
|
+
newField.options.maxLength < oldField.options.maxLength
|
|
250
|
+
) {
|
|
251
|
+
violations.push(
|
|
252
|
+
`maxLength decreased to ${newField.options.maxLength}`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (violations.length > 0) {
|
|
257
|
+
const affectedCount = existingEntries.filter((entry) => {
|
|
258
|
+
const value = entry.data[newField.name];
|
|
259
|
+
if (typeof value !== "string") return false;
|
|
260
|
+
|
|
261
|
+
if (
|
|
262
|
+
newField.options?.minLength !== undefined &&
|
|
263
|
+
value.length < newField.options.minLength
|
|
264
|
+
) {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
if (
|
|
268
|
+
newField.options?.maxLength !== undefined &&
|
|
269
|
+
value.length > newField.options.maxLength
|
|
270
|
+
) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
}).length;
|
|
275
|
+
|
|
276
|
+
if (affectedCount > 0) {
|
|
277
|
+
breakingChanges.push({
|
|
278
|
+
type: "VALIDATION_TIGHTENED",
|
|
279
|
+
fieldName: newField.name,
|
|
280
|
+
message: `Tightening validation for "${newField.name}" (${violations.join(", ")}) will invalidate ${affectedCount} entries`,
|
|
281
|
+
affectedEntriesCount: affectedCount,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return breakingChanges;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Validates field definitions for a content type.
|
|
293
|
+
* Checks for:
|
|
294
|
+
* - Unique field names
|
|
295
|
+
* - Valid field types
|
|
296
|
+
* - Required properties (name, label, type, required)
|
|
297
|
+
* - Valid field name format
|
|
298
|
+
* - Select/multiSelect fields have options defined
|
|
299
|
+
*/
|
|
300
|
+
function validateFieldDefinitions(
|
|
301
|
+
fields: FieldDefinition[]
|
|
302
|
+
): FieldValidationError[] {
|
|
303
|
+
const errors: FieldValidationError[] = [];
|
|
304
|
+
const seenNames = new Set<string>();
|
|
305
|
+
|
|
306
|
+
for (const field of fields) {
|
|
307
|
+
// Check for missing required properties
|
|
308
|
+
if (!field.name || typeof field.name !== "string") {
|
|
309
|
+
errors.push({
|
|
310
|
+
fieldName: field.name || "(unnamed)",
|
|
311
|
+
message: "Field must have a name property",
|
|
312
|
+
code: "MISSING_REQUIRED_PROPERTY",
|
|
313
|
+
});
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!field.label || typeof field.label !== "string") {
|
|
318
|
+
errors.push({
|
|
319
|
+
fieldName: field.name,
|
|
320
|
+
message: `Field "${field.name}" must have a label property`,
|
|
321
|
+
code: "MISSING_REQUIRED_PROPERTY",
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!field.type || typeof field.type !== "string") {
|
|
326
|
+
errors.push({
|
|
327
|
+
fieldName: field.name,
|
|
328
|
+
message: `Field "${field.name}" must have a type property`,
|
|
329
|
+
code: "MISSING_REQUIRED_PROPERTY",
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (typeof field.required !== "boolean") {
|
|
334
|
+
errors.push({
|
|
335
|
+
fieldName: field.name,
|
|
336
|
+
message: `Field "${field.name}" must have a required property (boolean)`,
|
|
337
|
+
code: "MISSING_REQUIRED_PROPERTY",
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Validate field name format
|
|
342
|
+
if (field.name && !isValidName(field.name)) {
|
|
343
|
+
errors.push({
|
|
344
|
+
fieldName: field.name,
|
|
345
|
+
message: `Field name "${field.name}" must start with a lowercase letter and contain only lowercase letters, numbers, and underscores (max 64 chars)`,
|
|
346
|
+
code: "INVALID_FIELD_NAME",
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Check for duplicate field names
|
|
351
|
+
if (seenNames.has(field.name)) {
|
|
352
|
+
errors.push({
|
|
353
|
+
fieldName: field.name,
|
|
354
|
+
message: `Duplicate field name: "${field.name}"`,
|
|
355
|
+
code: "DUPLICATE_FIELD_NAME",
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
seenNames.add(field.name);
|
|
359
|
+
|
|
360
|
+
// Validate field type is one of the supported types
|
|
361
|
+
if (field.type && !fieldTypes.includes(field.type as FieldType)) {
|
|
362
|
+
errors.push({
|
|
363
|
+
fieldName: field.name,
|
|
364
|
+
message: `Invalid field type "${field.type}". Must be one of: ${fieldTypes.join(", ")}`,
|
|
365
|
+
code: "INVALID_FIELD_TYPE",
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Validate select/multiSelect fields have options
|
|
370
|
+
if (
|
|
371
|
+
(field.type === "select" || field.type === "multiSelect") &&
|
|
372
|
+
(!field.options?.options || field.options.options.length === 0)
|
|
373
|
+
) {
|
|
374
|
+
errors.push({
|
|
375
|
+
fieldName: field.name,
|
|
376
|
+
message: `${field.type} field "${field.name}" must have options defined`,
|
|
377
|
+
code: "INVALID_SELECT_OPTIONS",
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return errors;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Mutation to create a new content type.
|
|
387
|
+
*
|
|
388
|
+
* Creates a content type definition with a unique name, display name, and
|
|
389
|
+
* field definitions. The content type can then be used to create content entries.
|
|
390
|
+
*
|
|
391
|
+
* @param name - Unique machine-readable name (e.g., "blog_post")
|
|
392
|
+
* @param displayName - Human-readable name (e.g., "Blog Post")
|
|
393
|
+
* @param description - Optional description of the content type
|
|
394
|
+
* @param fields - Array of field definitions
|
|
395
|
+
* @param icon - Optional icon identifier for UI
|
|
396
|
+
* @param singleton - If true, only one entry of this type can exist
|
|
397
|
+
* @param slugField - Field name to use for slug generation (defaults to first text field)
|
|
398
|
+
* @param titleField - Field name to use as display title (defaults to first text field)
|
|
399
|
+
* @param sortOrder - Custom sort order for admin UI
|
|
400
|
+
* @param createdBy - User ID who is creating this content type
|
|
401
|
+
*
|
|
402
|
+
* @returns The created content type document
|
|
403
|
+
*
|
|
404
|
+
* @throws Error if the name is not unique
|
|
405
|
+
* @throws Error if the name format is invalid
|
|
406
|
+
* @throws Error if field definitions are invalid
|
|
407
|
+
*
|
|
408
|
+
* @example
|
|
409
|
+
* ```typescript
|
|
410
|
+
* const blogPost = await ctx.runMutation(api.contentTypeMutations.createContentType, {
|
|
411
|
+
* name: "blog_post",
|
|
412
|
+
* displayName: "Blog Post",
|
|
413
|
+
* description: "Articles for the company blog",
|
|
414
|
+
* fields: [
|
|
415
|
+
* { name: "title", label: "Title", type: "text", required: true },
|
|
416
|
+
* { name: "content", label: "Content", type: "richText", required: true },
|
|
417
|
+
* { name: "published_date", label: "Published Date", type: "date", required: false },
|
|
418
|
+
* ],
|
|
419
|
+
* slugField: "title",
|
|
420
|
+
* titleField: "title",
|
|
421
|
+
* createdBy: currentUserId,
|
|
422
|
+
* });
|
|
423
|
+
* ```
|
|
424
|
+
*/
|
|
425
|
+
export const createContentType = mutation({
|
|
426
|
+
args: {
|
|
427
|
+
...createContentTypeArgs.fields,
|
|
428
|
+
/** Optional auth context for mutation-level authorization */
|
|
429
|
+
_auth: v.optional(mutationAuthContext),
|
|
430
|
+
},
|
|
431
|
+
returns: contentTypeDoc,
|
|
432
|
+
handler: async (ctx, args) => {
|
|
433
|
+
const {
|
|
434
|
+
name,
|
|
435
|
+
displayName,
|
|
436
|
+
description,
|
|
437
|
+
fields,
|
|
438
|
+
icon,
|
|
439
|
+
singleton,
|
|
440
|
+
slugField,
|
|
441
|
+
titleField,
|
|
442
|
+
sortOrder,
|
|
443
|
+
createdBy,
|
|
444
|
+
_auth,
|
|
445
|
+
} = args;
|
|
446
|
+
|
|
447
|
+
// Authorization check - contentTypes.create permission
|
|
448
|
+
requireMutationAuth(_auth, "contentTypes", "create");
|
|
449
|
+
|
|
450
|
+
// Validate content type name format
|
|
451
|
+
if (!isValidName(name)) {
|
|
452
|
+
throw contentTypeNameInvalid(name);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Check if name is already taken (must be unique)
|
|
456
|
+
const existingType = await ctx.db
|
|
457
|
+
.query("contentTypes")
|
|
458
|
+
.withIndex("by_name", (q) => q.eq("name", name))
|
|
459
|
+
.first();
|
|
460
|
+
|
|
461
|
+
if (existingType) {
|
|
462
|
+
throw contentTypeNameDuplicate(name);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Validate field definitions
|
|
466
|
+
const fieldErrors = validateFieldDefinitions(fields as FieldDefinition[]);
|
|
467
|
+
if (fieldErrors.length > 0) {
|
|
468
|
+
throw contentTypeFieldValidationFailed(fieldErrors);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Validate slugField references an existing field if provided
|
|
472
|
+
const fieldNames = fields.map((f) => f.name);
|
|
473
|
+
if (slugField) {
|
|
474
|
+
const slugFieldExists = fields.some((f) => f.name === slugField);
|
|
475
|
+
if (!slugFieldExists) {
|
|
476
|
+
throw contentTypeSlugFieldInvalid(slugField, fieldNames);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Validate titleField references an existing field if provided
|
|
481
|
+
if (titleField) {
|
|
482
|
+
const titleFieldExists = fields.some((f) => f.name === titleField);
|
|
483
|
+
if (!titleFieldExists) {
|
|
484
|
+
throw contentTypeTitleFieldInvalid(titleField, fieldNames);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Insert the new content type
|
|
489
|
+
const id = await ctx.db.insert("contentTypes", {
|
|
490
|
+
name,
|
|
491
|
+
displayName,
|
|
492
|
+
description,
|
|
493
|
+
fields,
|
|
494
|
+
icon,
|
|
495
|
+
singleton,
|
|
496
|
+
slugField,
|
|
497
|
+
titleField,
|
|
498
|
+
sortOrder,
|
|
499
|
+
isActive: true,
|
|
500
|
+
createdBy,
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Retrieve and return the created document
|
|
504
|
+
const created = await ctx.db.get(id);
|
|
505
|
+
if (!created) {
|
|
506
|
+
throw internalError("Failed to retrieve created content type");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Emit content type created event
|
|
510
|
+
await emitEvent(ctx, {
|
|
511
|
+
eventType: contentTypeEventType("created"),
|
|
512
|
+
resourceType: "contentType",
|
|
513
|
+
resourceId: id as unknown as string,
|
|
514
|
+
action: "created",
|
|
515
|
+
payload: {
|
|
516
|
+
name,
|
|
517
|
+
displayName,
|
|
518
|
+
fieldCount: fields.length,
|
|
519
|
+
isActive: true,
|
|
520
|
+
} as ContentTypeEventPayload,
|
|
521
|
+
userId: createdBy,
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
return created;
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// =============================================================================
|
|
529
|
+
// Update Content Type Mutation
|
|
530
|
+
// =============================================================================
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Validator for breaking change information returned by the mutation.
|
|
534
|
+
*/
|
|
535
|
+
const breakingChangeValidator = v.object({
|
|
536
|
+
type: v.union(
|
|
537
|
+
v.literal("FIELD_REMOVED"),
|
|
538
|
+
v.literal("FIELD_TYPE_CHANGED"),
|
|
539
|
+
v.literal("FIELD_MADE_REQUIRED"),
|
|
540
|
+
v.literal("SELECT_OPTIONS_REMOVED"),
|
|
541
|
+
v.literal("REFERENCE_TYPES_RESTRICTED"),
|
|
542
|
+
v.literal("VALIDATION_TIGHTENED")
|
|
543
|
+
),
|
|
544
|
+
fieldName: v.string(),
|
|
545
|
+
message: v.string(),
|
|
546
|
+
affectedEntriesCount: v.number(),
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Extended return type that includes breaking change warnings.
|
|
551
|
+
*/
|
|
552
|
+
const updateContentTypeResult = v.object({
|
|
553
|
+
...contentTypeDoc.fields,
|
|
554
|
+
/** Breaking changes that were detected (only populated if force=true was used) */
|
|
555
|
+
breakingChanges: v.optional(v.array(breakingChangeValidator)),
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Mutation to update an existing content type's fields and configuration.
|
|
560
|
+
*
|
|
561
|
+
* Includes validation to prevent breaking changes to fields with existing content.
|
|
562
|
+
* When breaking changes are detected and `force` is not set to true, the mutation
|
|
563
|
+
* will throw an error with details about the breaking changes.
|
|
564
|
+
*
|
|
565
|
+
* **Breaking Change Detection:**
|
|
566
|
+
* - Removing fields that have data in existing entries
|
|
567
|
+
* - Changing field types (e.g., text → number)
|
|
568
|
+
* - Making optional fields required when entries have empty values
|
|
569
|
+
* - Removing select/multiSelect options that are in use
|
|
570
|
+
* - Restricting allowed reference content types
|
|
571
|
+
* - Tightening validation rules (increased minLength, decreased maxLength)
|
|
572
|
+
*
|
|
573
|
+
* @param id - The content type ID to update
|
|
574
|
+
* @param displayName - Optional new display name
|
|
575
|
+
* @param description - Optional new description
|
|
576
|
+
* @param fields - Optional new field definitions (replaces all existing fields)
|
|
577
|
+
* @param icon - Optional new icon
|
|
578
|
+
* @param singleton - Optional singleton flag
|
|
579
|
+
* @param slugField - Optional field name for slug generation
|
|
580
|
+
* @param titleField - Optional field name for display title
|
|
581
|
+
* @param sortOrder - Optional new sort order
|
|
582
|
+
* @param isActive - Optional active status
|
|
583
|
+
* @param updatedBy - User ID making the update (for audit trail)
|
|
584
|
+
* @param force - If true, allow breaking changes (default: false)
|
|
585
|
+
*
|
|
586
|
+
* @returns The updated content type, with breakingChanges if force was used
|
|
587
|
+
*
|
|
588
|
+
* @throws Error if the content type does not exist
|
|
589
|
+
* @throws Error if breaking changes are detected and force is not true
|
|
590
|
+
* @throws Error if field definitions are invalid
|
|
591
|
+
*
|
|
592
|
+
* @example
|
|
593
|
+
* ```typescript
|
|
594
|
+
* // Simple update (no breaking changes)
|
|
595
|
+
* const updated = await ctx.runMutation(api.contentTypeMutations.updateContentType, {
|
|
596
|
+
* id: contentTypeId,
|
|
597
|
+
* displayName: "Updated Blog Post",
|
|
598
|
+
* description: "New description",
|
|
599
|
+
* updatedBy: currentUserId,
|
|
600
|
+
* });
|
|
601
|
+
*
|
|
602
|
+
* // Update fields (will check for breaking changes)
|
|
603
|
+
* const updated = await ctx.runMutation(api.contentTypeMutations.updateContentType, {
|
|
604
|
+
* id: contentTypeId,
|
|
605
|
+
* fields: [
|
|
606
|
+
* { name: "title", label: "Title", type: "text", required: true },
|
|
607
|
+
* { name: "content", label: "Content", type: "richText", required: true },
|
|
608
|
+
* { name: "author", label: "Author", type: "text", required: false }, // New field
|
|
609
|
+
* ],
|
|
610
|
+
* updatedBy: currentUserId,
|
|
611
|
+
* });
|
|
612
|
+
*
|
|
613
|
+
* // Force update with breaking changes
|
|
614
|
+
* const updated = await ctx.runMutation(api.contentTypeMutations.updateContentType, {
|
|
615
|
+
* id: contentTypeId,
|
|
616
|
+
* fields: newFields,
|
|
617
|
+
* force: true, // Acknowledge potential data loss
|
|
618
|
+
* updatedBy: currentUserId,
|
|
619
|
+
* });
|
|
620
|
+
* ```
|
|
621
|
+
*/
|
|
622
|
+
export const updateContentType = mutation({
|
|
623
|
+
args: {
|
|
624
|
+
...updateContentTypeArgs.fields,
|
|
625
|
+
/** If true, allow breaking changes that may affect existing content entries */
|
|
626
|
+
force: v.optional(v.boolean()),
|
|
627
|
+
/** Optional auth context for mutation-level authorization */
|
|
628
|
+
_auth: v.optional(mutationAuthContext),
|
|
629
|
+
},
|
|
630
|
+
returns: updateContentTypeResult,
|
|
631
|
+
handler: async (ctx, args) => {
|
|
632
|
+
const {
|
|
633
|
+
id,
|
|
634
|
+
displayName,
|
|
635
|
+
description,
|
|
636
|
+
fields,
|
|
637
|
+
icon,
|
|
638
|
+
singleton,
|
|
639
|
+
slugField,
|
|
640
|
+
titleField,
|
|
641
|
+
sortOrder,
|
|
642
|
+
isActive,
|
|
643
|
+
updatedBy,
|
|
644
|
+
force = false,
|
|
645
|
+
_auth,
|
|
646
|
+
} = args;
|
|
647
|
+
|
|
648
|
+
// Authorization check - contentTypes.update permission
|
|
649
|
+
requireMutationAuth(_auth, "contentTypes", "update");
|
|
650
|
+
|
|
651
|
+
const existingType = await ctx.db.get(id);
|
|
652
|
+
if (!existingType) {
|
|
653
|
+
throw contentTypeNotFound(id as unknown as string);
|
|
654
|
+
}
|
|
655
|
+
if (isDeleted(existingType)) {
|
|
656
|
+
throw contentTypeDeleted(id as unknown as string, existingType.name);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Build the update object with only provided fields
|
|
660
|
+
const updates: Record<string, unknown> = {
|
|
661
|
+
updatedBy,
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
// Handle simple field updates
|
|
665
|
+
if (displayName !== undefined) {
|
|
666
|
+
updates.displayName = displayName;
|
|
667
|
+
}
|
|
668
|
+
if (description !== undefined) {
|
|
669
|
+
updates.description = description;
|
|
670
|
+
}
|
|
671
|
+
if (icon !== undefined) {
|
|
672
|
+
updates.icon = icon;
|
|
673
|
+
}
|
|
674
|
+
if (singleton !== undefined) {
|
|
675
|
+
updates.singleton = singleton;
|
|
676
|
+
}
|
|
677
|
+
if (sortOrder !== undefined) {
|
|
678
|
+
updates.sortOrder = sortOrder;
|
|
679
|
+
}
|
|
680
|
+
if (isActive !== undefined) {
|
|
681
|
+
updates.isActive = isActive;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Track breaking changes if fields are being updated
|
|
685
|
+
let detectedBreakingChanges: BreakingChange[] = [];
|
|
686
|
+
|
|
687
|
+
// Handle field updates with breaking change detection
|
|
688
|
+
if (fields !== undefined) {
|
|
689
|
+
// Validate the new field definitions
|
|
690
|
+
const fieldErrors = validateFieldDefinitions(fields as FieldDefinition[]);
|
|
691
|
+
if (fieldErrors.length > 0) {
|
|
692
|
+
throw contentTypeFieldValidationFailed(fieldErrors);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Get all existing content entries for this content type
|
|
696
|
+
const existingEntries = await ctx.db
|
|
697
|
+
.query("contentEntries")
|
|
698
|
+
.withIndex("by_content_type", (q) => q.eq("contentTypeId", id))
|
|
699
|
+
.filter((q) => q.eq(q.field("deletedAt"), undefined))
|
|
700
|
+
.collect();
|
|
701
|
+
|
|
702
|
+
// Only check for breaking changes if there are existing entries
|
|
703
|
+
if (existingEntries.length > 0) {
|
|
704
|
+
detectedBreakingChanges = detectBreakingChanges(
|
|
705
|
+
existingType.fields as FieldDefinition[],
|
|
706
|
+
fields as FieldDefinition[],
|
|
707
|
+
existingEntries.map((e) => ({ data: e.data as Record<string, unknown> }))
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
// If breaking changes detected and force is not true, throw error
|
|
711
|
+
if (detectedBreakingChanges.length > 0 && !force) {
|
|
712
|
+
throw contentTypeBreakingChange(detectedBreakingChanges);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
updates.fields = fields;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Validate slugField references an existing field if provided
|
|
720
|
+
const effectiveFields = (fields ?? existingType.fields) as FieldDefinition[];
|
|
721
|
+
const effectiveSlugField = slugField !== undefined ? slugField : existingType.slugField;
|
|
722
|
+
const effectiveTitleField = titleField !== undefined ? titleField : existingType.titleField;
|
|
723
|
+
|
|
724
|
+
const availableFieldNames = effectiveFields.map((f) => f.name);
|
|
725
|
+
|
|
726
|
+
if (effectiveSlugField) {
|
|
727
|
+
const slugFieldExists = effectiveFields.some((f) => f.name === effectiveSlugField);
|
|
728
|
+
if (!slugFieldExists) {
|
|
729
|
+
throw contentTypeSlugFieldInvalid(effectiveSlugField, availableFieldNames);
|
|
730
|
+
}
|
|
731
|
+
if (slugField !== undefined) {
|
|
732
|
+
updates.slugField = slugField;
|
|
733
|
+
}
|
|
734
|
+
} else if (slugField !== undefined) {
|
|
735
|
+
updates.slugField = slugField;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (effectiveTitleField) {
|
|
739
|
+
const titleFieldExists = effectiveFields.some((f) => f.name === effectiveTitleField);
|
|
740
|
+
if (!titleFieldExists) {
|
|
741
|
+
throw contentTypeTitleFieldInvalid(effectiveTitleField, availableFieldNames);
|
|
742
|
+
}
|
|
743
|
+
if (titleField !== undefined) {
|
|
744
|
+
updates.titleField = titleField;
|
|
745
|
+
}
|
|
746
|
+
} else if (titleField !== undefined) {
|
|
747
|
+
updates.titleField = titleField;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Apply the updates
|
|
751
|
+
await ctx.db.patch(id, updates);
|
|
752
|
+
|
|
753
|
+
// Retrieve and return the updated document
|
|
754
|
+
const updated = await ctx.db.get(id);
|
|
755
|
+
if (!updated) {
|
|
756
|
+
throw internalError("Failed to retrieve updated content type");
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Emit content type updated event
|
|
760
|
+
const changedFields = Object.keys(updates).filter((k) => k !== "updatedBy");
|
|
761
|
+
await emitEvent(ctx, {
|
|
762
|
+
eventType: contentTypeEventType("updated"),
|
|
763
|
+
resourceType: "contentType",
|
|
764
|
+
resourceId: id as unknown as string,
|
|
765
|
+
action: "updated",
|
|
766
|
+
payload: {
|
|
767
|
+
name: updated.name,
|
|
768
|
+
displayName: updated.displayName,
|
|
769
|
+
fieldCount: updated.fields.length,
|
|
770
|
+
isActive: updated.isActive,
|
|
771
|
+
changedFields,
|
|
772
|
+
} as ContentTypeEventPayload,
|
|
773
|
+
userId: updatedBy,
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// Include breaking changes in the result if force was used
|
|
777
|
+
return {
|
|
778
|
+
...updated,
|
|
779
|
+
breakingChanges:
|
|
780
|
+
detectedBreakingChanges.length > 0 ? detectedBreakingChanges : undefined,
|
|
781
|
+
};
|
|
782
|
+
},
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
// =============================================================================
|
|
786
|
+
// Delete Content Type Mutation
|
|
787
|
+
// =============================================================================
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Result type for the delete content type mutation.
|
|
791
|
+
* Includes information about any cascade-deleted entries.
|
|
792
|
+
*/
|
|
793
|
+
const deleteContentTypeResult = v.object({
|
|
794
|
+
/** Whether the deletion was successful */
|
|
795
|
+
success: v.boolean(),
|
|
796
|
+
/** The ID of the deleted content type */
|
|
797
|
+
deletedId: v.id("contentTypes"),
|
|
798
|
+
/** Number of content entries that were deleted (when cascade=true) */
|
|
799
|
+
deletedEntriesCount: v.number(),
|
|
800
|
+
/** Number of content versions that were deleted (when cascade=true and hardDelete=true) */
|
|
801
|
+
deletedVersionsCount: v.number(),
|
|
802
|
+
/** Whether this was a hard delete (permanent) or soft delete */
|
|
803
|
+
wasHardDelete: v.boolean(),
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Mutation to delete a content type.
|
|
808
|
+
*
|
|
809
|
+
* Supports two deletion strategies via the `cascade` flag:
|
|
810
|
+
* 1. **Cascade delete** (`cascade: true`): Deletes all content entries of this type
|
|
811
|
+
* before deleting the content type itself.
|
|
812
|
+
* 2. **Prevent if entries exist** (`cascade: false` or not specified): Fails the
|
|
813
|
+
* deletion if any content entries exist for this type.
|
|
814
|
+
*
|
|
815
|
+
* Also supports two deletion modes via the `hardDelete` flag:
|
|
816
|
+
* - **Soft delete** (default): Sets `deletedAt` timestamp, entries remain in database
|
|
817
|
+
* - **Hard delete** (`hardDelete: true`): Permanently removes from database
|
|
818
|
+
*
|
|
819
|
+
* @param id - The content type ID to delete
|
|
820
|
+
* @param cascade - If true, delete all entries of this type first. Default: false
|
|
821
|
+
* @param hardDelete - If true, permanently delete. Default: false (soft delete)
|
|
822
|
+
* @param deletedBy - User ID performing the deletion (for audit trail)
|
|
823
|
+
*
|
|
824
|
+
* @returns Object with deletion results including counts of deleted entries/versions
|
|
825
|
+
*
|
|
826
|
+
* @throws Error if content type does not exist
|
|
827
|
+
* @throws Error if content type is already deleted (soft deleted)
|
|
828
|
+
* @throws Error if cascade is false and content entries exist
|
|
829
|
+
*
|
|
830
|
+
* @example
|
|
831
|
+
* ```typescript
|
|
832
|
+
* // Soft delete - fails if entries exist
|
|
833
|
+
* const result = await ctx.runMutation(api.contentTypeMutations.deleteContentType, {
|
|
834
|
+
* id: contentTypeId,
|
|
835
|
+
* deletedBy: currentUserId,
|
|
836
|
+
* });
|
|
837
|
+
*
|
|
838
|
+
* // Cascade soft delete - deletes all entries too
|
|
839
|
+
* const result = await ctx.runMutation(api.contentTypeMutations.deleteContentType, {
|
|
840
|
+
* id: contentTypeId,
|
|
841
|
+
* cascade: true,
|
|
842
|
+
* deletedBy: currentUserId,
|
|
843
|
+
* });
|
|
844
|
+
*
|
|
845
|
+
* // Hard delete with cascade - permanently removes everything
|
|
846
|
+
* const result = await ctx.runMutation(api.contentTypeMutations.deleteContentType, {
|
|
847
|
+
* id: contentTypeId,
|
|
848
|
+
* cascade: true,
|
|
849
|
+
* hardDelete: true,
|
|
850
|
+
* deletedBy: currentUserId,
|
|
851
|
+
* });
|
|
852
|
+
* ```
|
|
853
|
+
*/
|
|
854
|
+
export const deleteContentType = mutation({
|
|
855
|
+
args: {
|
|
856
|
+
...deleteContentTypeArgs.fields,
|
|
857
|
+
/** Optional auth context for mutation-level authorization */
|
|
858
|
+
_auth: v.optional(mutationAuthContext),
|
|
859
|
+
},
|
|
860
|
+
returns: deleteContentTypeResult,
|
|
861
|
+
handler: async (ctx, args) => {
|
|
862
|
+
const { id, cascade = false, hardDelete = false, deletedBy, _auth } = args;
|
|
863
|
+
|
|
864
|
+
// Authorization check - contentTypes.delete permission
|
|
865
|
+
requireMutationAuth(_auth, "contentTypes", "delete");
|
|
866
|
+
|
|
867
|
+
const contentType = await ctx.db.get(id);
|
|
868
|
+
if (!contentType) {
|
|
869
|
+
throw contentTypeNotFound(id as unknown as string);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Check if already soft-deleted
|
|
873
|
+
if (isDeleted(contentType)) {
|
|
874
|
+
throw contentTypeDeleted(id as unknown as string, contentType.name);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Get all content entries for this type (excluding already soft-deleted ones)
|
|
878
|
+
const existingEntries = await ctx.db
|
|
879
|
+
.query("contentEntries")
|
|
880
|
+
.withIndex("by_content_type", (q) => q.eq("contentTypeId", id))
|
|
881
|
+
.filter((q) => q.eq(q.field("deletedAt"), undefined))
|
|
882
|
+
.collect();
|
|
883
|
+
|
|
884
|
+
const entryCount = existingEntries.length;
|
|
885
|
+
|
|
886
|
+
// If entries exist and cascade is false, prevent deletion
|
|
887
|
+
if (entryCount > 0 && !cascade) {
|
|
888
|
+
throw contentTypeHasEntries(id as unknown as string, contentType.name, entryCount);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
let deletedEntriesCount = 0;
|
|
892
|
+
let deletedVersionsCount = 0;
|
|
893
|
+
const now = Date.now();
|
|
894
|
+
|
|
895
|
+
// If cascade is true, delete all entries first
|
|
896
|
+
if (cascade && entryCount > 0) {
|
|
897
|
+
if (hardDelete) {
|
|
898
|
+
// Hard delete: permanently remove entries and their versions
|
|
899
|
+
for (const entry of existingEntries) {
|
|
900
|
+
// Delete all versions for this entry
|
|
901
|
+
const versions = await ctx.db
|
|
902
|
+
.query("contentVersions")
|
|
903
|
+
.withIndex("by_entry", (q) => q.eq("entryId", entry._id))
|
|
904
|
+
.collect();
|
|
905
|
+
|
|
906
|
+
for (const version of versions) {
|
|
907
|
+
await ctx.db.delete(version._id);
|
|
908
|
+
deletedVersionsCount++;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Delete the entry
|
|
912
|
+
await ctx.db.delete(entry._id);
|
|
913
|
+
deletedEntriesCount++;
|
|
914
|
+
}
|
|
915
|
+
} else {
|
|
916
|
+
// Soft delete: set deletedAt on all entries
|
|
917
|
+
for (const entry of existingEntries) {
|
|
918
|
+
await ctx.db.patch(entry._id, {
|
|
919
|
+
deletedAt: now,
|
|
920
|
+
updatedBy: deletedBy,
|
|
921
|
+
});
|
|
922
|
+
deletedEntriesCount++;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Delete the content type itself
|
|
928
|
+
if (hardDelete) {
|
|
929
|
+
// Hard delete: permanently remove
|
|
930
|
+
await ctx.db.delete(id);
|
|
931
|
+
} else {
|
|
932
|
+
// Soft delete: set deletedAt
|
|
933
|
+
await ctx.db.patch(id, {
|
|
934
|
+
deletedAt: now,
|
|
935
|
+
isActive: false,
|
|
936
|
+
updatedBy: deletedBy,
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Emit content type deleted event
|
|
941
|
+
await emitEvent(ctx, {
|
|
942
|
+
eventType: contentTypeEventType("deleted"),
|
|
943
|
+
resourceType: "contentType",
|
|
944
|
+
resourceId: id as unknown as string,
|
|
945
|
+
action: "deleted",
|
|
946
|
+
payload: {
|
|
947
|
+
name: contentType.name,
|
|
948
|
+
displayName: contentType.displayName,
|
|
949
|
+
fieldCount: contentType.fields.length,
|
|
950
|
+
isActive: false,
|
|
951
|
+
} as ContentTypeEventPayload,
|
|
952
|
+
userId: deletedBy,
|
|
953
|
+
metadata: {
|
|
954
|
+
hardDelete,
|
|
955
|
+
cascade,
|
|
956
|
+
deletedEntriesCount,
|
|
957
|
+
deletedVersionsCount,
|
|
958
|
+
},
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
success: true,
|
|
963
|
+
deletedId: id,
|
|
964
|
+
deletedEntriesCount,
|
|
965
|
+
deletedVersionsCount,
|
|
966
|
+
wasHardDelete: hardDelete,
|
|
967
|
+
};
|
|
968
|
+
},
|
|
969
|
+
});
|