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,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slug Uniqueness Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides functions for checking slug uniqueness within content type scope
|
|
5
|
+
* and generating unique slugs with incremental suffixes when conflicts exist.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
generateSlug,
|
|
10
|
+
generateUniqueSlug,
|
|
11
|
+
isValidSlug,
|
|
12
|
+
} from "./slugGenerator.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Options for slug uniqueness checking
|
|
16
|
+
*/
|
|
17
|
+
export interface SlugUniquenessOptions {
|
|
18
|
+
/** Maximum number of suffix attempts before falling back to timestamp (default: 100) */
|
|
19
|
+
maxAttempts?: number;
|
|
20
|
+
/** Separator character used in slugs (default: '-') */
|
|
21
|
+
separator?: string;
|
|
22
|
+
/** ID of the current entry to exclude from uniqueness check (for updates) */
|
|
23
|
+
excludeEntryId?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Result of a slug uniqueness check
|
|
28
|
+
*/
|
|
29
|
+
export interface SlugCheckResult {
|
|
30
|
+
/** Whether the slug is unique */
|
|
31
|
+
isUnique: boolean;
|
|
32
|
+
/** The existing entry ID that has this slug (if not unique) */
|
|
33
|
+
existingEntryId?: string;
|
|
34
|
+
/** Suggested alternative slug if not unique */
|
|
35
|
+
suggestedSlug?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Entry data structure for uniqueness checking
|
|
40
|
+
*/
|
|
41
|
+
export interface SlugEntry {
|
|
42
|
+
/** The entry's unique identifier */
|
|
43
|
+
_id: string;
|
|
44
|
+
/** The entry's slug */
|
|
45
|
+
slug: string;
|
|
46
|
+
/** Soft delete timestamp (null/undefined if not deleted) */
|
|
47
|
+
deletedAt?: number | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Function type for querying existing entries by slug within a content type
|
|
52
|
+
*/
|
|
53
|
+
export type SlugQueryFn = (slug: string) => Promise<SlugEntry | null>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Function type for querying all entries with a slug prefix within a content type
|
|
57
|
+
* Used for finding the next available suffix number
|
|
58
|
+
*/
|
|
59
|
+
export type SlugPrefixQueryFn = (slugPrefix: string) => Promise<SlugEntry[]>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Checks if a slug is unique within a content type scope.
|
|
63
|
+
*
|
|
64
|
+
* @param slug - The slug to check
|
|
65
|
+
* @param queryFn - Function that queries the database for entries with the given slug
|
|
66
|
+
* @param options - Configuration options
|
|
67
|
+
* @returns Result indicating uniqueness and suggestions
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* // In a Convex mutation
|
|
72
|
+
* const queryFn = async (slug: string) => {
|
|
73
|
+
* return await ctx.db
|
|
74
|
+
* .query("contentEntries")
|
|
75
|
+
* .withIndex("by_content_type_and_slug", (q) =>
|
|
76
|
+
* q.eq("contentTypeId", contentTypeId).eq("slug", slug)
|
|
77
|
+
* )
|
|
78
|
+
* .filter((q) => q.eq(q.field("deletedAt"), undefined))
|
|
79
|
+
* .first();
|
|
80
|
+
* };
|
|
81
|
+
*
|
|
82
|
+
* const result = await checkSlugUniqueness("my-post", queryFn);
|
|
83
|
+
* if (!result.isUnique) {
|
|
84
|
+
* console.log(`Slug conflict with entry ${result.existingEntryId}`);
|
|
85
|
+
* console.log(`Suggested alternative: ${result.suggestedSlug}`);
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export async function checkSlugUniqueness(
|
|
90
|
+
slug: string,
|
|
91
|
+
queryFn: SlugQueryFn,
|
|
92
|
+
options: SlugUniquenessOptions = {},
|
|
93
|
+
): Promise<SlugCheckResult> {
|
|
94
|
+
const { excludeEntryId } = options;
|
|
95
|
+
|
|
96
|
+
// Validate the slug format
|
|
97
|
+
if (!isValidSlug(slug, options.separator)) {
|
|
98
|
+
return {
|
|
99
|
+
isUnique: false,
|
|
100
|
+
suggestedSlug: generateSlug(slug, { separator: options.separator }),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Query for existing entry with this slug
|
|
105
|
+
const existingEntry = await queryFn(slug);
|
|
106
|
+
|
|
107
|
+
// Check if the slug is available
|
|
108
|
+
if (!existingEntry) {
|
|
109
|
+
return { isUnique: true };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// If we're updating an entry, exclude it from the check
|
|
113
|
+
if (excludeEntryId && existingEntry._id === excludeEntryId) {
|
|
114
|
+
return { isUnique: true };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Slug is taken - generate a suggestion
|
|
118
|
+
const isUniqueFn = async (candidateSlug: string): Promise<boolean> => {
|
|
119
|
+
const entry = await queryFn(candidateSlug);
|
|
120
|
+
if (!entry) return true;
|
|
121
|
+
if (excludeEntryId && entry._id === excludeEntryId) return true;
|
|
122
|
+
return false;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const suggestedSlug = await generateUniqueSlug(
|
|
126
|
+
slug,
|
|
127
|
+
isUniqueFn,
|
|
128
|
+
options.maxAttempts,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
isUnique: false,
|
|
133
|
+
existingEntryId: existingEntry._id,
|
|
134
|
+
suggestedSlug,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Ensures a slug is unique by generating incremental suffixes if needed.
|
|
140
|
+
* This is the main function to use when creating or updating content entries.
|
|
141
|
+
*
|
|
142
|
+
* @param baseSlug - The desired slug (or title to generate slug from)
|
|
143
|
+
* @param queryFn - Function that queries the database for entries with a given slug
|
|
144
|
+
* @param options - Configuration options
|
|
145
|
+
* @returns A unique slug (either the original or with a numeric suffix)
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```typescript
|
|
149
|
+
* // In a Convex mutation for creating a new entry
|
|
150
|
+
* const queryFn = async (slug: string) => {
|
|
151
|
+
* return await ctx.db
|
|
152
|
+
* .query("contentEntries")
|
|
153
|
+
* .withIndex("by_content_type_and_slug", (q) =>
|
|
154
|
+
* q.eq("contentTypeId", contentTypeId).eq("slug", slug)
|
|
155
|
+
* )
|
|
156
|
+
* .filter((q) => q.eq(q.field("deletedAt"), undefined))
|
|
157
|
+
* .first();
|
|
158
|
+
* };
|
|
159
|
+
*
|
|
160
|
+
* const uniqueSlug = await ensureUniqueSlug("my-post", queryFn);
|
|
161
|
+
* // Returns "my-post" if unique, or "my-post-1", "my-post-2", etc.
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export async function ensureUniqueSlug(
|
|
165
|
+
baseSlug: string,
|
|
166
|
+
queryFn: SlugQueryFn,
|
|
167
|
+
options: SlugUniquenessOptions = {},
|
|
168
|
+
): Promise<string> {
|
|
169
|
+
const { excludeEntryId, maxAttempts = 100 } = options;
|
|
170
|
+
|
|
171
|
+
// Validate and normalize the base slug
|
|
172
|
+
let slug = baseSlug;
|
|
173
|
+
if (!isValidSlug(slug, options.separator)) {
|
|
174
|
+
slug = generateSlug(slug, { separator: options.separator });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// If slug is empty after normalization, use a fallback
|
|
178
|
+
if (!slug) {
|
|
179
|
+
slug = "untitled";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check if the base slug is available
|
|
183
|
+
const isUniqueFn = async (candidateSlug: string): Promise<boolean> => {
|
|
184
|
+
const entry = await queryFn(candidateSlug);
|
|
185
|
+
if (!entry) return true;
|
|
186
|
+
if (excludeEntryId && entry._id === excludeEntryId) return true;
|
|
187
|
+
return false;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return generateUniqueSlug(slug, isUniqueFn, maxAttempts);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Finds the next available slug suffix by analyzing existing slugs.
|
|
195
|
+
* This is useful when you want to pre-compute the next suffix without
|
|
196
|
+
* iterating through each number.
|
|
197
|
+
*
|
|
198
|
+
* @param baseSlug - The base slug to find the next suffix for
|
|
199
|
+
* @param prefixQueryFn - Function that returns all entries with slugs starting with the base
|
|
200
|
+
* @param options - Configuration options
|
|
201
|
+
* @returns The next available slug with the appropriate suffix
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```typescript
|
|
205
|
+
* // If entries exist with slugs: "post", "post-1", "post-2", "post-5"
|
|
206
|
+
* const nextSlug = await findNextAvailableSlug("post", queryPrefixFn);
|
|
207
|
+
* // Returns "post-3" (fills the gap)
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
export async function findNextAvailableSlug(
|
|
211
|
+
baseSlug: string,
|
|
212
|
+
prefixQueryFn: SlugPrefixQueryFn,
|
|
213
|
+
options: SlugUniquenessOptions = {},
|
|
214
|
+
): Promise<string> {
|
|
215
|
+
const { excludeEntryId, separator = "-" } = options;
|
|
216
|
+
|
|
217
|
+
// Validate the base slug
|
|
218
|
+
let slug = baseSlug;
|
|
219
|
+
if (!isValidSlug(slug, separator)) {
|
|
220
|
+
slug = generateSlug(slug, { separator });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!slug) {
|
|
224
|
+
slug = "untitled";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Get all entries with this prefix
|
|
228
|
+
const existingEntries = await prefixQueryFn(slug);
|
|
229
|
+
|
|
230
|
+
// Filter out the excluded entry and soft-deleted entries
|
|
231
|
+
const activeEntries = existingEntries.filter((entry) => {
|
|
232
|
+
if (excludeEntryId && entry._id === excludeEntryId) return false;
|
|
233
|
+
if (entry.deletedAt) return false;
|
|
234
|
+
return true;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// If no entries exist with this slug, the base is available
|
|
238
|
+
const hasSlugsToCheck = activeEntries.some((entry) => {
|
|
239
|
+
return entry.slug === slug || entry.slug.startsWith(`${slug}${separator}`);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (!hasSlugsToCheck) {
|
|
243
|
+
return slug;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Check if base slug itself is taken
|
|
247
|
+
const baseIsTaken = activeEntries.some((entry) => entry.slug === slug);
|
|
248
|
+
if (!baseIsTaken) {
|
|
249
|
+
return slug;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Extract existing suffix numbers
|
|
253
|
+
const suffixPattern = new RegExp(
|
|
254
|
+
`^${escapeRegex(slug)}${escapeRegex(separator)}(\\d+)$`,
|
|
255
|
+
);
|
|
256
|
+
const usedNumbers = new Set<number>();
|
|
257
|
+
|
|
258
|
+
for (const entry of activeEntries) {
|
|
259
|
+
const match = entry.slug.match(suffixPattern);
|
|
260
|
+
if (match) {
|
|
261
|
+
usedNumbers.add(parseInt(match[1], 10));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Find the smallest available number
|
|
266
|
+
let nextNumber = 1;
|
|
267
|
+
while (usedNumbers.has(nextNumber)) {
|
|
268
|
+
nextNumber++;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return `${slug}${separator}${nextNumber}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Escapes special regex characters in a string
|
|
276
|
+
*/
|
|
277
|
+
function escapeRegex(str: string): string {
|
|
278
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Validates a slug and returns validation errors if any.
|
|
283
|
+
*
|
|
284
|
+
* @param slug - The slug to validate
|
|
285
|
+
* @param separator - The separator character (default: '-')
|
|
286
|
+
* @returns Array of validation error messages (empty if valid)
|
|
287
|
+
*/
|
|
288
|
+
export function validateSlugFormat(
|
|
289
|
+
slug: string,
|
|
290
|
+
separator: string = "-",
|
|
291
|
+
): string[] {
|
|
292
|
+
const errors: string[] = [];
|
|
293
|
+
|
|
294
|
+
if (!slug || typeof slug !== "string") {
|
|
295
|
+
errors.push("Slug is required");
|
|
296
|
+
return errors;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (slug.length === 0) {
|
|
300
|
+
errors.push("Slug cannot be empty");
|
|
301
|
+
return errors;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (slug.length > 100) {
|
|
305
|
+
errors.push("Slug must be 100 characters or less");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (slug !== slug.toLowerCase()) {
|
|
309
|
+
errors.push("Slug must be lowercase");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (slug.startsWith(separator)) {
|
|
313
|
+
errors.push(`Slug cannot start with '${separator}'`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (slug.endsWith(separator)) {
|
|
317
|
+
errors.push(`Slug cannot end with '${separator}'`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const doubleSeparatorRegex = new RegExp(`${escapeRegex(separator)}{2,}`);
|
|
321
|
+
if (doubleSeparatorRegex.test(slug)) {
|
|
322
|
+
errors.push(`Slug cannot contain consecutive '${separator}' characters`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const invalidCharsRegex = new RegExp(`[^a-z0-9${escapeRegex(separator)}]`);
|
|
326
|
+
if (invalidCharsRegex.test(slug)) {
|
|
327
|
+
errors.push(
|
|
328
|
+
"Slug can only contain lowercase letters, numbers, and hyphens",
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return errors;
|
|
333
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Soft-delete utilities for CMS documents.
|
|
3
|
+
*
|
|
4
|
+
* Provides type-safe helpers for working with documents that use
|
|
5
|
+
* the soft-delete pattern (deletedAt timestamp).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface SoftDeletable {
|
|
9
|
+
deletedAt?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isDeleted<T extends SoftDeletable>(doc: T): boolean {
|
|
13
|
+
return doc.deletedAt !== undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isActive<T extends SoftDeletable>(doc: T): boolean {
|
|
17
|
+
return doc.deletedAt === undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function filterActive<T extends SoftDeletable>(docs: T[]): T[] {
|
|
21
|
+
return docs.filter(isActive);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function filterDeleted<T extends SoftDeletable>(docs: T[]): T[] {
|
|
25
|
+
return docs.filter(isDeleted);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function requireNotDeleted<T extends SoftDeletable>(
|
|
29
|
+
doc: T,
|
|
30
|
+
errorFactory: () => Error
|
|
31
|
+
): asserts doc is T & { deletedAt: undefined } {
|
|
32
|
+
if (isDeleted(doc)) {
|
|
33
|
+
throw errorFactory();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function requireDeleted<T extends SoftDeletable>(
|
|
38
|
+
doc: T,
|
|
39
|
+
errorFactory: () => Error
|
|
40
|
+
): void {
|
|
41
|
+
if (!isDeleted(doc)) {
|
|
42
|
+
throw errorFactory();
|
|
43
|
+
}
|
|
44
|
+
}
|