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,550 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Lock Functions
|
|
3
|
+
*
|
|
4
|
+
* Implements optimistic locking for content entries to prevent concurrent edit conflicts.
|
|
5
|
+
* Provides lock acquisition, release, renewal, and status checking.
|
|
6
|
+
*
|
|
7
|
+
* Lock Lifecycle:
|
|
8
|
+
* 1. User acquires lock when opening content for editing
|
|
9
|
+
* 2. Lock auto-expires after configured duration (default 30 minutes)
|
|
10
|
+
* 3. User can renew lock to extend editing session
|
|
11
|
+
* 4. User releases lock when done editing (or lock auto-expires)
|
|
12
|
+
* 5. Admins can force-release locks when needed
|
|
13
|
+
*
|
|
14
|
+
* Lock Behavior:
|
|
15
|
+
* - Only one user can hold a lock at a time
|
|
16
|
+
* - Locks automatically expire to prevent orphaned locks
|
|
17
|
+
* - The lock holder can update their locked entry
|
|
18
|
+
* - Other users receive an error when trying to update locked content
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { v } from "convex/values";
|
|
22
|
+
import { isDeleted } from "./lib/softDelete.js";
|
|
23
|
+
import { mutation, query } from "./_generated/server.js";
|
|
24
|
+
import {
|
|
25
|
+
acquireLockArgs,
|
|
26
|
+
releaseLockArgs,
|
|
27
|
+
forceReleaseLockArgs,
|
|
28
|
+
renewLockArgs,
|
|
29
|
+
checkLockArgs,
|
|
30
|
+
listLockedEntriesArgs,
|
|
31
|
+
lockStatusDoc,
|
|
32
|
+
lockAcquisitionResult,
|
|
33
|
+
contentEntryDoc,
|
|
34
|
+
DEFAULT_LOCK_DURATION_MS,
|
|
35
|
+
MAX_LOCK_DURATION_MS,
|
|
36
|
+
} from "./validators.js";
|
|
37
|
+
|
|
38
|
+
// =============================================================================
|
|
39
|
+
// Helper Functions
|
|
40
|
+
// =============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Checks if a lock is currently active (not expired).
|
|
44
|
+
* @param lockExpiresAt - The lock expiration timestamp
|
|
45
|
+
* @returns true if lock is active, false if expired or not set
|
|
46
|
+
*/
|
|
47
|
+
function isLockActive(lockExpiresAt: number | undefined): boolean {
|
|
48
|
+
if (lockExpiresAt === undefined) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return Date.now() < lockExpiresAt;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Calculates the time remaining on a lock.
|
|
56
|
+
* @param lockExpiresAt - The lock expiration timestamp
|
|
57
|
+
* @returns Time remaining in milliseconds, or 0 if expired
|
|
58
|
+
*/
|
|
59
|
+
function getTimeRemaining(lockExpiresAt: number | undefined): number {
|
|
60
|
+
if (lockExpiresAt === undefined) {
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
const remaining = lockExpiresAt - Date.now();
|
|
64
|
+
return remaining > 0 ? remaining : 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validates and clamps lock duration to allowed range.
|
|
69
|
+
* @param requestedDuration - Requested lock duration in ms
|
|
70
|
+
* @returns Clamped duration within allowed range
|
|
71
|
+
*/
|
|
72
|
+
function validateLockDuration(requestedDuration: number | undefined): number {
|
|
73
|
+
const duration = requestedDuration ?? DEFAULT_LOCK_DURATION_MS;
|
|
74
|
+
|
|
75
|
+
if (duration <= 0) {
|
|
76
|
+
return DEFAULT_LOCK_DURATION_MS;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return Math.min(duration, MAX_LOCK_DURATION_MS);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// =============================================================================
|
|
83
|
+
// Lock Query Functions
|
|
84
|
+
// =============================================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Query to check the lock status of a content entry.
|
|
88
|
+
*
|
|
89
|
+
* Returns detailed information about the current lock state,
|
|
90
|
+
* including whether it's locked, by whom, and how much time remains.
|
|
91
|
+
*
|
|
92
|
+
* @param id - The content entry ID to check
|
|
93
|
+
* @returns Lock status information
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```typescript
|
|
97
|
+
* const status = await ctx.runQuery(api.contentLock.checkLock, {
|
|
98
|
+
* id: entryId,
|
|
99
|
+
* });
|
|
100
|
+
* if (status.isLocked && status.lockedBy !== currentUserId) {
|
|
101
|
+
* console.log(`Entry is locked by ${status.lockedBy}`);
|
|
102
|
+
* }
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export const checkLock = query({
|
|
106
|
+
args: checkLockArgs.fields,
|
|
107
|
+
returns: lockStatusDoc,
|
|
108
|
+
handler: async (ctx, args) => {
|
|
109
|
+
const { id } = args;
|
|
110
|
+
|
|
111
|
+
const entry = await ctx.db.get(id);
|
|
112
|
+
if (!entry) {
|
|
113
|
+
throw new Error(`Content entry not found: ${id}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const _now = Date.now();
|
|
117
|
+
const hasLock =
|
|
118
|
+
entry.lockedBy !== undefined && entry.lockExpiresAt !== undefined;
|
|
119
|
+
const isActive = hasLock && isLockActive(entry.lockExpiresAt);
|
|
120
|
+
const isExpired = hasLock && !isActive;
|
|
121
|
+
const timeRemaining = isActive
|
|
122
|
+
? getTimeRemaining(entry.lockExpiresAt)
|
|
123
|
+
: undefined;
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
isLocked: isActive,
|
|
127
|
+
lockedBy: isActive ? entry.lockedBy : undefined,
|
|
128
|
+
lockExpiresAt: isActive ? entry.lockExpiresAt : undefined,
|
|
129
|
+
timeRemaining,
|
|
130
|
+
isExpired,
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Query to list all locked content entries.
|
|
137
|
+
*
|
|
138
|
+
* Useful for admin dashboards to see which entries are currently
|
|
139
|
+
* being edited and by whom.
|
|
140
|
+
*
|
|
141
|
+
* @param contentTypeId - Optional filter by content type
|
|
142
|
+
* @param lockedBy - Optional filter by locking user
|
|
143
|
+
* @param paginationOpts - Pagination options
|
|
144
|
+
* @returns Paginated list of locked entries
|
|
145
|
+
*/
|
|
146
|
+
export const listLockedEntries = query({
|
|
147
|
+
args: listLockedEntriesArgs.fields,
|
|
148
|
+
returns: v.object({
|
|
149
|
+
page: v.array(
|
|
150
|
+
v.object({
|
|
151
|
+
...contentEntryDoc.fields,
|
|
152
|
+
timeRemaining: v.optional(v.number()),
|
|
153
|
+
}),
|
|
154
|
+
),
|
|
155
|
+
continueCursor: v.union(v.string(), v.null()),
|
|
156
|
+
isDone: v.boolean(),
|
|
157
|
+
}),
|
|
158
|
+
handler: async (ctx, args) => {
|
|
159
|
+
const { contentTypeId, lockedBy, paginationOpts } = args;
|
|
160
|
+
const _now = Date.now();
|
|
161
|
+
|
|
162
|
+
// Query entries with locks using the by_locked index
|
|
163
|
+
const query = ctx.db.query("contentEntries").withIndex("by_locked");
|
|
164
|
+
|
|
165
|
+
// Collect all entries with locks
|
|
166
|
+
const allLocked = await query.collect();
|
|
167
|
+
|
|
168
|
+
// Filter to only active (non-expired) locks
|
|
169
|
+
const entries = allLocked.filter((entry) => {
|
|
170
|
+
// Must have lock fields set
|
|
171
|
+
if (entry.lockedBy === undefined || entry.lockExpiresAt === undefined) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
// Must not be expired
|
|
175
|
+
if (!isLockActive(entry.lockExpiresAt)) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
// Must not be deleted
|
|
179
|
+
if (isDeleted(entry)) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
// Apply content type filter if provided
|
|
183
|
+
if (contentTypeId && entry.contentTypeId !== contentTypeId) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
// Apply lockedBy filter if provided
|
|
187
|
+
if (lockedBy && entry.lockedBy !== lockedBy) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Simple pagination (manual implementation since we filtered in memory)
|
|
194
|
+
const numItems = paginationOpts.numItems ?? 50;
|
|
195
|
+
const cursor = paginationOpts.cursor;
|
|
196
|
+
|
|
197
|
+
let startIndex = 0;
|
|
198
|
+
if (cursor) {
|
|
199
|
+
const cursorIndex = entries.findIndex((e) => e._id === cursor);
|
|
200
|
+
if (cursorIndex !== -1) {
|
|
201
|
+
startIndex = cursorIndex + 1;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const page = entries.slice(startIndex, startIndex + numItems);
|
|
206
|
+
const hasMore = startIndex + numItems < entries.length;
|
|
207
|
+
const nextCursor = hasMore ? page[page.length - 1]?._id ?? null : null;
|
|
208
|
+
|
|
209
|
+
// Add time remaining to each entry
|
|
210
|
+
const pageWithRemaining = page.map((entry) => ({
|
|
211
|
+
...entry,
|
|
212
|
+
timeRemaining: getTimeRemaining(entry.lockExpiresAt),
|
|
213
|
+
}));
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
page: pageWithRemaining,
|
|
217
|
+
continueCursor: nextCursor,
|
|
218
|
+
isDone: !hasMore,
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// =============================================================================
|
|
224
|
+
// Lock Mutation Functions
|
|
225
|
+
// =============================================================================
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Mutation to acquire a lock on a content entry.
|
|
229
|
+
*
|
|
230
|
+
* Attempts to acquire an exclusive lock on an entry for editing.
|
|
231
|
+
* The lock will automatically expire after the specified duration.
|
|
232
|
+
*
|
|
233
|
+
* Lock acquisition rules:
|
|
234
|
+
* - If entry is not locked, lock is acquired
|
|
235
|
+
* - If entry is locked by the same user, lock is renewed
|
|
236
|
+
* - If entry is locked by another user and lock is expired, lock is acquired
|
|
237
|
+
* - If entry is locked by another user and lock is active, acquisition fails
|
|
238
|
+
*
|
|
239
|
+
* @param id - The content entry ID to lock
|
|
240
|
+
* @param userId - User ID acquiring the lock
|
|
241
|
+
* @param lockDuration - Optional lock duration (default 30 min, max 4 hours)
|
|
242
|
+
* @returns Lock acquisition result with success status and entry
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```typescript
|
|
246
|
+
* const result = await ctx.runMutation(api.contentLock.acquireLock, {
|
|
247
|
+
* id: entryId,
|
|
248
|
+
* userId: currentUserId,
|
|
249
|
+
* lockDuration: 60 * 60 * 1000, // 1 hour
|
|
250
|
+
* });
|
|
251
|
+
*
|
|
252
|
+
* if (result.success) {
|
|
253
|
+
* console.log("Lock acquired, editing enabled");
|
|
254
|
+
* } else {
|
|
255
|
+
* console.log(`Lock held by ${result.currentLockHolder}`);
|
|
256
|
+
* }
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
export const acquireLock = mutation({
|
|
260
|
+
args: acquireLockArgs.fields,
|
|
261
|
+
returns: lockAcquisitionResult,
|
|
262
|
+
handler: async (ctx, args) => {
|
|
263
|
+
const { id, userId, lockDuration } = args;
|
|
264
|
+
|
|
265
|
+
const entry = await ctx.db.get(id);
|
|
266
|
+
if (!entry) {
|
|
267
|
+
return {
|
|
268
|
+
success: false,
|
|
269
|
+
error: `Content entry not found: ${id}`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check if entry is deleted
|
|
274
|
+
if (isDeleted(entry)) {
|
|
275
|
+
return {
|
|
276
|
+
success: false,
|
|
277
|
+
error: `Content entry has been deleted: ${id}`,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Calculate lock expiration
|
|
282
|
+
const validDuration = validateLockDuration(lockDuration);
|
|
283
|
+
const now = Date.now();
|
|
284
|
+
const newLockExpiresAt = now + validDuration;
|
|
285
|
+
|
|
286
|
+
// Check current lock status
|
|
287
|
+
const hasExistingLock =
|
|
288
|
+
entry.lockedBy !== undefined && entry.lockExpiresAt !== undefined;
|
|
289
|
+
const isExistingLockActive =
|
|
290
|
+
hasExistingLock && isLockActive(entry.lockExpiresAt);
|
|
291
|
+
const isSameUser = entry.lockedBy === userId;
|
|
292
|
+
|
|
293
|
+
// Case 1: Entry is locked by another user with an active lock
|
|
294
|
+
if (isExistingLockActive && !isSameUser) {
|
|
295
|
+
return {
|
|
296
|
+
success: false,
|
|
297
|
+
error: `Entry is locked by another user`,
|
|
298
|
+
currentLockHolder: entry.lockedBy,
|
|
299
|
+
currentLockExpiresAt: entry.lockExpiresAt,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Case 2: Same user re-acquiring (renew) OR expired lock OR no lock
|
|
304
|
+
// Acquire/renew the lock
|
|
305
|
+
await ctx.db.patch(id, {
|
|
306
|
+
lockedBy: userId,
|
|
307
|
+
lockExpiresAt: newLockExpiresAt,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Fetch updated entry
|
|
311
|
+
const updatedEntry = await ctx.db.get(id);
|
|
312
|
+
if (!updatedEntry) {
|
|
313
|
+
return {
|
|
314
|
+
success: false,
|
|
315
|
+
error: "Failed to retrieve updated entry",
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
success: true,
|
|
321
|
+
entry: updatedEntry,
|
|
322
|
+
};
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Mutation to release a lock on a content entry.
|
|
328
|
+
*
|
|
329
|
+
* Only the lock owner can release their lock. This should be called
|
|
330
|
+
* when the user finishes editing or navigates away from the editor.
|
|
331
|
+
*
|
|
332
|
+
* @param id - The content entry ID to unlock
|
|
333
|
+
* @param userId - User ID releasing the lock (must match lock owner)
|
|
334
|
+
* @returns The unlocked content entry
|
|
335
|
+
*
|
|
336
|
+
* @throws Error if entry not found
|
|
337
|
+
* @throws Error if entry not locked by this user
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* ```typescript
|
|
341
|
+
* const entry = await ctx.runMutation(api.contentLock.releaseLock, {
|
|
342
|
+
* id: entryId,
|
|
343
|
+
* userId: currentUserId,
|
|
344
|
+
* });
|
|
345
|
+
* console.log("Lock released");
|
|
346
|
+
* ```
|
|
347
|
+
*/
|
|
348
|
+
export const releaseLock = mutation({
|
|
349
|
+
args: releaseLockArgs.fields,
|
|
350
|
+
returns: contentEntryDoc,
|
|
351
|
+
handler: async (ctx, args) => {
|
|
352
|
+
const { id, userId } = args;
|
|
353
|
+
|
|
354
|
+
const entry = await ctx.db.get(id);
|
|
355
|
+
if (!entry) {
|
|
356
|
+
throw new Error(`Content entry not found: ${id}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Verify the user owns the lock
|
|
360
|
+
if (entry.lockedBy !== userId) {
|
|
361
|
+
if (entry.lockedBy === undefined) {
|
|
362
|
+
throw new Error(`Content entry is not locked: ${id}`);
|
|
363
|
+
}
|
|
364
|
+
throw new Error(`Cannot release lock: entry is locked by another user`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Release the lock
|
|
368
|
+
await ctx.db.patch(id, {
|
|
369
|
+
lockedBy: undefined,
|
|
370
|
+
lockExpiresAt: undefined,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const updatedEntry = await ctx.db.get(id);
|
|
374
|
+
if (!updatedEntry) {
|
|
375
|
+
throw new Error("Failed to retrieve updated entry");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return updatedEntry;
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Mutation to force-release a lock (admin operation).
|
|
384
|
+
*
|
|
385
|
+
* Allows administrators to remove locks from entries locked by other users.
|
|
386
|
+
* This should be used sparingly - only when a user has abandoned an editing
|
|
387
|
+
* session without releasing their lock, and the auto-expiry hasn't occurred yet.
|
|
388
|
+
*
|
|
389
|
+
* @param id - The content entry ID to force unlock
|
|
390
|
+
* @param releasedBy - User ID performing the force release (for audit trail)
|
|
391
|
+
* @returns The unlocked content entry
|
|
392
|
+
*
|
|
393
|
+
* @throws Error if entry not found
|
|
394
|
+
* @throws Error if entry is not locked
|
|
395
|
+
*
|
|
396
|
+
* @example
|
|
397
|
+
* ```typescript
|
|
398
|
+
* // Admin forcing release of abandoned lock
|
|
399
|
+
* const entry = await ctx.runMutation(api.contentLock.forceReleaseLock, {
|
|
400
|
+
* id: entryId,
|
|
401
|
+
* releasedBy: adminUserId,
|
|
402
|
+
* });
|
|
403
|
+
* console.log("Lock forcibly released");
|
|
404
|
+
* ```
|
|
405
|
+
*/
|
|
406
|
+
export const forceReleaseLock = mutation({
|
|
407
|
+
args: forceReleaseLockArgs.fields,
|
|
408
|
+
returns: contentEntryDoc,
|
|
409
|
+
handler: async (ctx, args) => {
|
|
410
|
+
const { id, releasedBy } = args;
|
|
411
|
+
|
|
412
|
+
const entry = await ctx.db.get(id);
|
|
413
|
+
if (!entry) {
|
|
414
|
+
throw new Error(`Content entry not found: ${id}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Check if entry is actually locked
|
|
418
|
+
if (entry.lockedBy === undefined) {
|
|
419
|
+
throw new Error(`Content entry is not locked: ${id}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Force release the lock
|
|
423
|
+
await ctx.db.patch(id, {
|
|
424
|
+
lockedBy: undefined,
|
|
425
|
+
lockExpiresAt: undefined,
|
|
426
|
+
// Track who force-released in updatedBy for audit purposes
|
|
427
|
+
updatedBy: releasedBy,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const updatedEntry = await ctx.db.get(id);
|
|
431
|
+
if (!updatedEntry) {
|
|
432
|
+
throw new Error("Failed to retrieve updated entry");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return updatedEntry;
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Mutation to renew an existing lock.
|
|
441
|
+
*
|
|
442
|
+
* Extends the lock expiration time for continued editing sessions.
|
|
443
|
+
* Only the lock owner can renew their lock.
|
|
444
|
+
*
|
|
445
|
+
* This is typically called periodically by the client to keep the lock
|
|
446
|
+
* active during long editing sessions.
|
|
447
|
+
*
|
|
448
|
+
* @param id - The content entry ID whose lock to renew
|
|
449
|
+
* @param userId - User ID renewing the lock (must match lock owner)
|
|
450
|
+
* @param lockDuration - Optional new lock duration (default 30 min, max 4 hours)
|
|
451
|
+
* @returns The entry with renewed lock
|
|
452
|
+
*
|
|
453
|
+
* @throws Error if entry not found
|
|
454
|
+
* @throws Error if entry not locked by this user
|
|
455
|
+
*
|
|
456
|
+
* @example
|
|
457
|
+
* ```typescript
|
|
458
|
+
* // Renew lock every 15 minutes during editing
|
|
459
|
+
* setInterval(async () => {
|
|
460
|
+
* await ctx.runMutation(api.contentLock.renewLock, {
|
|
461
|
+
* id: entryId,
|
|
462
|
+
* userId: currentUserId,
|
|
463
|
+
* });
|
|
464
|
+
* }, 15 * 60 * 1000);
|
|
465
|
+
* ```
|
|
466
|
+
*/
|
|
467
|
+
export const renewLock = mutation({
|
|
468
|
+
args: renewLockArgs.fields,
|
|
469
|
+
returns: contentEntryDoc,
|
|
470
|
+
handler: async (ctx, args) => {
|
|
471
|
+
const { id, userId, lockDuration } = args;
|
|
472
|
+
|
|
473
|
+
const entry = await ctx.db.get(id);
|
|
474
|
+
if (!entry) {
|
|
475
|
+
throw new Error(`Content entry not found: ${id}`);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Verify the user owns the lock
|
|
479
|
+
if (entry.lockedBy !== userId) {
|
|
480
|
+
if (entry.lockedBy === undefined) {
|
|
481
|
+
throw new Error(`Content entry is not locked: ${id}`);
|
|
482
|
+
}
|
|
483
|
+
throw new Error(`Cannot renew lock: entry is locked by another user`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Check if lock has already expired
|
|
487
|
+
if (!isLockActive(entry.lockExpiresAt)) {
|
|
488
|
+
throw new Error(
|
|
489
|
+
`Lock has expired and cannot be renewed. Please acquire a new lock.`,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Calculate new lock expiration
|
|
494
|
+
const validDuration = validateLockDuration(lockDuration);
|
|
495
|
+
const now = Date.now();
|
|
496
|
+
const newLockExpiresAt = now + validDuration;
|
|
497
|
+
|
|
498
|
+
// Renew the lock
|
|
499
|
+
await ctx.db.patch(id, {
|
|
500
|
+
lockExpiresAt: newLockExpiresAt,
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const updatedEntry = await ctx.db.get(id);
|
|
504
|
+
if (!updatedEntry) {
|
|
505
|
+
throw new Error("Failed to retrieve updated entry");
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return updatedEntry;
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// =============================================================================
|
|
513
|
+
// Internal Helper for Update Validation
|
|
514
|
+
// =============================================================================
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Validates that a user can update a locked entry.
|
|
518
|
+
* This is exported for use by contentEntryMutations.
|
|
519
|
+
*
|
|
520
|
+
* @param entry - The content entry to check
|
|
521
|
+
* @param userId - The user attempting the update
|
|
522
|
+
* @returns Object with isAllowed boolean and optional error message
|
|
523
|
+
*/
|
|
524
|
+
export function validateLockForUpdate(
|
|
525
|
+
entry: { lockedBy?: string; lockExpiresAt?: number },
|
|
526
|
+
userId: string | undefined,
|
|
527
|
+
): { isAllowed: boolean; error?: string } {
|
|
528
|
+
// If no lock, update is allowed
|
|
529
|
+
if (entry.lockedBy === undefined || entry.lockExpiresAt === undefined) {
|
|
530
|
+
return { isAllowed: true };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// If lock has expired, update is allowed
|
|
534
|
+
if (!isLockActive(entry.lockExpiresAt)) {
|
|
535
|
+
return { isAllowed: true };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// If same user holds the lock, update is allowed
|
|
539
|
+
if (userId && entry.lockedBy === userId) {
|
|
540
|
+
return { isAllowed: true };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Another user holds an active lock
|
|
544
|
+
return {
|
|
545
|
+
isAllowed: false,
|
|
546
|
+
error: `Cannot update: entry is locked by user ${
|
|
547
|
+
entry.lockedBy
|
|
548
|
+
}. Lock expires at ${new Date(entry.lockExpiresAt).toISOString()}`,
|
|
549
|
+
};
|
|
550
|
+
}
|