convex-cms 0.0.2 → 0.0.5-alpha.0

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 (311) hide show
  1. package/README.md +109 -13
  2. package/admin-dist/nitro.json +15 -0
  3. package/admin-dist/public/assets/CmsEmptyState-CiMQwSQV.js +5 -0
  4. package/admin-dist/public/assets/CmsPageHeader-ohOq0luT.js +1 -0
  5. package/admin-dist/public/assets/CmsStatusBadge-BdNf0V9v.js +1 -0
  6. package/admin-dist/public/assets/CmsSurface-CWup6Jh7.js +1 -0
  7. package/admin-dist/public/assets/CmsToolbar-cEBlCHa3.js +1 -0
  8. package/admin-dist/public/assets/ContentEntryEditor-BY5ypfUs.js +4 -0
  9. package/admin-dist/public/assets/ErrorState-C4nJ-ml4.js +1 -0
  10. package/admin-dist/public/assets/TaxonomyFilter-BgE_SR_O.js +1 -0
  11. package/admin-dist/public/assets/_contentTypeId-DtZectcC.js +1 -0
  12. package/admin-dist/public/assets/_entryId-BpSmrfAm.js +1 -0
  13. package/admin-dist/public/assets/alert-Bf2l8kxw.js +1 -0
  14. package/admin-dist/public/assets/badge-qPrc4AUM.js +1 -0
  15. package/admin-dist/public/assets/circle-check-big-Dgozy3vV.js +1 -0
  16. package/admin-dist/public/assets/command-QOmNhlb0.js +1 -0
  17. package/admin-dist/public/assets/content-OEBGlxg1.js +1 -0
  18. package/admin-dist/public/assets/content-types-CjQliqVV.js +2 -0
  19. package/admin-dist/public/assets/globals-hAmgC66w.css +1 -0
  20. package/admin-dist/public/assets/index-BH_ECMhv.js +1 -0
  21. package/admin-dist/public/assets/label-DCsUdvFh.js +1 -0
  22. package/admin-dist/public/assets/link-2-Czw1N61H.js +1 -0
  23. package/admin-dist/public/assets/list-DtCsXj8-.js +1 -0
  24. package/admin-dist/public/assets/main-CXgkZMhe.js +97 -0
  25. package/admin-dist/public/assets/media-DTJ3-ViE.js +1 -0
  26. package/admin-dist/public/assets/new._contentTypeId-CoTDxKzf.js +1 -0
  27. package/admin-dist/public/assets/plus-xCFJK0RC.js +1 -0
  28. package/admin-dist/public/assets/rotate-ccw-DIqK63wY.js +1 -0
  29. package/admin-dist/public/assets/scroll-area-B-yrE66a.js +1 -0
  30. package/admin-dist/public/assets/search-CbCbboeU.js +1 -0
  31. package/admin-dist/public/assets/select-Co3TZFJb.js +1 -0
  32. package/admin-dist/public/assets/settings-BspTTv_o.js +1 -0
  33. package/admin-dist/public/assets/switch-CfavASmR.js +1 -0
  34. package/admin-dist/public/assets/tabs-CN5s5u2W.js +1 -0
  35. package/admin-dist/public/assets/tanstack-adapter-npeE3RdY.js +1 -0
  36. package/admin-dist/public/assets/taxonomies-CgG46fIF.js +1 -0
  37. package/admin-dist/public/assets/textarea-BJ0XFZpT.js +1 -0
  38. package/admin-dist/public/assets/trash-B3daldm5.js +1 -0
  39. package/admin-dist/public/assets/triangle-alert-BZRcqsUg.js +1 -0
  40. package/admin-dist/public/assets/useBreadcrumbLabel-DwZlwvFF.js +1 -0
  41. package/admin-dist/public/assets/usePermissions-C1JQhfqb.js +1 -0
  42. package/admin-dist/public/favicon.ico +0 -0
  43. package/admin-dist/server/_chunks/_libs/@date-fns/tz.mjs +217 -0
  44. package/admin-dist/server/_chunks/_libs/@floating-ui/core.mjs +719 -0
  45. package/admin-dist/server/_chunks/_libs/@floating-ui/dom.mjs +622 -0
  46. package/admin-dist/server/_chunks/_libs/@floating-ui/react-dom.mjs +292 -0
  47. package/admin-dist/server/_chunks/_libs/@floating-ui/utils.mjs +320 -0
  48. package/admin-dist/server/_chunks/_libs/@radix-ui/number.mjs +6 -0
  49. package/admin-dist/server/_chunks/_libs/@radix-ui/primitive.mjs +11 -0
  50. package/admin-dist/server/_chunks/_libs/@radix-ui/react-arrow.mjs +23 -0
  51. package/admin-dist/server/_chunks/_libs/@radix-ui/react-avatar.mjs +119 -0
  52. package/admin-dist/server/_chunks/_libs/@radix-ui/react-checkbox.mjs +270 -0
  53. package/admin-dist/server/_chunks/_libs/@radix-ui/react-collection.mjs +69 -0
  54. package/admin-dist/server/_chunks/_libs/@radix-ui/react-compose-refs.mjs +39 -0
  55. package/admin-dist/server/_chunks/_libs/@radix-ui/react-context.mjs +137 -0
  56. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dialog.mjs +325 -0
  57. package/admin-dist/server/_chunks/_libs/@radix-ui/react-direction.mjs +9 -0
  58. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dismissable-layer.mjs +210 -0
  59. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dropdown-menu.mjs +253 -0
  60. package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-guards.mjs +29 -0
  61. package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-scope.mjs +206 -0
  62. package/admin-dist/server/_chunks/_libs/@radix-ui/react-id.mjs +14 -0
  63. package/admin-dist/server/_chunks/_libs/@radix-ui/react-label.mjs +23 -0
  64. package/admin-dist/server/_chunks/_libs/@radix-ui/react-menu.mjs +812 -0
  65. package/admin-dist/server/_chunks/_libs/@radix-ui/react-popover.mjs +300 -0
  66. package/admin-dist/server/_chunks/_libs/@radix-ui/react-popper.mjs +286 -0
  67. package/admin-dist/server/_chunks/_libs/@radix-ui/react-portal.mjs +16 -0
  68. package/admin-dist/server/_chunks/_libs/@radix-ui/react-presence.mjs +128 -0
  69. package/admin-dist/server/_chunks/_libs/@radix-ui/react-primitive.mjs +141 -0
  70. package/admin-dist/server/_chunks/_libs/@radix-ui/react-roving-focus.mjs +224 -0
  71. package/admin-dist/server/_chunks/_libs/@radix-ui/react-scroll-area.mjs +721 -0
  72. package/admin-dist/server/_chunks/_libs/@radix-ui/react-select.mjs +1163 -0
  73. package/admin-dist/server/_chunks/_libs/@radix-ui/react-separator.mjs +28 -0
  74. package/admin-dist/server/_chunks/_libs/@radix-ui/react-slot.mjs +601 -0
  75. package/admin-dist/server/_chunks/_libs/@radix-ui/react-switch.mjs +152 -0
  76. package/admin-dist/server/_chunks/_libs/@radix-ui/react-tabs.mjs +189 -0
  77. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-callback-ref.mjs +11 -0
  78. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-controllable-state.mjs +69 -0
  79. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-effect-event.mjs +1 -0
  80. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-escape-keydown.mjs +17 -0
  81. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-is-hydrated.mjs +15 -0
  82. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-layout-effect.mjs +6 -0
  83. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-previous.mjs +14 -0
  84. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-size.mjs +39 -0
  85. package/admin-dist/server/_chunks/_libs/@radix-ui/react-visually-hidden.mjs +33 -0
  86. package/admin-dist/server/_chunks/_libs/@tanstack/history.mjs +409 -0
  87. package/admin-dist/server/_chunks/_libs/@tanstack/react-router.mjs +1718 -0
  88. package/admin-dist/server/_chunks/_libs/@tanstack/react-store.mjs +56 -0
  89. package/admin-dist/server/_chunks/_libs/@tanstack/router-core.mjs +4829 -0
  90. package/admin-dist/server/_chunks/_libs/@tanstack/store.mjs +134 -0
  91. package/admin-dist/server/_chunks/_libs/react-dom.mjs +10781 -0
  92. package/admin-dist/server/_chunks/_libs/react.mjs +513 -0
  93. package/admin-dist/server/_libs/aria-hidden.mjs +122 -0
  94. package/admin-dist/server/_libs/class-variance-authority.mjs +44 -0
  95. package/admin-dist/server/_libs/clsx.mjs +16 -0
  96. package/admin-dist/server/_libs/cmdk.mjs +315 -0
  97. package/admin-dist/server/_libs/convex.mjs +4841 -0
  98. package/admin-dist/server/_libs/cookie-es.mjs +58 -0
  99. package/admin-dist/server/_libs/croner.mjs +1 -0
  100. package/admin-dist/server/_libs/crossws.mjs +1 -0
  101. package/admin-dist/server/_libs/date-fns.mjs +1716 -0
  102. package/admin-dist/server/_libs/detect-node-es.mjs +1 -0
  103. package/admin-dist/server/_libs/get-nonce.mjs +9 -0
  104. package/admin-dist/server/_libs/h3-v2.mjs +277 -0
  105. package/admin-dist/server/_libs/h3.mjs +401 -0
  106. package/admin-dist/server/_libs/hookable.mjs +1 -0
  107. package/admin-dist/server/_libs/isbot.mjs +20 -0
  108. package/admin-dist/server/_libs/lucide-react.mjs +850 -0
  109. package/admin-dist/server/_libs/ohash.mjs +1 -0
  110. package/admin-dist/server/_libs/react-day-picker.mjs +2201 -0
  111. package/admin-dist/server/_libs/react-remove-scroll-bar.mjs +82 -0
  112. package/admin-dist/server/_libs/react-remove-scroll.mjs +328 -0
  113. package/admin-dist/server/_libs/react-style-singleton.mjs +69 -0
  114. package/admin-dist/server/_libs/rou3.mjs +8 -0
  115. package/admin-dist/server/_libs/seroval-plugins.mjs +58 -0
  116. package/admin-dist/server/_libs/seroval.mjs +1765 -0
  117. package/admin-dist/server/_libs/srvx.mjs +719 -0
  118. package/admin-dist/server/_libs/tailwind-merge.mjs +3010 -0
  119. package/admin-dist/server/_libs/tiny-invariant.mjs +12 -0
  120. package/admin-dist/server/_libs/tiny-warning.mjs +5 -0
  121. package/admin-dist/server/_libs/tslib.mjs +39 -0
  122. package/admin-dist/server/_libs/ufo.mjs +54 -0
  123. package/admin-dist/server/_libs/unctx.mjs +1 -0
  124. package/admin-dist/server/_libs/unstorage.mjs +1 -0
  125. package/admin-dist/server/_libs/use-callback-ref.mjs +66 -0
  126. package/admin-dist/server/_libs/use-sidecar.mjs +106 -0
  127. package/admin-dist/server/_libs/use-sync-external-store.mjs +139 -0
  128. package/admin-dist/server/_libs/zod.mjs +4223 -0
  129. package/admin-dist/server/_ssr/CmsButton-B45JAKR1.mjs +125 -0
  130. package/admin-dist/server/_ssr/CmsEmptyState-D_BQFAVR.mjs +290 -0
  131. package/admin-dist/server/_ssr/CmsPageHeader-CrUZA59A.mjs +24 -0
  132. package/admin-dist/server/_ssr/CmsStatusBadge-B-sj6yaj.mjs +127 -0
  133. package/admin-dist/server/_ssr/CmsSurface-DKJZhpjk.mjs +44 -0
  134. package/admin-dist/server/_ssr/CmsToolbar-ByaW5iXf.mjs +49 -0
  135. package/admin-dist/server/_ssr/ContentEntryEditor-D3_Jb1dq.mjs +3720 -0
  136. package/admin-dist/server/_ssr/ErrorState-cI-bKLez.mjs +89 -0
  137. package/admin-dist/server/_ssr/TaxonomyFilter-BRJkuCtA.mjs +188 -0
  138. package/admin-dist/server/_ssr/_contentTypeId-B9kA6CaM.mjs +379 -0
  139. package/admin-dist/server/_ssr/_entryId-BddcMkZN.mjs +161 -0
  140. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-Dd7AmelK.mjs +4 -0
  141. package/admin-dist/server/_ssr/command-CGtVr8Gb.mjs +128 -0
  142. package/admin-dist/server/_ssr/config.server-D7JHDcDv.mjs +117 -0
  143. package/admin-dist/server/_ssr/content-D1tbeOd0.mjs +647 -0
  144. package/admin-dist/server/_ssr/content-types-BZqY_BER.mjs +1342 -0
  145. package/admin-dist/server/_ssr/index-BIdq4xaC.mjs +264 -0
  146. package/admin-dist/server/_ssr/index.mjs +1275 -0
  147. package/admin-dist/server/_ssr/label-T-QNKAr6.mjs +22 -0
  148. package/admin-dist/server/_ssr/media-C-xqjBrl.mjs +1832 -0
  149. package/admin-dist/server/_ssr/new._contentTypeId-DWic9cRq.mjs +144 -0
  150. package/admin-dist/server/_ssr/router-D1BMAMJT.mjs +1556 -0
  151. package/admin-dist/server/_ssr/scroll-area-C0pic_WA.mjs +59 -0
  152. package/admin-dist/server/_ssr/select-CqmuN2F6.mjs +142 -0
  153. package/admin-dist/server/_ssr/settings-CAkncGGV.mjs +430 -0
  154. package/admin-dist/server/_ssr/start-HYkvq4Ni.mjs +4 -0
  155. package/admin-dist/server/_ssr/switch-CgmuJkT9.mjs +31 -0
  156. package/admin-dist/server/_ssr/tabs-CnMj0aRy.mjs +630 -0
  157. package/admin-dist/server/_ssr/tanstack-adapter-BXZrMauE.mjs +119 -0
  158. package/admin-dist/server/_ssr/taxonomies-thl3BfVm.mjs +1015 -0
  159. package/admin-dist/server/_ssr/textarea-4K5OJgeh.mjs +18 -0
  160. package/admin-dist/server/_ssr/trash-B40Gx5zP.mjs +411 -0
  161. package/admin-dist/server/_ssr/useBreadcrumbLabel-rn-fL4zV.mjs +16 -0
  162. package/admin-dist/server/_ssr/usePermissions-CKeM6_Vw.mjs +68 -0
  163. package/admin-dist/server/favicon.ico +0 -0
  164. package/admin-dist/server/index.mjs +641 -0
  165. package/dist/cli/commands/init.d.ts +6 -0
  166. package/dist/cli/commands/init.d.ts.map +1 -0
  167. package/dist/cli/commands/init.js +156 -0
  168. package/dist/cli/commands/init.js.map +1 -0
  169. package/dist/cli/index.js +6 -0
  170. package/dist/cli/index.js.map +1 -1
  171. package/dist/client/admin-config.d.ts +2 -3
  172. package/dist/client/admin-config.d.ts.map +1 -1
  173. package/dist/client/admin-config.js +2 -3
  174. package/dist/client/admin-config.js.map +1 -1
  175. package/dist/client/adminApi.d.ts +1877 -1851
  176. package/dist/client/adminApi.d.ts.map +1 -1
  177. package/dist/client/adminApi.js +649 -629
  178. package/dist/client/adminApi.js.map +1 -1
  179. package/dist/client/agentTools.d.ts +1231 -139
  180. package/dist/client/agentTools.d.ts.map +1 -1
  181. package/dist/client/agentTools.js +37 -13
  182. package/dist/client/agentTools.js.map +1 -1
  183. package/dist/client/index.d.ts +5 -5
  184. package/dist/client/index.d.ts.map +1 -1
  185. package/dist/client/index.js +4 -4
  186. package/dist/client/index.js.map +1 -1
  187. package/dist/client/schema/codegen.d.ts +2 -2
  188. package/dist/client/schema/codegen.d.ts.map +1 -1
  189. package/dist/client/schema/codegen.js +3 -3
  190. package/dist/client/schema/codegen.js.map +1 -1
  191. package/dist/client/schema/defineContentType.d.ts +3 -3
  192. package/dist/client/schema/defineContentType.js +3 -3
  193. package/dist/client/schema/index.d.ts +7 -7
  194. package/dist/client/schema/index.d.ts.map +1 -1
  195. package/dist/client/schema/index.js +5 -5
  196. package/dist/client/schema/index.js.map +1 -1
  197. package/dist/client/schema/schemaDrift.d.ts +1 -1
  198. package/dist/client/schema/schemaDrift.js +1 -1
  199. package/dist/client/schema/typedClient.d.ts +2 -2
  200. package/dist/client/schema/typedClient.js +2 -2
  201. package/dist/client/schema/types.d.ts +1 -1
  202. package/dist/client/schema/types.js +1 -1
  203. package/dist/client/wrapper.d.ts +108 -65
  204. package/dist/client/wrapper.d.ts.map +1 -1
  205. package/dist/client/wrapper.js +22 -22
  206. package/dist/client/wrapper.js.map +1 -1
  207. package/dist/component/_generated/component.d.ts +9 -0
  208. package/dist/component/_generated/component.d.ts.map +1 -1
  209. package/dist/component/convex.config.d.ts +2 -2
  210. package/dist/component/convex.config.js +2 -2
  211. package/dist/component/index.d.ts +1 -1
  212. package/dist/component/index.js +1 -1
  213. package/dist/component/lib/ragContentChunker.d.ts +1 -1
  214. package/dist/component/lib/ragContentChunker.js +1 -1
  215. package/dist/component/mediaAssets.d.ts +35 -0
  216. package/dist/component/mediaAssets.d.ts.map +1 -1
  217. package/dist/component/mediaAssets.js +81 -0
  218. package/dist/component/mediaAssets.js.map +1 -1
  219. package/dist/component/roles.d.ts +1 -1
  220. package/dist/component/roles.js +1 -1
  221. package/dist/react/index.d.ts +2 -2
  222. package/dist/react/index.d.ts.map +1 -1
  223. package/dist/react/index.js +13 -7
  224. package/dist/react/index.js.map +1 -1
  225. package/dist/test.d.ts +2 -2
  226. package/dist/test.d.ts.map +1 -1
  227. package/dist/test.js +4 -3
  228. package/dist/test.js.map +1 -1
  229. package/package.json +37 -13
  230. package/dist/component/auditLog.d.ts +0 -410
  231. package/dist/component/auditLog.d.ts.map +0 -1
  232. package/dist/component/auditLog.js +0 -607
  233. package/dist/component/auditLog.js.map +0 -1
  234. package/dist/component/types.d.ts +0 -4
  235. package/dist/component/types.d.ts.map +0 -1
  236. package/dist/component/types.js +0 -2
  237. package/dist/component/types.js.map +0 -1
  238. package/src/cli/commands/admin.ts +0 -104
  239. package/src/cli/index.ts +0 -21
  240. package/src/cli/utils/detectConvexUrl.ts +0 -54
  241. package/src/cli/utils/openBrowser.ts +0 -16
  242. package/src/client/admin-config.ts +0 -138
  243. package/src/client/adminApi.ts +0 -942
  244. package/src/client/agentTools.ts +0 -1311
  245. package/src/client/argTypes.ts +0 -316
  246. package/src/client/field-types.ts +0 -187
  247. package/src/client/index.ts +0 -1301
  248. package/src/client/queryBuilder.ts +0 -1100
  249. package/src/client/schema/codegen.ts +0 -500
  250. package/src/client/schema/defineContentType.ts +0 -501
  251. package/src/client/schema/index.ts +0 -169
  252. package/src/client/schema/schemaDrift.ts +0 -574
  253. package/src/client/schema/typedClient.ts +0 -688
  254. package/src/client/schema/types.ts +0 -666
  255. package/src/client/types.ts +0 -723
  256. package/src/client/workflows.ts +0 -141
  257. package/src/client/wrapper.ts +0 -4304
  258. package/src/component/_generated/api.ts +0 -140
  259. package/src/component/_generated/component.ts +0 -5029
  260. package/src/component/_generated/dataModel.ts +0 -60
  261. package/src/component/_generated/server.ts +0 -156
  262. package/src/component/authorization.ts +0 -647
  263. package/src/component/authorizationHooks.ts +0 -668
  264. package/src/component/bulkOperations.ts +0 -687
  265. package/src/component/contentEntries.ts +0 -1976
  266. package/src/component/contentEntryMutations.ts +0 -1223
  267. package/src/component/contentEntryValidation.ts +0 -707
  268. package/src/component/contentLock.ts +0 -550
  269. package/src/component/contentTypeMigration.ts +0 -1064
  270. package/src/component/contentTypeMutations.ts +0 -969
  271. package/src/component/contentTypes.ts +0 -346
  272. package/src/component/convex.config.ts +0 -44
  273. package/src/component/documentTypes.ts +0 -240
  274. package/src/component/eventEmitter.ts +0 -485
  275. package/src/component/exportImport.ts +0 -1169
  276. package/src/component/index.ts +0 -491
  277. package/src/component/lib/deepReferenceResolver.ts +0 -999
  278. package/src/component/lib/errors.ts +0 -816
  279. package/src/component/lib/index.ts +0 -145
  280. package/src/component/lib/mediaReferenceResolver.ts +0 -495
  281. package/src/component/lib/metadataExtractor.ts +0 -792
  282. package/src/component/lib/mutationAuth.ts +0 -199
  283. package/src/component/lib/queries.ts +0 -79
  284. package/src/component/lib/ragContentChunker.ts +0 -1371
  285. package/src/component/lib/referenceResolver.ts +0 -430
  286. package/src/component/lib/slugGenerator.ts +0 -262
  287. package/src/component/lib/slugUniqueness.ts +0 -333
  288. package/src/component/lib/softDelete.ts +0 -44
  289. package/src/component/localeFallbackChain.ts +0 -673
  290. package/src/component/localeFields.ts +0 -896
  291. package/src/component/mediaAssetMutations.ts +0 -725
  292. package/src/component/mediaAssets.ts +0 -932
  293. package/src/component/mediaFolderMutations.ts +0 -1046
  294. package/src/component/mediaUploadMutations.ts +0 -224
  295. package/src/component/mediaVariantMutations.ts +0 -900
  296. package/src/component/mediaVariants.ts +0 -793
  297. package/src/component/ragContentIndexer.ts +0 -1067
  298. package/src/component/rateLimitHooks.ts +0 -572
  299. package/src/component/roles.ts +0 -1360
  300. package/src/component/scheduledPublish.ts +0 -358
  301. package/src/component/schema.ts +0 -617
  302. package/src/component/taxonomies.ts +0 -949
  303. package/src/component/taxonomyMutations.ts +0 -1210
  304. package/src/component/trash.ts +0 -724
  305. package/src/component/userContext.ts +0 -898
  306. package/src/component/validation.ts +0 -1388
  307. package/src/component/validators.ts +0 -949
  308. package/src/component/versionMutations.ts +0 -392
  309. package/src/component/webhookTrigger.ts +0 -1922
  310. package/src/react/index.ts +0 -898
  311. package/src/test.ts +0 -1580
@@ -1,550 +0,0 @@
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
- }