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,1001 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Taxonomy Mutation Functions
|
|
3
|
+
*
|
|
4
|
+
* Provides mutation functions for managing taxonomies and terms.
|
|
5
|
+
*
|
|
6
|
+
* Available mutations:
|
|
7
|
+
* - Taxonomies: create, update, delete (soft), restore
|
|
8
|
+
* - Terms: create, update, delete (soft), restore, reorder
|
|
9
|
+
* - Entry Tags: setEntryTerms, addTermToEntry, removeTermFromEntry
|
|
10
|
+
*/
|
|
11
|
+
import { v } from "convex/values";
|
|
12
|
+
import { isDeleted } from "./lib/softDelete.js";
|
|
13
|
+
import { mutation } from "./_generated/server.js";
|
|
14
|
+
import { generateSlug } from "./lib/slugGenerator.js";
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// Helper Functions
|
|
17
|
+
// =============================================================================
|
|
18
|
+
/**
|
|
19
|
+
* Build the full path for a term in a hierarchy.
|
|
20
|
+
*/
|
|
21
|
+
async function buildTermPath(ctx, parentId, slug) {
|
|
22
|
+
if (!parentId) {
|
|
23
|
+
return `/${slug}`;
|
|
24
|
+
}
|
|
25
|
+
const parent = await ctx.db.get(parentId);
|
|
26
|
+
if (!parent || isDeleted(parent)) {
|
|
27
|
+
return `/${slug}`;
|
|
28
|
+
}
|
|
29
|
+
const parentPath = parent.path ?? `/${parent.slug}`;
|
|
30
|
+
return `${parentPath}/${slug}`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Calculate depth based on parent.
|
|
34
|
+
*/
|
|
35
|
+
async function calculateDepth(ctx, parentId) {
|
|
36
|
+
if (!parentId) {
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
const parent = await ctx.db.get(parentId);
|
|
40
|
+
if (!parent || isDeleted(parent)) {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
return parent.depth + 1;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Update all descendant paths when a term is moved.
|
|
47
|
+
*/
|
|
48
|
+
async function updateDescendantPaths(ctx, termId, oldPath, newPath) {
|
|
49
|
+
// Get all terms that start with the old path
|
|
50
|
+
const descendants = await ctx.db
|
|
51
|
+
.query("taxonomyTerms")
|
|
52
|
+
.filter((q) => q.gte(q.field("path"), oldPath))
|
|
53
|
+
.collect();
|
|
54
|
+
for (const desc of descendants) {
|
|
55
|
+
if (desc._id !== termId && desc.path?.startsWith(oldPath + "/")) {
|
|
56
|
+
const updatedPath = desc.path.replace(oldPath, newPath);
|
|
57
|
+
const updatedDepth = updatedPath.split("/").filter((p) => p).length - 1;
|
|
58
|
+
await ctx.db.patch(desc._id, {
|
|
59
|
+
path: updatedPath,
|
|
60
|
+
depth: updatedDepth,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// Taxonomy Mutations
|
|
67
|
+
// =============================================================================
|
|
68
|
+
/**
|
|
69
|
+
* Create a new taxonomy.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const taxonomyId = await ctx.runMutation(api.taxonomyMutations.createTaxonomy, {
|
|
74
|
+
* name: "tags",
|
|
75
|
+
* displayName: "Tags",
|
|
76
|
+
* isHierarchical: false,
|
|
77
|
+
* allowInlineCreation: true,
|
|
78
|
+
* });
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export const createTaxonomy = mutation({
|
|
82
|
+
args: {
|
|
83
|
+
name: v.string(),
|
|
84
|
+
displayName: v.string(),
|
|
85
|
+
description: v.optional(v.string()),
|
|
86
|
+
isHierarchical: v.boolean(),
|
|
87
|
+
allowInlineCreation: v.boolean(),
|
|
88
|
+
icon: v.optional(v.string()),
|
|
89
|
+
sortOrder: v.optional(v.number()),
|
|
90
|
+
userId: v.optional(v.string()),
|
|
91
|
+
},
|
|
92
|
+
returns: v.id("taxonomies"),
|
|
93
|
+
handler: async (ctx, args) => {
|
|
94
|
+
const { name, displayName, description, isHierarchical, allowInlineCreation, icon, sortOrder, userId, } = args;
|
|
95
|
+
// Check for duplicate name
|
|
96
|
+
const existing = await ctx.db
|
|
97
|
+
.query("taxonomies")
|
|
98
|
+
.withIndex("by_name", (q) => q.eq("name", name))
|
|
99
|
+
.first();
|
|
100
|
+
if (existing && !isDeleted(existing)) {
|
|
101
|
+
throw new Error(`Taxonomy with name "${name}" already exists`);
|
|
102
|
+
}
|
|
103
|
+
// If there was a soft-deleted taxonomy with this name, restore and update it
|
|
104
|
+
if (existing) {
|
|
105
|
+
await ctx.db.patch(existing._id, {
|
|
106
|
+
displayName,
|
|
107
|
+
description,
|
|
108
|
+
isHierarchical,
|
|
109
|
+
allowInlineCreation,
|
|
110
|
+
icon,
|
|
111
|
+
sortOrder,
|
|
112
|
+
isActive: true,
|
|
113
|
+
deletedAt: undefined,
|
|
114
|
+
updatedBy: userId,
|
|
115
|
+
});
|
|
116
|
+
return existing._id;
|
|
117
|
+
}
|
|
118
|
+
const taxonomyId = await ctx.db.insert("taxonomies", {
|
|
119
|
+
name,
|
|
120
|
+
displayName,
|
|
121
|
+
description,
|
|
122
|
+
isHierarchical,
|
|
123
|
+
allowInlineCreation,
|
|
124
|
+
icon,
|
|
125
|
+
sortOrder,
|
|
126
|
+
isActive: true,
|
|
127
|
+
createdBy: userId,
|
|
128
|
+
});
|
|
129
|
+
return taxonomyId;
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
/**
|
|
133
|
+
* Update an existing taxonomy.
|
|
134
|
+
*/
|
|
135
|
+
export const updateTaxonomy = mutation({
|
|
136
|
+
args: {
|
|
137
|
+
id: v.id("taxonomies"),
|
|
138
|
+
displayName: v.optional(v.string()),
|
|
139
|
+
description: v.optional(v.string()),
|
|
140
|
+
allowInlineCreation: v.optional(v.boolean()),
|
|
141
|
+
icon: v.optional(v.string()),
|
|
142
|
+
sortOrder: v.optional(v.number()),
|
|
143
|
+
isActive: v.optional(v.boolean()),
|
|
144
|
+
userId: v.optional(v.string()),
|
|
145
|
+
},
|
|
146
|
+
returns: v.id("taxonomies"),
|
|
147
|
+
handler: async (ctx, args) => {
|
|
148
|
+
const { id, userId, ...updates } = args;
|
|
149
|
+
const taxonomy = await ctx.db.get(id);
|
|
150
|
+
if (!taxonomy) {
|
|
151
|
+
throw new Error("Taxonomy not found");
|
|
152
|
+
}
|
|
153
|
+
if (isDeleted(taxonomy)) {
|
|
154
|
+
throw new Error("Cannot update deleted taxonomy");
|
|
155
|
+
}
|
|
156
|
+
// Build update object
|
|
157
|
+
const updateFields = { updatedBy: userId };
|
|
158
|
+
if (updates.displayName !== undefined)
|
|
159
|
+
updateFields.displayName = updates.displayName;
|
|
160
|
+
if (updates.description !== undefined)
|
|
161
|
+
updateFields.description = updates.description;
|
|
162
|
+
if (updates.allowInlineCreation !== undefined)
|
|
163
|
+
updateFields.allowInlineCreation = updates.allowInlineCreation;
|
|
164
|
+
if (updates.icon !== undefined)
|
|
165
|
+
updateFields.icon = updates.icon;
|
|
166
|
+
if (updates.sortOrder !== undefined)
|
|
167
|
+
updateFields.sortOrder = updates.sortOrder;
|
|
168
|
+
if (updates.isActive !== undefined)
|
|
169
|
+
updateFields.isActive = updates.isActive;
|
|
170
|
+
await ctx.db.patch(id, updateFields);
|
|
171
|
+
return id;
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
/**
|
|
175
|
+
* Soft delete a taxonomy.
|
|
176
|
+
*/
|
|
177
|
+
export const deleteTaxonomy = mutation({
|
|
178
|
+
args: {
|
|
179
|
+
id: v.id("taxonomies"),
|
|
180
|
+
userId: v.optional(v.string()),
|
|
181
|
+
},
|
|
182
|
+
returns: v.null(),
|
|
183
|
+
handler: async (ctx, args) => {
|
|
184
|
+
const { id, userId } = args;
|
|
185
|
+
const taxonomy = await ctx.db.get(id);
|
|
186
|
+
if (!taxonomy) {
|
|
187
|
+
throw new Error("Taxonomy not found");
|
|
188
|
+
}
|
|
189
|
+
if (isDeleted(taxonomy)) {
|
|
190
|
+
return null; // Already deleted
|
|
191
|
+
}
|
|
192
|
+
// Soft delete the taxonomy
|
|
193
|
+
await ctx.db.patch(id, {
|
|
194
|
+
deletedAt: Date.now(),
|
|
195
|
+
isActive: false,
|
|
196
|
+
updatedBy: userId,
|
|
197
|
+
});
|
|
198
|
+
// Also soft delete all terms in this taxonomy
|
|
199
|
+
const terms = await ctx.db
|
|
200
|
+
.query("taxonomyTerms")
|
|
201
|
+
.withIndex("by_taxonomy", (q) => q.eq("taxonomyId", id))
|
|
202
|
+
.collect();
|
|
203
|
+
for (const term of terms) {
|
|
204
|
+
if (!isDeleted(term)) {
|
|
205
|
+
await ctx.db.patch(term._id, {
|
|
206
|
+
deletedAt: Date.now(),
|
|
207
|
+
updatedBy: userId,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
/**
|
|
215
|
+
* Restore a soft-deleted taxonomy.
|
|
216
|
+
*/
|
|
217
|
+
export const restoreTaxonomy = mutation({
|
|
218
|
+
args: {
|
|
219
|
+
id: v.id("taxonomies"),
|
|
220
|
+
userId: v.optional(v.string()),
|
|
221
|
+
},
|
|
222
|
+
returns: v.id("taxonomies"),
|
|
223
|
+
handler: async (ctx, args) => {
|
|
224
|
+
const { id, userId } = args;
|
|
225
|
+
const taxonomy = await ctx.db.get(id);
|
|
226
|
+
if (!taxonomy) {
|
|
227
|
+
throw new Error("Taxonomy not found");
|
|
228
|
+
}
|
|
229
|
+
if (!isDeleted(taxonomy)) {
|
|
230
|
+
return id; // Not deleted
|
|
231
|
+
}
|
|
232
|
+
await ctx.db.patch(id, {
|
|
233
|
+
deletedAt: undefined,
|
|
234
|
+
isActive: true,
|
|
235
|
+
updatedBy: userId,
|
|
236
|
+
});
|
|
237
|
+
return id;
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
// =============================================================================
|
|
241
|
+
// Term Mutations
|
|
242
|
+
// =============================================================================
|
|
243
|
+
/**
|
|
244
|
+
* Create a new taxonomy term.
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```typescript
|
|
248
|
+
* // Create a flat tag
|
|
249
|
+
* const tagId = await ctx.runMutation(api.taxonomyMutations.createTerm, {
|
|
250
|
+
* taxonomyId: tagsTaxonomyId,
|
|
251
|
+
* name: "JavaScript",
|
|
252
|
+
* });
|
|
253
|
+
*
|
|
254
|
+
* // Create a hierarchical category
|
|
255
|
+
* const categoryId = await ctx.runMutation(api.taxonomyMutations.createTerm, {
|
|
256
|
+
* taxonomyId: categoriesTaxonomyId,
|
|
257
|
+
* name: "Web Development",
|
|
258
|
+
* parentId: techCategoryId,
|
|
259
|
+
* });
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
export const createTerm = mutation({
|
|
263
|
+
args: {
|
|
264
|
+
taxonomyId: v.id("taxonomies"),
|
|
265
|
+
name: v.string(),
|
|
266
|
+
slug: v.optional(v.string()),
|
|
267
|
+
description: v.optional(v.string()),
|
|
268
|
+
parentId: v.optional(v.id("taxonomyTerms")),
|
|
269
|
+
color: v.optional(v.string()),
|
|
270
|
+
icon: v.optional(v.string()),
|
|
271
|
+
sortOrder: v.optional(v.number()),
|
|
272
|
+
userId: v.optional(v.string()),
|
|
273
|
+
},
|
|
274
|
+
returns: v.id("taxonomyTerms"),
|
|
275
|
+
handler: async (ctx, args) => {
|
|
276
|
+
const { taxonomyId, name, description, parentId, color, icon, sortOrder, userId, } = args;
|
|
277
|
+
// Verify taxonomy exists
|
|
278
|
+
const taxonomy = await ctx.db.get(taxonomyId);
|
|
279
|
+
if (!taxonomy || isDeleted(taxonomy)) {
|
|
280
|
+
throw new Error("Taxonomy not found");
|
|
281
|
+
}
|
|
282
|
+
// Check if hierarchy is allowed
|
|
283
|
+
if (parentId && !taxonomy.isHierarchical) {
|
|
284
|
+
throw new Error("Cannot create nested terms in a flat taxonomy");
|
|
285
|
+
}
|
|
286
|
+
// Verify parent exists if specified
|
|
287
|
+
if (parentId) {
|
|
288
|
+
const parent = await ctx.db.get(parentId);
|
|
289
|
+
if (!parent || isDeleted(parent)) {
|
|
290
|
+
throw new Error("Parent term not found");
|
|
291
|
+
}
|
|
292
|
+
if (parent.taxonomyId !== taxonomyId) {
|
|
293
|
+
throw new Error("Parent term belongs to a different taxonomy");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Generate or validate slug
|
|
297
|
+
const slug = args.slug || generateSlug(name);
|
|
298
|
+
// Check for duplicate slug in taxonomy
|
|
299
|
+
const existing = await ctx.db
|
|
300
|
+
.query("taxonomyTerms")
|
|
301
|
+
.withIndex("by_taxonomy_and_slug", (q) => q.eq("taxonomyId", taxonomyId).eq("slug", slug))
|
|
302
|
+
.first();
|
|
303
|
+
if (existing && !isDeleted(existing)) {
|
|
304
|
+
throw new Error(`Term with slug "${slug}" already exists in this taxonomy`);
|
|
305
|
+
}
|
|
306
|
+
// Calculate path and depth
|
|
307
|
+
const path = await buildTermPath(ctx, parentId, slug);
|
|
308
|
+
const depth = await calculateDepth(ctx, parentId);
|
|
309
|
+
// Build searchText for search index
|
|
310
|
+
const searchText = [name, description].filter(Boolean).join(" ");
|
|
311
|
+
const termId = await ctx.db.insert("taxonomyTerms", {
|
|
312
|
+
taxonomyId,
|
|
313
|
+
slug,
|
|
314
|
+
name,
|
|
315
|
+
description,
|
|
316
|
+
parentId,
|
|
317
|
+
path,
|
|
318
|
+
depth,
|
|
319
|
+
color,
|
|
320
|
+
icon,
|
|
321
|
+
sortOrder,
|
|
322
|
+
usageCount: 0,
|
|
323
|
+
searchText,
|
|
324
|
+
createdBy: userId,
|
|
325
|
+
});
|
|
326
|
+
return termId;
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
/**
|
|
330
|
+
* Update an existing term.
|
|
331
|
+
*/
|
|
332
|
+
export const updateTerm = mutation({
|
|
333
|
+
args: {
|
|
334
|
+
id: v.id("taxonomyTerms"),
|
|
335
|
+
name: v.optional(v.string()),
|
|
336
|
+
slug: v.optional(v.string()),
|
|
337
|
+
description: v.optional(v.string()),
|
|
338
|
+
parentId: v.optional(v.union(v.id("taxonomyTerms"), v.null())),
|
|
339
|
+
color: v.optional(v.string()),
|
|
340
|
+
icon: v.optional(v.string()),
|
|
341
|
+
sortOrder: v.optional(v.number()),
|
|
342
|
+
userId: v.optional(v.string()),
|
|
343
|
+
},
|
|
344
|
+
returns: v.id("taxonomyTerms"),
|
|
345
|
+
handler: async (ctx, args) => {
|
|
346
|
+
const { id, userId, ...updates } = args;
|
|
347
|
+
const term = await ctx.db.get(id);
|
|
348
|
+
if (!term) {
|
|
349
|
+
throw new Error("Term not found");
|
|
350
|
+
}
|
|
351
|
+
if (isDeleted(term)) {
|
|
352
|
+
throw new Error("Cannot update deleted term");
|
|
353
|
+
}
|
|
354
|
+
const taxonomy = await ctx.db.get(term.taxonomyId);
|
|
355
|
+
if (!taxonomy || isDeleted(taxonomy)) {
|
|
356
|
+
throw new Error("Taxonomy not found");
|
|
357
|
+
}
|
|
358
|
+
// Build update object
|
|
359
|
+
const updateFields = { updatedBy: userId };
|
|
360
|
+
if (updates.name !== undefined) {
|
|
361
|
+
updateFields.name = updates.name;
|
|
362
|
+
// Update searchText
|
|
363
|
+
updateFields.searchText = [
|
|
364
|
+
updates.name,
|
|
365
|
+
updates.description ?? term.description,
|
|
366
|
+
]
|
|
367
|
+
.filter(Boolean)
|
|
368
|
+
.join(" ");
|
|
369
|
+
}
|
|
370
|
+
if (updates.description !== undefined) {
|
|
371
|
+
updateFields.description = updates.description;
|
|
372
|
+
if (!updates.name) {
|
|
373
|
+
updateFields.searchText = [term.name, updates.description]
|
|
374
|
+
.filter(Boolean)
|
|
375
|
+
.join(" ");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (updates.color !== undefined)
|
|
379
|
+
updateFields.color = updates.color;
|
|
380
|
+
if (updates.icon !== undefined)
|
|
381
|
+
updateFields.icon = updates.icon;
|
|
382
|
+
if (updates.sortOrder !== undefined)
|
|
383
|
+
updateFields.sortOrder = updates.sortOrder;
|
|
384
|
+
// Handle slug change
|
|
385
|
+
if (updates.slug !== undefined && updates.slug !== term.slug) {
|
|
386
|
+
// Check for duplicate
|
|
387
|
+
const existing = await ctx.db
|
|
388
|
+
.query("taxonomyTerms")
|
|
389
|
+
.withIndex("by_taxonomy_and_slug", (q) => q.eq("taxonomyId", term.taxonomyId).eq("slug", updates.slug))
|
|
390
|
+
.first();
|
|
391
|
+
if (existing && existing._id !== id && !isDeleted(existing)) {
|
|
392
|
+
throw new Error(`Term with slug "${updates.slug}" already exists`);
|
|
393
|
+
}
|
|
394
|
+
updateFields.slug = updates.slug;
|
|
395
|
+
// Update path for this term and descendants
|
|
396
|
+
const oldPath = term.path ?? `/${term.slug}`;
|
|
397
|
+
const newPath = await buildTermPath(ctx, term.parentId, updates.slug);
|
|
398
|
+
updateFields.path = newPath;
|
|
399
|
+
// Update descendants if this term h
|
|
400
|
+
await updateDescendantPaths(ctx, id, oldPath, newPath);
|
|
401
|
+
}
|
|
402
|
+
// Handle parent change (moving in hierarchy)
|
|
403
|
+
if (updates.parentId !== undefined) {
|
|
404
|
+
const newParentId = updates.parentId === null ? undefined : updates.parentId;
|
|
405
|
+
if (!taxonomy.isHierarchical && newParentId) {
|
|
406
|
+
throw new Error("Cannot create nested terms in a flat taxonomy");
|
|
407
|
+
}
|
|
408
|
+
// Verify new parent if specified
|
|
409
|
+
if (newParentId) {
|
|
410
|
+
const newParent = await ctx.db.get(newParentId);
|
|
411
|
+
if (!newParent || isDeleted(newParent)) {
|
|
412
|
+
throw new Error("New parent term not found");
|
|
413
|
+
}
|
|
414
|
+
if (newParent.taxonomyId !== term.taxonomyId) {
|
|
415
|
+
throw new Error("New parent belongs to a different taxonomy");
|
|
416
|
+
}
|
|
417
|
+
// Check for circular reference
|
|
418
|
+
let current = newParent;
|
|
419
|
+
while (current) {
|
|
420
|
+
if (current._id === id) {
|
|
421
|
+
throw new Error("Cannot move term under its own descendant");
|
|
422
|
+
}
|
|
423
|
+
if (current.parentId) {
|
|
424
|
+
current = await ctx.db.get(current.parentId);
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
updateFields.parentId = newParentId;
|
|
432
|
+
updateFields.depth = await calculateDepth(ctx, newParentId);
|
|
433
|
+
// Update path
|
|
434
|
+
const slug = updates.slug ?? term.slug;
|
|
435
|
+
const oldPath = term.path ?? `/${term.slug}`;
|
|
436
|
+
const newPath = await buildTermPath(ctx, newParentId, slug);
|
|
437
|
+
updateFields.path = newPath;
|
|
438
|
+
// Update descendants
|
|
439
|
+
await updateDescendantPaths(ctx, id, oldPath, newPath);
|
|
440
|
+
}
|
|
441
|
+
await ctx.db.patch(id, updateFields);
|
|
442
|
+
return id;
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
/**
|
|
446
|
+
* Soft delete a term.
|
|
447
|
+
*/
|
|
448
|
+
export const deleteTerm = mutation({
|
|
449
|
+
args: {
|
|
450
|
+
id: v.id("taxonomyTerms"),
|
|
451
|
+
cascade: v.optional(v.boolean()),
|
|
452
|
+
userId: v.optional(v.string()),
|
|
453
|
+
},
|
|
454
|
+
returns: v.null(),
|
|
455
|
+
handler: async (ctx, args) => {
|
|
456
|
+
const { id, cascade = true, userId } = args;
|
|
457
|
+
const term = await ctx.db.get(id);
|
|
458
|
+
if (!term) {
|
|
459
|
+
throw new Error("Term not found");
|
|
460
|
+
}
|
|
461
|
+
if (isDeleted(term)) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
// Check for children
|
|
465
|
+
const children = await ctx.db
|
|
466
|
+
.query("taxonomyTerms")
|
|
467
|
+
.withIndex("by_parent", (q) => q.eq("parentId", id))
|
|
468
|
+
.collect();
|
|
469
|
+
const activeChildren = children.filter((c) => !isDeleted(c));
|
|
470
|
+
if (activeChildren.length > 0 && !cascade) {
|
|
471
|
+
throw new Error("Cannot delete term with children. Use cascade=true or delete children first.");
|
|
472
|
+
}
|
|
473
|
+
// Delete this term
|
|
474
|
+
await ctx.db.patch(id, {
|
|
475
|
+
deletedAt: Date.now(),
|
|
476
|
+
updatedBy: userId,
|
|
477
|
+
});
|
|
478
|
+
// Also delete children if cascading
|
|
479
|
+
if (cascade) {
|
|
480
|
+
for (const child of activeChildren) {
|
|
481
|
+
await ctx.db.patch(child._id, {
|
|
482
|
+
deletedAt: Date.now(),
|
|
483
|
+
updatedBy: userId,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// Remove entry tag associations
|
|
488
|
+
const associations = await ctx.db
|
|
489
|
+
.query("contentEntryTags")
|
|
490
|
+
.withIndex("by_term", (q) => q.eq("termId", id))
|
|
491
|
+
.collect();
|
|
492
|
+
for (const assoc of associations) {
|
|
493
|
+
await ctx.db.delete(assoc._id);
|
|
494
|
+
}
|
|
495
|
+
// Remove media asset tag associations
|
|
496
|
+
const mediaAssociations = await ctx.db
|
|
497
|
+
.query("mediaAssetTags")
|
|
498
|
+
.withIndex("by_term", (q) => q.eq("termId", id))
|
|
499
|
+
.collect();
|
|
500
|
+
for (const assoc of mediaAssociations) {
|
|
501
|
+
await ctx.db.delete(assoc._id);
|
|
502
|
+
}
|
|
503
|
+
return null;
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
/**
|
|
507
|
+
* Restore a soft-deleted term.
|
|
508
|
+
*/
|
|
509
|
+
export const restoreTerm = mutation({
|
|
510
|
+
args: {
|
|
511
|
+
id: v.id("taxonomyTerms"),
|
|
512
|
+
userId: v.optional(v.string()),
|
|
513
|
+
},
|
|
514
|
+
returns: v.id("taxonomyTerms"),
|
|
515
|
+
handler: async (ctx, args) => {
|
|
516
|
+
const { id, userId } = args;
|
|
517
|
+
const term = await ctx.db.get(id);
|
|
518
|
+
if (!term) {
|
|
519
|
+
throw new Error("Term not found");
|
|
520
|
+
}
|
|
521
|
+
if (!isDeleted(term)) {
|
|
522
|
+
return id;
|
|
523
|
+
}
|
|
524
|
+
// Make sure parent exists if there is one
|
|
525
|
+
if (term.parentId) {
|
|
526
|
+
const parent = await ctx.db.get(term.parentId);
|
|
527
|
+
if (!parent || isDeleted(parent)) {
|
|
528
|
+
// Restore as root term
|
|
529
|
+
await ctx.db.patch(id, {
|
|
530
|
+
deletedAt: undefined,
|
|
531
|
+
parentId: undefined,
|
|
532
|
+
path: `/${term.slug}`,
|
|
533
|
+
depth: 0,
|
|
534
|
+
updatedBy: userId,
|
|
535
|
+
});
|
|
536
|
+
return id;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
await ctx.db.patch(id, {
|
|
540
|
+
deletedAt: undefined,
|
|
541
|
+
updatedBy: userId,
|
|
542
|
+
});
|
|
543
|
+
return id;
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
// =============================================================================
|
|
547
|
+
// Entry Tag Mutations
|
|
548
|
+
// =============================================================================
|
|
549
|
+
/**
|
|
550
|
+
* Set the terms for an entry field (replaces all existing terms).
|
|
551
|
+
*
|
|
552
|
+
* @example
|
|
553
|
+
* ```typescript
|
|
554
|
+
* await ctx.runMutation(api.taxonomyMutations.setEntryTerms, {
|
|
555
|
+
* entryId: blogPostId,
|
|
556
|
+
* fieldName: "tags",
|
|
557
|
+
* termIds: [javascriptTagId, reactTagId, typescriptTagId],
|
|
558
|
+
* });
|
|
559
|
+
* ```
|
|
560
|
+
*/
|
|
561
|
+
export const setEntryTerms = mutation({
|
|
562
|
+
args: {
|
|
563
|
+
entryId: v.id("contentEntries"),
|
|
564
|
+
fieldName: v.string(),
|
|
565
|
+
termIds: v.array(v.id("taxonomyTerms")),
|
|
566
|
+
},
|
|
567
|
+
returns: v.null(),
|
|
568
|
+
handler: async (ctx, args) => {
|
|
569
|
+
const { entryId, fieldName, termIds } = args;
|
|
570
|
+
// Verify entry exists
|
|
571
|
+
const entry = await ctx.db.get(entryId);
|
|
572
|
+
if (!entry || isDeleted(entry)) {
|
|
573
|
+
throw new Error("Content entry not found");
|
|
574
|
+
}
|
|
575
|
+
// Get existing associations for this field
|
|
576
|
+
const existing = await ctx.db
|
|
577
|
+
.query("contentEntryTags")
|
|
578
|
+
.withIndex("by_entry_and_field", (q) => q.eq("entryId", entryId).eq("fieldName", fieldName))
|
|
579
|
+
.collect();
|
|
580
|
+
const existingTermIds = new Set(existing.map((e) => e.termId));
|
|
581
|
+
const newTermIds = new Set(termIds);
|
|
582
|
+
// Calculate terms to remove and add
|
|
583
|
+
const toRemove = existing.filter((e) => !newTermIds.has(e.termId));
|
|
584
|
+
const toAdd = termIds.filter((id) => !existingTermIds.has(id));
|
|
585
|
+
// Remove old associations and update usage counts
|
|
586
|
+
for (const assoc of toRemove) {
|
|
587
|
+
const term = await ctx.db.get(assoc.termId);
|
|
588
|
+
if (term && term.usageCount > 0) {
|
|
589
|
+
await ctx.db.patch(assoc.termId, {
|
|
590
|
+
usageCount: term.usageCount - 1,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
await ctx.db.delete(assoc._id);
|
|
594
|
+
}
|
|
595
|
+
// Add new associations and update usage counts
|
|
596
|
+
for (let i = 0; i < toAdd.length; i++) {
|
|
597
|
+
const termId = toAdd[i];
|
|
598
|
+
const term = await ctx.db.get(termId);
|
|
599
|
+
if (!term || isDeleted(term)) {
|
|
600
|
+
continue; // Skip invalid terms
|
|
601
|
+
}
|
|
602
|
+
// Update usage count
|
|
603
|
+
await ctx.db.patch(termId, {
|
|
604
|
+
usageCount: term.usageCount + 1,
|
|
605
|
+
});
|
|
606
|
+
// Create association
|
|
607
|
+
await ctx.db.insert("contentEntryTags", {
|
|
608
|
+
entryId,
|
|
609
|
+
termId,
|
|
610
|
+
taxonomyId: term.taxonomyId,
|
|
611
|
+
fieldName,
|
|
612
|
+
sortOrder: i,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
// Update sort order for existing items that weren't removed
|
|
616
|
+
const remainingExisting = existing.filter((e) => newTermIds.has(e.termId));
|
|
617
|
+
for (const assoc of remainingExisting) {
|
|
618
|
+
const newIndex = termIds.indexOf(assoc.termId);
|
|
619
|
+
if (newIndex !== assoc.sortOrder) {
|
|
620
|
+
await ctx.db.patch(assoc._id, { sortOrder: newIndex });
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return null;
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
/**
|
|
627
|
+
* Add a single term to an entry field.
|
|
628
|
+
*/
|
|
629
|
+
export const addTermToEntry = mutation({
|
|
630
|
+
args: {
|
|
631
|
+
entryId: v.id("contentEntries"),
|
|
632
|
+
fieldName: v.string(),
|
|
633
|
+
termId: v.id("taxonomyTerms"),
|
|
634
|
+
},
|
|
635
|
+
returns: v.null(),
|
|
636
|
+
handler: async (ctx, args) => {
|
|
637
|
+
const { entryId, fieldName, termId } = args;
|
|
638
|
+
// Verify entry exists
|
|
639
|
+
const entry = await ctx.db.get(entryId);
|
|
640
|
+
if (!entry || isDeleted(entry)) {
|
|
641
|
+
throw new Error("Content entry not found");
|
|
642
|
+
}
|
|
643
|
+
// Verify term exists
|
|
644
|
+
const term = await ctx.db.get(termId);
|
|
645
|
+
if (!term || isDeleted(term)) {
|
|
646
|
+
throw new Error("Term not found");
|
|
647
|
+
}
|
|
648
|
+
// Check if already associated
|
|
649
|
+
const existing = await ctx.db
|
|
650
|
+
.query("contentEntryTags")
|
|
651
|
+
.withIndex("by_entry_and_field", (q) => q.eq("entryId", entryId).eq("fieldName", fieldName))
|
|
652
|
+
.collect();
|
|
653
|
+
if (existing.some((e) => e.termId === termId)) {
|
|
654
|
+
return null; // Already associated
|
|
655
|
+
}
|
|
656
|
+
// Update usage count
|
|
657
|
+
await ctx.db.patch(termId, {
|
|
658
|
+
usageCount: term.usageCount + 1,
|
|
659
|
+
});
|
|
660
|
+
// Create association
|
|
661
|
+
await ctx.db.insert("contentEntryTags", {
|
|
662
|
+
entryId,
|
|
663
|
+
termId,
|
|
664
|
+
taxonomyId: term.taxonomyId,
|
|
665
|
+
fieldName,
|
|
666
|
+
sortOrder: existing.length,
|
|
667
|
+
});
|
|
668
|
+
return null;
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
/**
|
|
672
|
+
* Remove a single term from an entry field.
|
|
673
|
+
*/
|
|
674
|
+
export const removeTermFromEntry = mutation({
|
|
675
|
+
args: {
|
|
676
|
+
entryId: v.id("contentEntries"),
|
|
677
|
+
fieldName: v.string(),
|
|
678
|
+
termId: v.id("taxonomyTerms"),
|
|
679
|
+
},
|
|
680
|
+
returns: v.null(),
|
|
681
|
+
handler: async (ctx, args) => {
|
|
682
|
+
const { entryId, fieldName, termId } = args;
|
|
683
|
+
// Find the association
|
|
684
|
+
const associations = await ctx.db
|
|
685
|
+
.query("contentEntryTags")
|
|
686
|
+
.withIndex("by_entry_and_field", (q) => q.eq("entryId", entryId).eq("fieldName", fieldName))
|
|
687
|
+
.collect();
|
|
688
|
+
const assoc = associations.find((a) => a.termId === termId);
|
|
689
|
+
if (!assoc) {
|
|
690
|
+
return null; // Not associated
|
|
691
|
+
}
|
|
692
|
+
// Update usage count
|
|
693
|
+
const term = await ctx.db.get(termId);
|
|
694
|
+
if (term && term.usageCount > 0) {
|
|
695
|
+
await ctx.db.patch(termId, {
|
|
696
|
+
usageCount: term.usageCount - 1,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
// Delete association
|
|
700
|
+
await ctx.db.delete(assoc._id);
|
|
701
|
+
return null;
|
|
702
|
+
},
|
|
703
|
+
});
|
|
704
|
+
/**
|
|
705
|
+
* Create a term and add it to an entry in one operation.
|
|
706
|
+
* Useful for inline tag creation.
|
|
707
|
+
*/
|
|
708
|
+
export const createTermAndAddToEntry = mutation({
|
|
709
|
+
args: {
|
|
710
|
+
taxonomyId: v.id("taxonomies"),
|
|
711
|
+
name: v.string(),
|
|
712
|
+
entryId: v.id("contentEntries"),
|
|
713
|
+
fieldName: v.string(),
|
|
714
|
+
userId: v.optional(v.string()),
|
|
715
|
+
},
|
|
716
|
+
returns: v.id("taxonomyTerms"),
|
|
717
|
+
handler: async (ctx, args) => {
|
|
718
|
+
const { taxonomyId, name, entryId, fieldName, userId } = args;
|
|
719
|
+
// Verify taxonomy allows inline creation
|
|
720
|
+
const taxonomy = await ctx.db.get(taxonomyId);
|
|
721
|
+
if (!taxonomy || isDeleted(taxonomy)) {
|
|
722
|
+
throw new Error("Taxonomy not found");
|
|
723
|
+
}
|
|
724
|
+
if (!taxonomy.allowInlineCreation) {
|
|
725
|
+
throw new Error("Inline term creation is not allowed for this taxonomy");
|
|
726
|
+
}
|
|
727
|
+
// Generate slug
|
|
728
|
+
const slug = generateSlug(name);
|
|
729
|
+
// Check if term already exists
|
|
730
|
+
const existingTerm = await ctx.db
|
|
731
|
+
.query("taxonomyTerms")
|
|
732
|
+
.withIndex("by_taxonomy_and_slug", (q) => q.eq("taxonomyId", taxonomyId).eq("slug", slug))
|
|
733
|
+
.first();
|
|
734
|
+
let termId;
|
|
735
|
+
if (existingTerm && !isDeleted(existingTerm)) {
|
|
736
|
+
// Use existing term
|
|
737
|
+
termId = existingTerm._id;
|
|
738
|
+
}
|
|
739
|
+
else if (existingTerm) {
|
|
740
|
+
// Restore soft-deleted term
|
|
741
|
+
await ctx.db.patch(existingTerm._id, {
|
|
742
|
+
deletedAt: undefined,
|
|
743
|
+
name,
|
|
744
|
+
searchText: name,
|
|
745
|
+
updatedBy: userId,
|
|
746
|
+
});
|
|
747
|
+
termId = existingTerm._id;
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
// Create new term
|
|
751
|
+
termId = await ctx.db.insert("taxonomyTerms", {
|
|
752
|
+
taxonomyId,
|
|
753
|
+
slug,
|
|
754
|
+
name,
|
|
755
|
+
depth: 0,
|
|
756
|
+
usageCount: 0,
|
|
757
|
+
searchText: name,
|
|
758
|
+
createdBy: userId,
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
// Add to entry
|
|
762
|
+
const existingAssoc = await ctx.db
|
|
763
|
+
.query("contentEntryTags")
|
|
764
|
+
.withIndex("by_entry_and_field", (q) => q.eq("entryId", entryId).eq("fieldName", fieldName))
|
|
765
|
+
.collect();
|
|
766
|
+
if (!existingAssoc.some((a) => a.termId === termId)) {
|
|
767
|
+
// Get the term for usage count update
|
|
768
|
+
const termDoc = await ctx.db.get(termId);
|
|
769
|
+
if (termDoc) {
|
|
770
|
+
await ctx.db.patch(termId, {
|
|
771
|
+
usageCount: termDoc.usageCount + 1,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
await ctx.db.insert("contentEntryTags", {
|
|
775
|
+
entryId,
|
|
776
|
+
termId,
|
|
777
|
+
taxonomyId,
|
|
778
|
+
fieldName,
|
|
779
|
+
sortOrder: existingAssoc.length,
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
return termId;
|
|
783
|
+
},
|
|
784
|
+
});
|
|
785
|
+
// =============================================================================
|
|
786
|
+
// Media Asset Tag Mutations
|
|
787
|
+
// =============================================================================
|
|
788
|
+
/**
|
|
789
|
+
* Set the terms for a media asset in a taxonomy (replaces all existing terms).
|
|
790
|
+
*
|
|
791
|
+
* @example
|
|
792
|
+
* ```typescript
|
|
793
|
+
* await ctx.runMutation(api.taxonomyMutations.setMediaTerms, {
|
|
794
|
+
* mediaId: imageId,
|
|
795
|
+
* taxonomyId: categoriesTaxonomyId,
|
|
796
|
+
* termIds: [landscapeTagId, summerTagId],
|
|
797
|
+
* });
|
|
798
|
+
* ```
|
|
799
|
+
*/
|
|
800
|
+
export const setMediaTerms = mutation({
|
|
801
|
+
args: {
|
|
802
|
+
mediaId: v.id("mediaItems"),
|
|
803
|
+
taxonomyId: v.id("taxonomies"),
|
|
804
|
+
termIds: v.array(v.id("taxonomyTerms")),
|
|
805
|
+
},
|
|
806
|
+
returns: v.null(),
|
|
807
|
+
handler: async (ctx, args) => {
|
|
808
|
+
const { mediaId, taxonomyId, termIds } = args;
|
|
809
|
+
const media = await ctx.db.get(mediaId);
|
|
810
|
+
if (!media || isDeleted(media)) {
|
|
811
|
+
throw new Error("Media asset not found");
|
|
812
|
+
}
|
|
813
|
+
const taxonomy = await ctx.db.get(taxonomyId);
|
|
814
|
+
if (!taxonomy || isDeleted(taxonomy)) {
|
|
815
|
+
throw new Error("Taxonomy not found");
|
|
816
|
+
}
|
|
817
|
+
const existing = await ctx.db
|
|
818
|
+
.query("mediaAssetTags")
|
|
819
|
+
.withIndex("by_media_and_taxonomy", (q) => q.eq("mediaId", mediaId).eq("taxonomyId", taxonomyId))
|
|
820
|
+
.collect();
|
|
821
|
+
const existingTermIds = new Set(existing.map((e) => e.termId));
|
|
822
|
+
const newTermIds = new Set(termIds);
|
|
823
|
+
const toRemove = existing.filter((e) => !newTermIds.has(e.termId));
|
|
824
|
+
const toAdd = termIds.filter((id) => !existingTermIds.has(id));
|
|
825
|
+
for (const assoc of toRemove) {
|
|
826
|
+
const term = await ctx.db.get(assoc.termId);
|
|
827
|
+
if (term && term.usageCount > 0) {
|
|
828
|
+
await ctx.db.patch(assoc.termId, {
|
|
829
|
+
usageCount: term.usageCount - 1,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
await ctx.db.delete(assoc._id);
|
|
833
|
+
}
|
|
834
|
+
for (let i = 0; i < toAdd.length; i++) {
|
|
835
|
+
const termId = toAdd[i];
|
|
836
|
+
const term = await ctx.db.get(termId);
|
|
837
|
+
if (!term || isDeleted(term)) {
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
if (term.taxonomyId !== taxonomyId) {
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
await ctx.db.patch(termId, {
|
|
844
|
+
usageCount: term.usageCount + 1,
|
|
845
|
+
});
|
|
846
|
+
await ctx.db.insert("mediaAssetTags", {
|
|
847
|
+
mediaId,
|
|
848
|
+
termId,
|
|
849
|
+
taxonomyId,
|
|
850
|
+
sortOrder: i,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
const remainingExisting = existing.filter((e) => newTermIds.has(e.termId));
|
|
854
|
+
for (const assoc of remainingExisting) {
|
|
855
|
+
const newIndex = termIds.indexOf(assoc.termId);
|
|
856
|
+
if (newIndex !== assoc.sortOrder) {
|
|
857
|
+
await ctx.db.patch(assoc._id, { sortOrder: newIndex });
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return null;
|
|
861
|
+
},
|
|
862
|
+
});
|
|
863
|
+
/**
|
|
864
|
+
* Add a single term to a media asset.
|
|
865
|
+
*/
|
|
866
|
+
export const addTermToMedia = mutation({
|
|
867
|
+
args: {
|
|
868
|
+
mediaId: v.id("mediaItems"),
|
|
869
|
+
termId: v.id("taxonomyTerms"),
|
|
870
|
+
},
|
|
871
|
+
returns: v.null(),
|
|
872
|
+
handler: async (ctx, args) => {
|
|
873
|
+
const { mediaId, termId } = args;
|
|
874
|
+
const media = await ctx.db.get(mediaId);
|
|
875
|
+
if (!media || isDeleted(media)) {
|
|
876
|
+
throw new Error("Media asset not found");
|
|
877
|
+
}
|
|
878
|
+
const term = await ctx.db.get(termId);
|
|
879
|
+
if (!term || isDeleted(term)) {
|
|
880
|
+
throw new Error("Term not found");
|
|
881
|
+
}
|
|
882
|
+
const existing = await ctx.db
|
|
883
|
+
.query("mediaAssetTags")
|
|
884
|
+
.withIndex("by_media", (q) => q.eq("mediaId", mediaId))
|
|
885
|
+
.collect();
|
|
886
|
+
if (existing.some((e) => e.termId === termId)) {
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
await ctx.db.patch(termId, {
|
|
890
|
+
usageCount: term.usageCount + 1,
|
|
891
|
+
});
|
|
892
|
+
await ctx.db.insert("mediaAssetTags", {
|
|
893
|
+
mediaId,
|
|
894
|
+
termId,
|
|
895
|
+
taxonomyId: term.taxonomyId,
|
|
896
|
+
sortOrder: existing.length,
|
|
897
|
+
});
|
|
898
|
+
return null;
|
|
899
|
+
},
|
|
900
|
+
});
|
|
901
|
+
/**
|
|
902
|
+
* Remove a single term from a media asset.
|
|
903
|
+
*/
|
|
904
|
+
export const removeTermFromMedia = mutation({
|
|
905
|
+
args: {
|
|
906
|
+
mediaId: v.id("mediaItems"),
|
|
907
|
+
termId: v.id("taxonomyTerms"),
|
|
908
|
+
},
|
|
909
|
+
returns: v.null(),
|
|
910
|
+
handler: async (ctx, args) => {
|
|
911
|
+
const { mediaId, termId } = args;
|
|
912
|
+
const associations = await ctx.db
|
|
913
|
+
.query("mediaAssetTags")
|
|
914
|
+
.withIndex("by_media", (q) => q.eq("mediaId", mediaId))
|
|
915
|
+
.collect();
|
|
916
|
+
const assoc = associations.find((a) => a.termId === termId);
|
|
917
|
+
if (!assoc) {
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
const term = await ctx.db.get(termId);
|
|
921
|
+
if (term && term.usageCount > 0) {
|
|
922
|
+
await ctx.db.patch(termId, {
|
|
923
|
+
usageCount: term.usageCount - 1,
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
await ctx.db.delete(assoc._id);
|
|
927
|
+
return null;
|
|
928
|
+
},
|
|
929
|
+
});
|
|
930
|
+
/**
|
|
931
|
+
* Create a term and add it to a media asset in one operation.
|
|
932
|
+
* Useful for inline tag creation in the media library.
|
|
933
|
+
*/
|
|
934
|
+
export const createTermAndAddToMedia = mutation({
|
|
935
|
+
args: {
|
|
936
|
+
taxonomyId: v.id("taxonomies"),
|
|
937
|
+
name: v.string(),
|
|
938
|
+
mediaId: v.id("mediaItems"),
|
|
939
|
+
userId: v.optional(v.string()),
|
|
940
|
+
},
|
|
941
|
+
returns: v.id("taxonomyTerms"),
|
|
942
|
+
handler: async (ctx, args) => {
|
|
943
|
+
const { taxonomyId, name, mediaId, userId } = args;
|
|
944
|
+
const taxonomy = await ctx.db.get(taxonomyId);
|
|
945
|
+
if (!taxonomy || isDeleted(taxonomy)) {
|
|
946
|
+
throw new Error("Taxonomy not found");
|
|
947
|
+
}
|
|
948
|
+
if (!taxonomy.allowInlineCreation) {
|
|
949
|
+
throw new Error("Inline term creation is not allowed for this taxonomy");
|
|
950
|
+
}
|
|
951
|
+
const slug = generateSlug(name);
|
|
952
|
+
const existingTerm = await ctx.db
|
|
953
|
+
.query("taxonomyTerms")
|
|
954
|
+
.withIndex("by_taxonomy_and_slug", (q) => q.eq("taxonomyId", taxonomyId).eq("slug", slug))
|
|
955
|
+
.first();
|
|
956
|
+
let termId;
|
|
957
|
+
if (existingTerm && !isDeleted(existingTerm)) {
|
|
958
|
+
termId = existingTerm._id;
|
|
959
|
+
}
|
|
960
|
+
else if (existingTerm) {
|
|
961
|
+
await ctx.db.patch(existingTerm._id, {
|
|
962
|
+
deletedAt: undefined,
|
|
963
|
+
name,
|
|
964
|
+
searchText: name,
|
|
965
|
+
updatedBy: userId,
|
|
966
|
+
});
|
|
967
|
+
termId = existingTerm._id;
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
termId = await ctx.db.insert("taxonomyTerms", {
|
|
971
|
+
taxonomyId,
|
|
972
|
+
slug,
|
|
973
|
+
name,
|
|
974
|
+
depth: 0,
|
|
975
|
+
usageCount: 0,
|
|
976
|
+
searchText: name,
|
|
977
|
+
createdBy: userId,
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
const existingAssoc = await ctx.db
|
|
981
|
+
.query("mediaAssetTags")
|
|
982
|
+
.withIndex("by_media", (q) => q.eq("mediaId", mediaId))
|
|
983
|
+
.collect();
|
|
984
|
+
if (!existingAssoc.some((a) => a.termId === termId)) {
|
|
985
|
+
const termDoc = await ctx.db.get(termId);
|
|
986
|
+
if (termDoc) {
|
|
987
|
+
await ctx.db.patch(termId, {
|
|
988
|
+
usageCount: termDoc.usageCount + 1,
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
await ctx.db.insert("mediaAssetTags", {
|
|
992
|
+
mediaId,
|
|
993
|
+
termId,
|
|
994
|
+
taxonomyId,
|
|
995
|
+
sortOrder: existingAssoc.length,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
return termId;
|
|
999
|
+
},
|
|
1000
|
+
});
|
|
1001
|
+
//# sourceMappingURL=taxonomyMutations.js.map
|