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.
Files changed (379) hide show
  1. package/dist/cli/commands/admin.d.ts +16 -0
  2. package/dist/cli/commands/admin.d.ts.map +1 -0
  3. package/dist/cli/commands/admin.js +88 -0
  4. package/dist/cli/commands/admin.js.map +1 -0
  5. package/dist/cli/index.d.ts +3 -0
  6. package/dist/cli/index.d.ts.map +1 -0
  7. package/dist/cli/index.js +18 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/cli/utils/detectConvexUrl.d.ts +13 -0
  10. package/dist/cli/utils/detectConvexUrl.d.ts.map +1 -0
  11. package/dist/cli/utils/detectConvexUrl.js +48 -0
  12. package/dist/cli/utils/detectConvexUrl.js.map +1 -0
  13. package/dist/cli/utils/openBrowser.d.ts +7 -0
  14. package/dist/cli/utils/openBrowser.d.ts.map +1 -0
  15. package/dist/cli/utils/openBrowser.js +17 -0
  16. package/dist/cli/utils/openBrowser.js.map +1 -0
  17. package/dist/client/admin-config.d.ts +126 -0
  18. package/dist/client/admin-config.d.ts.map +1 -0
  19. package/dist/client/admin-config.js +117 -0
  20. package/dist/client/admin-config.js.map +1 -0
  21. package/dist/client/adminApi.d.ts +2273 -0
  22. package/dist/client/adminApi.d.ts.map +1 -0
  23. package/dist/client/adminApi.js +716 -0
  24. package/dist/client/adminApi.js.map +1 -0
  25. package/dist/client/agentTools.d.ts +933 -0
  26. package/dist/client/agentTools.d.ts.map +1 -0
  27. package/dist/client/agentTools.js +1004 -0
  28. package/dist/client/agentTools.js.map +1 -0
  29. package/dist/client/argTypes.d.ts +212 -0
  30. package/dist/client/argTypes.d.ts.map +1 -0
  31. package/dist/client/argTypes.js +5 -0
  32. package/dist/client/argTypes.js.map +1 -0
  33. package/dist/client/field-types.d.ts +55 -0
  34. package/dist/client/field-types.d.ts.map +1 -0
  35. package/dist/client/field-types.js +152 -0
  36. package/dist/client/field-types.js.map +1 -0
  37. package/dist/client/index.d.ts +189 -0
  38. package/dist/client/index.d.ts.map +1 -0
  39. package/dist/client/index.js +668 -0
  40. package/dist/client/index.js.map +1 -0
  41. package/dist/client/queryBuilder.d.ts +765 -0
  42. package/dist/client/queryBuilder.d.ts.map +1 -0
  43. package/dist/client/queryBuilder.js +970 -0
  44. package/dist/client/queryBuilder.js.map +1 -0
  45. package/dist/client/schema/codegen.d.ts +128 -0
  46. package/dist/client/schema/codegen.d.ts.map +1 -0
  47. package/dist/client/schema/codegen.js +318 -0
  48. package/dist/client/schema/codegen.js.map +1 -0
  49. package/dist/client/schema/defineContentType.d.ts +221 -0
  50. package/dist/client/schema/defineContentType.d.ts.map +1 -0
  51. package/dist/client/schema/defineContentType.js +380 -0
  52. package/dist/client/schema/defineContentType.js.map +1 -0
  53. package/dist/client/schema/index.d.ts +85 -0
  54. package/dist/client/schema/index.d.ts.map +1 -0
  55. package/dist/client/schema/index.js +92 -0
  56. package/dist/client/schema/index.js.map +1 -0
  57. package/dist/client/schema/schemaDrift.d.ts +199 -0
  58. package/dist/client/schema/schemaDrift.d.ts.map +1 -0
  59. package/dist/client/schema/schemaDrift.js +340 -0
  60. package/dist/client/schema/schemaDrift.js.map +1 -0
  61. package/dist/client/schema/typedClient.d.ts +401 -0
  62. package/dist/client/schema/typedClient.d.ts.map +1 -0
  63. package/dist/client/schema/typedClient.js +269 -0
  64. package/dist/client/schema/typedClient.js.map +1 -0
  65. package/dist/client/schema/types.d.ts +477 -0
  66. package/dist/client/schema/types.d.ts.map +1 -0
  67. package/dist/client/schema/types.js +39 -0
  68. package/dist/client/schema/types.js.map +1 -0
  69. package/dist/client/types.d.ts +449 -0
  70. package/dist/client/types.d.ts.map +1 -0
  71. package/dist/client/types.js +149 -0
  72. package/dist/client/types.js.map +1 -0
  73. package/dist/client/workflows.d.ts +51 -0
  74. package/dist/client/workflows.d.ts.map +1 -0
  75. package/dist/client/workflows.js +103 -0
  76. package/dist/client/workflows.js.map +1 -0
  77. package/dist/client/wrapper.d.ts +2198 -0
  78. package/dist/client/wrapper.d.ts.map +1 -0
  79. package/dist/client/wrapper.js +2651 -0
  80. package/dist/client/wrapper.js.map +1 -0
  81. package/dist/component/_generated/api.d.ts +124 -0
  82. package/dist/component/_generated/api.d.ts.map +1 -0
  83. package/dist/component/_generated/api.js +31 -0
  84. package/dist/component/_generated/api.js.map +1 -0
  85. package/dist/component/_generated/component.d.ts +4321 -0
  86. package/dist/component/_generated/component.d.ts.map +1 -0
  87. package/dist/component/_generated/component.js +11 -0
  88. package/dist/component/_generated/component.js.map +1 -0
  89. package/dist/component/_generated/dataModel.d.ts +46 -0
  90. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  91. package/dist/component/_generated/dataModel.js +11 -0
  92. package/dist/component/_generated/dataModel.js.map +1 -0
  93. package/dist/component/_generated/server.d.ts +121 -0
  94. package/dist/component/_generated/server.d.ts.map +1 -0
  95. package/dist/component/_generated/server.js +78 -0
  96. package/dist/component/_generated/server.js.map +1 -0
  97. package/dist/component/auditLog.d.ts +410 -0
  98. package/dist/component/auditLog.d.ts.map +1 -0
  99. package/dist/component/auditLog.js +607 -0
  100. package/dist/component/auditLog.js.map +1 -0
  101. package/dist/component/authorization.d.ts +323 -0
  102. package/dist/component/authorization.d.ts.map +1 -0
  103. package/dist/component/authorization.js +464 -0
  104. package/dist/component/authorization.js.map +1 -0
  105. package/dist/component/authorizationHooks.d.ts +184 -0
  106. package/dist/component/authorizationHooks.d.ts.map +1 -0
  107. package/dist/component/authorizationHooks.js +521 -0
  108. package/dist/component/authorizationHooks.js.map +1 -0
  109. package/dist/component/bulkOperations.d.ts +200 -0
  110. package/dist/component/bulkOperations.d.ts.map +1 -0
  111. package/dist/component/bulkOperations.js +568 -0
  112. package/dist/component/bulkOperations.js.map +1 -0
  113. package/dist/component/contentEntries.d.ts +719 -0
  114. package/dist/component/contentEntries.d.ts.map +1 -0
  115. package/dist/component/contentEntries.js +1617 -0
  116. package/dist/component/contentEntries.js.map +1 -0
  117. package/dist/component/contentEntryMutations.d.ts +505 -0
  118. package/dist/component/contentEntryMutations.d.ts.map +1 -0
  119. package/dist/component/contentEntryMutations.js +1009 -0
  120. package/dist/component/contentEntryMutations.js.map +1 -0
  121. package/dist/component/contentEntryValidation.d.ts +115 -0
  122. package/dist/component/contentEntryValidation.d.ts.map +1 -0
  123. package/dist/component/contentEntryValidation.js +546 -0
  124. package/dist/component/contentEntryValidation.js.map +1 -0
  125. package/dist/component/contentLock.d.ts +328 -0
  126. package/dist/component/contentLock.d.ts.map +1 -0
  127. package/dist/component/contentLock.js +471 -0
  128. package/dist/component/contentLock.js.map +1 -0
  129. package/dist/component/contentTypeMigration.d.ts +411 -0
  130. package/dist/component/contentTypeMigration.d.ts.map +1 -0
  131. package/dist/component/contentTypeMigration.js +805 -0
  132. package/dist/component/contentTypeMigration.js.map +1 -0
  133. package/dist/component/contentTypeMutations.d.ts +975 -0
  134. package/dist/component/contentTypeMutations.d.ts.map +1 -0
  135. package/dist/component/contentTypeMutations.js +768 -0
  136. package/dist/component/contentTypeMutations.js.map +1 -0
  137. package/dist/component/contentTypes.d.ts +538 -0
  138. package/dist/component/contentTypes.d.ts.map +1 -0
  139. package/dist/component/contentTypes.js +304 -0
  140. package/dist/component/contentTypes.js.map +1 -0
  141. package/dist/component/convex.config.d.ts +42 -0
  142. package/dist/component/convex.config.d.ts.map +1 -0
  143. package/dist/component/convex.config.js +43 -0
  144. package/dist/component/convex.config.js.map +1 -0
  145. package/dist/component/documentTypes.d.ts +186 -0
  146. package/dist/component/documentTypes.d.ts.map +1 -0
  147. package/dist/component/documentTypes.js +23 -0
  148. package/dist/component/documentTypes.js.map +1 -0
  149. package/dist/component/eventEmitter.d.ts +281 -0
  150. package/dist/component/eventEmitter.d.ts.map +1 -0
  151. package/dist/component/eventEmitter.js +300 -0
  152. package/dist/component/eventEmitter.js.map +1 -0
  153. package/dist/component/exportImport.d.ts +1120 -0
  154. package/dist/component/exportImport.d.ts.map +1 -0
  155. package/dist/component/exportImport.js +931 -0
  156. package/dist/component/exportImport.js.map +1 -0
  157. package/dist/component/index.d.ts +28 -0
  158. package/dist/component/index.d.ts.map +1 -0
  159. package/dist/component/index.js +142 -0
  160. package/dist/component/index.js.map +1 -0
  161. package/dist/component/lib/deepReferenceResolver.d.ts +252 -0
  162. package/dist/component/lib/deepReferenceResolver.d.ts.map +1 -0
  163. package/dist/component/lib/deepReferenceResolver.js +601 -0
  164. package/dist/component/lib/deepReferenceResolver.js.map +1 -0
  165. package/dist/component/lib/errors.d.ts +306 -0
  166. package/dist/component/lib/errors.d.ts.map +1 -0
  167. package/dist/component/lib/errors.js +407 -0
  168. package/dist/component/lib/errors.js.map +1 -0
  169. package/dist/component/lib/index.d.ts +10 -0
  170. package/dist/component/lib/index.d.ts.map +1 -0
  171. package/dist/component/lib/index.js +33 -0
  172. package/dist/component/lib/index.js.map +1 -0
  173. package/dist/component/lib/mediaReferenceResolver.d.ts +217 -0
  174. package/dist/component/lib/mediaReferenceResolver.d.ts.map +1 -0
  175. package/dist/component/lib/mediaReferenceResolver.js +326 -0
  176. package/dist/component/lib/mediaReferenceResolver.js.map +1 -0
  177. package/dist/component/lib/metadataExtractor.d.ts +245 -0
  178. package/dist/component/lib/metadataExtractor.d.ts.map +1 -0
  179. package/dist/component/lib/metadataExtractor.js +548 -0
  180. package/dist/component/lib/metadataExtractor.js.map +1 -0
  181. package/dist/component/lib/mutationAuth.d.ts +95 -0
  182. package/dist/component/lib/mutationAuth.d.ts.map +1 -0
  183. package/dist/component/lib/mutationAuth.js +146 -0
  184. package/dist/component/lib/mutationAuth.js.map +1 -0
  185. package/dist/component/lib/queries.d.ts +17 -0
  186. package/dist/component/lib/queries.d.ts.map +1 -0
  187. package/dist/component/lib/queries.js +49 -0
  188. package/dist/component/lib/queries.js.map +1 -0
  189. package/dist/component/lib/ragContentChunker.d.ts +423 -0
  190. package/dist/component/lib/ragContentChunker.d.ts.map +1 -0
  191. package/dist/component/lib/ragContentChunker.js +897 -0
  192. package/dist/component/lib/ragContentChunker.js.map +1 -0
  193. package/dist/component/lib/referenceResolver.d.ts +175 -0
  194. package/dist/component/lib/referenceResolver.d.ts.map +1 -0
  195. package/dist/component/lib/referenceResolver.js +293 -0
  196. package/dist/component/lib/referenceResolver.js.map +1 -0
  197. package/dist/component/lib/slugGenerator.d.ts +71 -0
  198. package/dist/component/lib/slugGenerator.d.ts.map +1 -0
  199. package/dist/component/lib/slugGenerator.js +207 -0
  200. package/dist/component/lib/slugGenerator.js.map +1 -0
  201. package/dist/component/lib/slugUniqueness.d.ts +131 -0
  202. package/dist/component/lib/slugUniqueness.d.ts.map +1 -0
  203. package/dist/component/lib/slugUniqueness.js +229 -0
  204. package/dist/component/lib/slugUniqueness.js.map +1 -0
  205. package/dist/component/lib/softDelete.d.ts +18 -0
  206. package/dist/component/lib/softDelete.d.ts.map +1 -0
  207. package/dist/component/lib/softDelete.js +29 -0
  208. package/dist/component/lib/softDelete.js.map +1 -0
  209. package/dist/component/localeFallbackChain.d.ts +410 -0
  210. package/dist/component/localeFallbackChain.d.ts.map +1 -0
  211. package/dist/component/localeFallbackChain.js +467 -0
  212. package/dist/component/localeFallbackChain.js.map +1 -0
  213. package/dist/component/localeFields.d.ts +508 -0
  214. package/dist/component/localeFields.d.ts.map +1 -0
  215. package/dist/component/localeFields.js +592 -0
  216. package/dist/component/localeFields.js.map +1 -0
  217. package/dist/component/mediaAssetMutations.d.ts +235 -0
  218. package/dist/component/mediaAssetMutations.d.ts.map +1 -0
  219. package/dist/component/mediaAssetMutations.js +558 -0
  220. package/dist/component/mediaAssetMutations.js.map +1 -0
  221. package/dist/component/mediaAssets.d.ts +168 -0
  222. package/dist/component/mediaAssets.d.ts.map +1 -0
  223. package/dist/component/mediaAssets.js +618 -0
  224. package/dist/component/mediaAssets.js.map +1 -0
  225. package/dist/component/mediaFolderMutations.d.ts +642 -0
  226. package/dist/component/mediaFolderMutations.d.ts.map +1 -0
  227. package/dist/component/mediaFolderMutations.js +849 -0
  228. package/dist/component/mediaFolderMutations.js.map +1 -0
  229. package/dist/component/mediaUploadMutations.d.ts +136 -0
  230. package/dist/component/mediaUploadMutations.d.ts.map +1 -0
  231. package/dist/component/mediaUploadMutations.js +205 -0
  232. package/dist/component/mediaUploadMutations.js.map +1 -0
  233. package/dist/component/mediaVariantMutations.d.ts +468 -0
  234. package/dist/component/mediaVariantMutations.d.ts.map +1 -0
  235. package/dist/component/mediaVariantMutations.js +737 -0
  236. package/dist/component/mediaVariantMutations.js.map +1 -0
  237. package/dist/component/mediaVariants.d.ts +525 -0
  238. package/dist/component/mediaVariants.d.ts.map +1 -0
  239. package/dist/component/mediaVariants.js +661 -0
  240. package/dist/component/mediaVariants.js.map +1 -0
  241. package/dist/component/ragContentIndexer.d.ts +595 -0
  242. package/dist/component/ragContentIndexer.d.ts.map +1 -0
  243. package/dist/component/ragContentIndexer.js +794 -0
  244. package/dist/component/ragContentIndexer.js.map +1 -0
  245. package/dist/component/rateLimitHooks.d.ts +266 -0
  246. package/dist/component/rateLimitHooks.d.ts.map +1 -0
  247. package/dist/component/rateLimitHooks.js +412 -0
  248. package/dist/component/rateLimitHooks.js.map +1 -0
  249. package/dist/component/roles.d.ts +649 -0
  250. package/dist/component/roles.d.ts.map +1 -0
  251. package/dist/component/roles.js +884 -0
  252. package/dist/component/roles.js.map +1 -0
  253. package/dist/component/scheduledPublish.d.ts +182 -0
  254. package/dist/component/scheduledPublish.d.ts.map +1 -0
  255. package/dist/component/scheduledPublish.js +304 -0
  256. package/dist/component/scheduledPublish.js.map +1 -0
  257. package/dist/component/schema.d.ts +4114 -0
  258. package/dist/component/schema.d.ts.map +1 -0
  259. package/dist/component/schema.js +469 -0
  260. package/dist/component/schema.js.map +1 -0
  261. package/dist/component/taxonomies.d.ts +476 -0
  262. package/dist/component/taxonomies.d.ts.map +1 -0
  263. package/dist/component/taxonomies.js +785 -0
  264. package/dist/component/taxonomies.js.map +1 -0
  265. package/dist/component/taxonomyMutations.d.ts +206 -0
  266. package/dist/component/taxonomyMutations.d.ts.map +1 -0
  267. package/dist/component/taxonomyMutations.js +1001 -0
  268. package/dist/component/taxonomyMutations.js.map +1 -0
  269. package/dist/component/trash.d.ts +265 -0
  270. package/dist/component/trash.d.ts.map +1 -0
  271. package/dist/component/trash.js +621 -0
  272. package/dist/component/trash.js.map +1 -0
  273. package/dist/component/types.d.ts +4 -0
  274. package/dist/component/types.d.ts.map +1 -0
  275. package/dist/component/types.js +2 -0
  276. package/dist/component/types.js.map +1 -0
  277. package/dist/component/userContext.d.ts +508 -0
  278. package/dist/component/userContext.d.ts.map +1 -0
  279. package/dist/component/userContext.js +615 -0
  280. package/dist/component/userContext.js.map +1 -0
  281. package/dist/component/validation.d.ts +387 -0
  282. package/dist/component/validation.d.ts.map +1 -0
  283. package/dist/component/validation.js +1052 -0
  284. package/dist/component/validation.js.map +1 -0
  285. package/dist/component/validators.d.ts +4645 -0
  286. package/dist/component/validators.d.ts.map +1 -0
  287. package/dist/component/validators.js +641 -0
  288. package/dist/component/validators.js.map +1 -0
  289. package/dist/component/versionMutations.d.ts +216 -0
  290. package/dist/component/versionMutations.d.ts.map +1 -0
  291. package/dist/component/versionMutations.js +321 -0
  292. package/dist/component/versionMutations.js.map +1 -0
  293. package/dist/component/webhookTrigger.d.ts +770 -0
  294. package/dist/component/webhookTrigger.d.ts.map +1 -0
  295. package/dist/component/webhookTrigger.js +1413 -0
  296. package/dist/component/webhookTrigger.js.map +1 -0
  297. package/dist/react/index.d.ts +316 -0
  298. package/dist/react/index.d.ts.map +1 -0
  299. package/dist/react/index.js +558 -0
  300. package/dist/react/index.js.map +1 -0
  301. package/dist/test.d.ts +2230 -0
  302. package/dist/test.d.ts.map +1 -0
  303. package/dist/test.js +1107 -0
  304. package/dist/test.js.map +1 -0
  305. package/package.json +95 -0
  306. package/src/cli/commands/admin.ts +104 -0
  307. package/src/cli/index.ts +21 -0
  308. package/src/cli/utils/detectConvexUrl.ts +54 -0
  309. package/src/cli/utils/openBrowser.ts +16 -0
  310. package/src/client/admin-config.ts +138 -0
  311. package/src/client/adminApi.ts +942 -0
  312. package/src/client/agentTools.ts +1311 -0
  313. package/src/client/argTypes.ts +316 -0
  314. package/src/client/field-types.ts +187 -0
  315. package/src/client/index.ts +1301 -0
  316. package/src/client/queryBuilder.ts +1100 -0
  317. package/src/client/schema/codegen.ts +500 -0
  318. package/src/client/schema/defineContentType.ts +501 -0
  319. package/src/client/schema/index.ts +169 -0
  320. package/src/client/schema/schemaDrift.ts +574 -0
  321. package/src/client/schema/typedClient.ts +688 -0
  322. package/src/client/schema/types.ts +666 -0
  323. package/src/client/types.ts +723 -0
  324. package/src/client/workflows.ts +141 -0
  325. package/src/client/wrapper.ts +4304 -0
  326. package/src/component/_generated/api.ts +140 -0
  327. package/src/component/_generated/component.ts +5029 -0
  328. package/src/component/_generated/dataModel.ts +60 -0
  329. package/src/component/_generated/server.ts +156 -0
  330. package/src/component/authorization.ts +647 -0
  331. package/src/component/authorizationHooks.ts +668 -0
  332. package/src/component/bulkOperations.ts +687 -0
  333. package/src/component/contentEntries.ts +1976 -0
  334. package/src/component/contentEntryMutations.ts +1223 -0
  335. package/src/component/contentEntryValidation.ts +707 -0
  336. package/src/component/contentLock.ts +550 -0
  337. package/src/component/contentTypeMigration.ts +1064 -0
  338. package/src/component/contentTypeMutations.ts +969 -0
  339. package/src/component/contentTypes.ts +346 -0
  340. package/src/component/convex.config.ts +44 -0
  341. package/src/component/documentTypes.ts +240 -0
  342. package/src/component/eventEmitter.ts +485 -0
  343. package/src/component/exportImport.ts +1169 -0
  344. package/src/component/index.ts +491 -0
  345. package/src/component/lib/deepReferenceResolver.ts +999 -0
  346. package/src/component/lib/errors.ts +816 -0
  347. package/src/component/lib/index.ts +145 -0
  348. package/src/component/lib/mediaReferenceResolver.ts +495 -0
  349. package/src/component/lib/metadataExtractor.ts +792 -0
  350. package/src/component/lib/mutationAuth.ts +199 -0
  351. package/src/component/lib/queries.ts +79 -0
  352. package/src/component/lib/ragContentChunker.ts +1371 -0
  353. package/src/component/lib/referenceResolver.ts +430 -0
  354. package/src/component/lib/slugGenerator.ts +262 -0
  355. package/src/component/lib/slugUniqueness.ts +333 -0
  356. package/src/component/lib/softDelete.ts +44 -0
  357. package/src/component/localeFallbackChain.ts +673 -0
  358. package/src/component/localeFields.ts +896 -0
  359. package/src/component/mediaAssetMutations.ts +725 -0
  360. package/src/component/mediaAssets.ts +932 -0
  361. package/src/component/mediaFolderMutations.ts +1046 -0
  362. package/src/component/mediaUploadMutations.ts +224 -0
  363. package/src/component/mediaVariantMutations.ts +900 -0
  364. package/src/component/mediaVariants.ts +793 -0
  365. package/src/component/ragContentIndexer.ts +1067 -0
  366. package/src/component/rateLimitHooks.ts +572 -0
  367. package/src/component/roles.ts +1360 -0
  368. package/src/component/scheduledPublish.ts +358 -0
  369. package/src/component/schema.ts +617 -0
  370. package/src/component/taxonomies.ts +949 -0
  371. package/src/component/taxonomyMutations.ts +1210 -0
  372. package/src/component/trash.ts +724 -0
  373. package/src/component/userContext.ts +898 -0
  374. package/src/component/validation.ts +1388 -0
  375. package/src/component/validators.ts +949 -0
  376. package/src/component/versionMutations.ts +392 -0
  377. package/src/component/webhookTrigger.ts +1922 -0
  378. package/src/react/index.ts +898 -0
  379. 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
+ }