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,2651 @@
1
+ /** CMS Client Wrapper with typed method APIs */
2
+ /** @internal Bridges wrapper's simplified types to generated Convex types */
3
+ function _callMutation(ctx, fn, args) {
4
+ return ctx.runMutation(fn, args);
5
+ }
6
+ /** @internal */
7
+ function callQuery(ctx, fn, args) {
8
+ return ctx.runQuery(fn, args);
9
+ }
10
+ import { resolveConfig, AuthorizationNotConfiguredError } from "./types.js";
11
+ // Import query builder
12
+ import { createQueryBuilder } from "./queryBuilder.js";
13
+ // Import authorization hooks execution
14
+ import { executeAuthorizationHooks, contextToRbacOptions, operationToRbac, } from "../component/authorizationHooks.js";
15
+ // Import rate limit hooks execution
16
+ import { requireRateLimit, createRateLimitContext, } from "../component/rateLimitHooks.js";
17
+ // Import RBAC utilities from component
18
+ import { hasPermission, hasContentTypePermission, getPermittedContentTypes, DEFAULT_ROLES, } from "../component/roles.js";
19
+ // Import locale fallback chain utilities
20
+ import { resolveFallbackChain, getFallbackChain, } from "../component/localeFallbackChain.js";
21
+ import { resolveLocaleContent, resolveLocaleContentBatch, } from "../component/localeFields.js";
22
+ // =============================================================================
23
+ // Content Types API Wrapper
24
+ // =============================================================================
25
+ /** Content type CRUD operations */
26
+ export class ContentTypesApi {
27
+ api;
28
+ config;
29
+ authHelper;
30
+ rateLimitHelper;
31
+ constructor(api, config, authHelper, rateLimitHelper) {
32
+ this.api = api;
33
+ this.config = config;
34
+ this.authHelper = authHelper;
35
+ this.rateLimitHelper = rateLimitHelper;
36
+ }
37
+ /** @throws AuthorizationNotConfiguredError if not configured and not in permissiveMode */
38
+ async authorize(ctx, operation, userId, resourceId) {
39
+ // Check if authorization is configured
40
+ if (!this.authHelper) {
41
+ if (this.config.permissiveMode) {
42
+ console.warn(`[ConvexCMS] Authorization not configured for "${operation}". ` +
43
+ "Operations are allowed in permissiveMode, but this should NOT be used in production. " +
44
+ "Configure getUserRole hook to enable proper authorization.");
45
+ return;
46
+ }
47
+ throw new AuthorizationNotConfiguredError(operation);
48
+ }
49
+ // Skip RBAC checks if explicitly disabled
50
+ if (this.authHelper.skipRbac) {
51
+ return;
52
+ }
53
+ // Check if userId is provided
54
+ if (!userId) {
55
+ if (this.config.permissiveMode) {
56
+ console.warn(`[ConvexCMS] Anonymous operation attempted for "${operation}". ` +
57
+ "Operations without userId are allowed in permissiveMode, but this should NOT be used in production.");
58
+ return;
59
+ }
60
+ throw new AuthorizationNotConfiguredError(`${operation} (no userId provided - anonymous operations require permissiveMode)`);
61
+ }
62
+ const role = await this.authHelper.getUserRole(ctx, userId);
63
+ await this.authHelper.requireAuthorization(ctx, {
64
+ operation,
65
+ userId,
66
+ role,
67
+ resourceId,
68
+ });
69
+ }
70
+ async rateLimit(ctx, operation, userId) {
71
+ if (!this.rateLimitHelper)
72
+ return;
73
+ const role = userId ? await this.rateLimitHelper.getUserRole(ctx, userId) : null;
74
+ await this.rateLimitHelper.requireRateLimit(operation, { userId, role });
75
+ }
76
+ async create(ctx, args) {
77
+ await this.authorize(ctx, "contentTypes.create", args.createdBy);
78
+ await this.rateLimit(ctx, "contentTypes.create", args.createdBy);
79
+ return ctx.runMutation(this.api.contentTypeMutations.createContentType, args);
80
+ }
81
+ /** Detects breaking changes; fails unless force:true is specified */
82
+ async update(ctx, args) {
83
+ await this.authorize(ctx, "contentTypes.update", args.updatedBy, args.id);
84
+ await this.rateLimit(ctx, "contentTypes.update", args.updatedBy);
85
+ return ctx.runMutation(this.api.contentTypeMutations.updateContentType, args);
86
+ }
87
+ /** Soft delete by default; use hardDelete:true for permanent, cascade:true to delete entries */
88
+ async delete(ctx, args) {
89
+ await this.authorize(ctx, "contentTypes.delete", args.deletedBy, args.id);
90
+ await this.rateLimit(ctx, "contentTypes.delete", args.deletedBy);
91
+ return ctx.runMutation(this.api.contentTypeMutations.deleteContentType, args);
92
+ }
93
+ /**
94
+ * Get a content type by ID or name.
95
+ *
96
+ * @param ctx - Convex query context
97
+ * @param args - Get arguments (id or name)
98
+ * @returns The content type or null if not found
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * // Get by ID (fastest - direct document lookup)
103
+ * const type = await cms.contentTypes.get(ctx, { id: typeId });
104
+ *
105
+ * // Get by name (uses index)
106
+ * const type = await cms.contentTypes.get(ctx, { name: "blog_post" });
107
+ *
108
+ * // Include soft-deleted types
109
+ * const type = await cms.contentTypes.get(ctx, {
110
+ * name: "archived_type",
111
+ * includeDeleted: true,
112
+ * });
113
+ * ```
114
+ */
115
+ async get(ctx, args) {
116
+ return ctx.runQuery(this.api.contentTypes.get, args);
117
+ }
118
+ /**
119
+ * Get a content type by name.
120
+ *
121
+ * Convenience method that wraps `get()` for name-based lookup.
122
+ *
123
+ * @param ctx - Convex query context
124
+ * @param name - The machine-readable name of the content type
125
+ * @param includeDeleted - Whether to include soft-deleted types
126
+ * @returns The content type or null if not found
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * const blogType = await cms.contentTypes.getByName(ctx, "blog_post");
131
+ * if (blogType) {
132
+ * console.log("Fields:", blogType.fields);
133
+ * }
134
+ * ```
135
+ */
136
+ async getByName(ctx, name, includeDeleted = false) {
137
+ return this.get(ctx, { name, includeDeleted });
138
+ }
139
+ async getById(ctx, id, includeDeleted = false) {
140
+ return this.get(ctx, { id, includeDeleted });
141
+ }
142
+ async exists(ctx, name, includeDeleted = false) {
143
+ const type = await this.getByName(ctx, name, includeDeleted);
144
+ return type !== null;
145
+ }
146
+ async list(ctx, args = {}) {
147
+ return ctx.runQuery(this.api.contentTypes.list, args);
148
+ }
149
+ async listActive(ctx, paginationOpts) {
150
+ return this.list(ctx, { isActive: true, includeDeleted: false, paginationOpts });
151
+ }
152
+ async getAll(ctx, includeInactive = false) {
153
+ const result = await this.list(ctx, { isActive: includeInactive ? undefined : true, includeDeleted: false });
154
+ return result.page;
155
+ }
156
+ /**
157
+ * @example
158
+ * ```typescript
159
+ * const count = await cms.contentTypes.count(ctx);
160
+ * console.log(`You have ${count} content types`);
161
+ * ```
162
+ */
163
+ async count(ctx, includeInactive = false) {
164
+ const all = await this.getAll(ctx, includeInactive);
165
+ return all.length;
166
+ }
167
+ /**
168
+ * Deactivate a content type without deleting it.
169
+ *
170
+ * Deactivated types remain in the database but are filtered out by default
171
+ * when listing content types. Existing content entries remain accessible.
172
+ *
173
+ * @param ctx - Convex mutation context
174
+ * @param id - The content type ID to deactivate
175
+ * @param updatedBy - User ID making the change
176
+ * @returns The updated content type
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * await cms.contentTypes.deactivate(ctx, contentTypeId, currentUserId);
181
+ * ```
182
+ */
183
+ async deactivate(ctx, id, updatedBy) {
184
+ return this.update(ctx, { id, isActive: false, updatedBy });
185
+ }
186
+ /**
187
+ * Reactivate a previously deactivated content type.
188
+ *
189
+ * @param ctx - Convex mutation context
190
+ * @param id - The content type ID to reactivate
191
+ * @param updatedBy - User ID making the change
192
+ * @returns The updated content type
193
+ *
194
+ * @example
195
+ * ```typescript
196
+ * await cms.contentTypes.reactivate(ctx, contentTypeId, currentUserId);
197
+ * ```
198
+ */
199
+ async reactivate(ctx, id, updatedBy) {
200
+ return this.update(ctx, { id, isActive: true, updatedBy });
201
+ }
202
+ }
203
+ // =============================================================================
204
+ // Content Entries API Wrapper
205
+ // =============================================================================
206
+ /** Content entry CRUD and workflow operations */
207
+ export class ContentEntriesApi {
208
+ api;
209
+ config;
210
+ authHelper;
211
+ rateLimitHelper;
212
+ constructor(api, config, authHelper, rateLimitHelper) {
213
+ this.api = api;
214
+ this.config = config;
215
+ this.authHelper = authHelper;
216
+ this.rateLimitHelper = rateLimitHelper;
217
+ }
218
+ /** @throws AuthorizationNotConfiguredError if not configured and not in permissiveMode */
219
+ async authorize(ctx, operation, userId, resourceId, resourceOwnerId, contentTypeId) {
220
+ if (!this.authHelper) {
221
+ if (this.config.permissiveMode) {
222
+ console.warn(`[ConvexCMS] Authorization not configured for "${operation}". ` +
223
+ "Operations are allowed in permissiveMode, but this should NOT be used in production.");
224
+ return;
225
+ }
226
+ throw new AuthorizationNotConfiguredError(operation);
227
+ }
228
+ if (this.authHelper.skipRbac)
229
+ return;
230
+ if (!userId) {
231
+ if (this.config.permissiveMode) {
232
+ console.warn(`[ConvexCMS] Anonymous operation attempted for "${operation}". ` +
233
+ "Operations without userId are allowed in permissiveMode, but this should NOT be used in production.");
234
+ return;
235
+ }
236
+ throw new AuthorizationNotConfiguredError(`${operation} (no userId provided - anonymous operations require permissiveMode)`);
237
+ }
238
+ const role = await this.authHelper.getUserRole(ctx, userId);
239
+ await this.authHelper.requireAuthorization(ctx, { operation, userId, role, resourceId, resourceOwnerId, contentTypeId });
240
+ }
241
+ async rateLimit(ctx, operation, userId, contentTypeId) {
242
+ if (!this.rateLimitHelper)
243
+ return;
244
+ const role = userId ? await this.rateLimitHelper.getUserRole(ctx, userId) : null;
245
+ await this.rateLimitHelper.requireRateLimit(operation, { userId, role, contentTypeId });
246
+ }
247
+ async create(ctx, args) {
248
+ await this.authorize(ctx, "contentEntries.create", args.createdBy, undefined, undefined, args.contentTypeId);
249
+ await this.rateLimit(ctx, "contentEntries.create", args.createdBy, args.contentTypeId);
250
+ const argsWithDefaults = { ...args, locale: args.locale ?? this.config.defaultLocale };
251
+ return ctx.runMutation(this.api.contentEntryMutations.createEntry, argsWithDefaults);
252
+ }
253
+ async update(ctx, args) {
254
+ const entry = await ctx.runQuery(this.api.contentEntries.get, { id: args.id });
255
+ if (!entry)
256
+ throw new Error(`Content entry not found: ${args.id}`);
257
+ await this.authorize(ctx, "contentEntries.update", args.updatedBy, args.id, entry.createdBy, entry.contentTypeId);
258
+ await this.rateLimit(ctx, "contentEntries.update", args.updatedBy, entry.contentTypeId);
259
+ return ctx.runMutation(this.api.contentEntryMutations.updateEntry, args);
260
+ }
261
+ async delete(ctx, args) {
262
+ const entry = await ctx.runQuery(this.api.contentEntries.get, { id: args.id });
263
+ if (!entry)
264
+ throw new Error(`Content entry not found: ${args.id}`);
265
+ await this.authorize(ctx, "contentEntries.delete", args.deletedBy, args.id, entry.createdBy, entry.contentTypeId);
266
+ await this.rateLimit(ctx, "contentEntries.delete", args.deletedBy, entry.contentTypeId);
267
+ return ctx.runMutation(this.api.contentEntryMutations.deleteEntry, args);
268
+ }
269
+ async get(ctx, args) {
270
+ return ctx.runQuery(this.api.contentEntries.get, args);
271
+ }
272
+ /** Looks up by contentTypeId+slug or contentTypeName+slug */
273
+ async getBySlug(ctx, args) {
274
+ // The wrapper's unified interface adapts to the component's split API
275
+ if (args.contentTypeId) {
276
+ return ctx.runQuery(this.api.contentEntries.getBySlug, {
277
+ contentTypeId: args.contentTypeId,
278
+ slug: args.slug,
279
+ includeDeleted: false,
280
+ });
281
+ }
282
+ if (args.contentTypeName) {
283
+ return ctx.runQuery(this.api.contentEntries.getBySlugAndTypeName, {
284
+ contentTypeName: args.contentTypeName,
285
+ slug: args.slug,
286
+ includeDeleted: false,
287
+ });
288
+ }
289
+ throw new Error("getBySlug requires either contentTypeId or contentTypeName");
290
+ }
291
+ /** Standard Convex pagination format compatible with usePaginatedQuery */
292
+ async list(ctx, args) {
293
+ return ctx.runQuery(this.api.contentEntries.list, args);
294
+ }
295
+ /**
296
+ * Publish a content entry.
297
+ *
298
+ * @param ctx - Convex mutation context
299
+ * @param args - Publish arguments
300
+ * @returns The published entry
301
+ */
302
+ async publish(ctx, args) {
303
+ // Fetch entry for ownership-based authorization
304
+ const entry = await ctx.runQuery(this.api.contentEntries.get, { id: args.id });
305
+ if (!entry) {
306
+ throw new Error(`Content entry not found: ${args.id}`);
307
+ }
308
+ // Authorization check - contentEntries.publish (with ownership info)
309
+ await this.authorize(ctx, "contentEntries.publish", args.updatedBy, args.id, entry.createdBy, entry.contentTypeId);
310
+ // Rate limit check - contentEntries.publish
311
+ await this.rateLimit(ctx, "contentEntries.publish", args.updatedBy, entry.contentTypeId);
312
+ return ctx.runMutation(this.api.contentEntryMutations.publishEntry, args);
313
+ }
314
+ /**
315
+ * Unpublish a content entry (revert to draft).
316
+ *
317
+ * @param ctx - Convex mutation context
318
+ * @param args - Unpublish arguments
319
+ * @returns The unpublished entry
320
+ */
321
+ async unpublish(ctx, args) {
322
+ // Fetch entry for ownership-based authorization
323
+ const entry = await ctx.runQuery(this.api.contentEntries.get, { id: args.id });
324
+ if (!entry) {
325
+ throw new Error(`Content entry not found: ${args.id}`);
326
+ }
327
+ // Authorization check - contentEntries.unpublish (with ownership info)
328
+ await this.authorize(ctx, "contentEntries.unpublish", args.updatedBy, args.id, entry.createdBy, entry.contentTypeId);
329
+ // Rate limit check - contentEntries.unpublish
330
+ await this.rateLimit(ctx, "contentEntries.unpublish", args.updatedBy, entry.contentTypeId);
331
+ return ctx.runMutation(this.api.contentEntryMutations.unpublishEntry, args);
332
+ }
333
+ /**
334
+ * Schedule a content entry for future publication.
335
+ *
336
+ * @param ctx - Convex mutation context
337
+ * @param args - Schedule arguments
338
+ * @returns The scheduled entry
339
+ *
340
+ * @example
341
+ * ```typescript
342
+ * await cms.contentEntries.schedule(ctx, {
343
+ * id: entryId,
344
+ * publishAt: Date.now() + 24 * 60 * 60 * 1000, // Tomorrow
345
+ * });
346
+ * ```
347
+ */
348
+ async schedule(ctx, args) {
349
+ if (!this.config.features.scheduling) {
350
+ throw new Error("Scheduling feature is not enabled");
351
+ }
352
+ // Fetch entry for ownership-based authorization
353
+ const entry = await ctx.runQuery(this.api.contentEntries.get, { id: args.id });
354
+ if (!entry) {
355
+ throw new Error(`Content entry not found: ${args.id}`);
356
+ }
357
+ // Authorization check - contentEntries.schedule (with ownership info)
358
+ await this.authorize(ctx, "contentEntries.schedule", args.updatedBy, args.id, entry.createdBy, entry.contentTypeId);
359
+ // Rate limit check - contentEntries.schedule
360
+ await this.rateLimit(ctx, "contentEntries.schedule", args.updatedBy, entry.contentTypeId);
361
+ return ctx.runMutation(this.api.scheduledPublish.scheduleEntry, args);
362
+ }
363
+ /**
364
+ * Restore a soft-deleted content entry.
365
+ *
366
+ * Removes the deletedAt timestamp from a soft-deleted entry,
367
+ * making it active again. Only works for soft-deleted entries;
368
+ * hard-deleted entries cannot be recovered.
369
+ *
370
+ * @param ctx - Convex mutation context
371
+ * @param args - Restore arguments
372
+ * @returns The restored entry
373
+ *
374
+ * @example
375
+ * ```typescript
376
+ * // Restore a soft-deleted entry
377
+ * const restored = await cms.contentEntries.restore(ctx, {
378
+ * id: entryId,
379
+ * restoredBy: currentUserId,
380
+ * });
381
+ * console.log(restored.deletedAt); // undefined
382
+ * ```
383
+ */
384
+ async restore(ctx, args) {
385
+ if (!this.config.features.softDelete) {
386
+ throw new Error("Soft delete feature is not enabled");
387
+ }
388
+ // Fetch entry for ownership-based authorization
389
+ const entry = await ctx.runQuery(this.api.contentEntries.get, { id: args.id });
390
+ if (!entry) {
391
+ throw new Error(`Content entry not found: ${args.id}`);
392
+ }
393
+ // Authorization check - contentEntries.restore (with ownership info)
394
+ await this.authorize(ctx, "contentEntries.restore", args.restoredBy, args.id, entry.createdBy, entry.contentTypeId);
395
+ // Rate limit check - contentEntries.restore
396
+ await this.rateLimit(ctx, "contentEntries.restore", args.restoredBy, entry.contentTypeId);
397
+ return ctx.runMutation(this.api.contentEntryMutations.restoreEntry, args);
398
+ }
399
+ /**
400
+ * Create a fluent query builder for constructing complex content queries.
401
+ *
402
+ * The query builder provides a chainable API for building queries with:
403
+ * - Content type filtering
404
+ * - Status filtering (single or multiple)
405
+ * - Field-level filters with various operators
406
+ * - Full-text search
407
+ * - Locale filtering
408
+ * - Cursor-based pagination
409
+ * - Sort direction
410
+ *
411
+ * @returns A new ContentQueryBuilder instance
412
+ *
413
+ * @example
414
+ * ```typescript
415
+ * // Simple query
416
+ * const posts = await cms.contentEntries
417
+ * .query()
418
+ * .contentType("blog_post")
419
+ * .status("published")
420
+ * .limit(10)
421
+ * .execute(ctx);
422
+ *
423
+ * // Complex query with field filters
424
+ * const featured = await cms.contentEntries
425
+ * .query()
426
+ * .contentType("blog_post")
427
+ * .where("category", "eq", "technology")
428
+ * .whereContains("tags", "featured")
429
+ * .whereGreaterThan("views", 100)
430
+ * .newestFirst()
431
+ * .limit(5)
432
+ * .execute(ctx);
433
+ *
434
+ * // Pagination
435
+ * const page1 = await cms.contentEntries
436
+ * .query()
437
+ * .contentType("blog_post")
438
+ * .limit(20)
439
+ * .execute(ctx);
440
+ *
441
+ * const page2 = await cms.contentEntries
442
+ * .query()
443
+ * .contentType("blog_post")
444
+ * .limit(20)
445
+ * .cursor(page1.continueCursor)
446
+ * .execute(ctx);
447
+ *
448
+ * // Get first result only
449
+ * const latest = await cms.contentEntries
450
+ * .query()
451
+ * .contentType("blog_post")
452
+ * .published()
453
+ * .newestFirst()
454
+ * .first(ctx);
455
+ *
456
+ * // Check if results exist
457
+ * const hasPublished = await cms.contentEntries
458
+ * .query()
459
+ * .contentType("blog_post")
460
+ * .published()
461
+ * .exists(ctx);
462
+ * ```
463
+ */
464
+ query() {
465
+ return createQueryBuilder(this.api);
466
+ }
467
+ /**
468
+ * Resolve locale content for a single content entry.
469
+ *
470
+ * Takes a content entry with potentially localized field values and resolves
471
+ * all localized fields to single values based on the requested locale and
472
+ * fallback chain. This merges localized and default field values.
473
+ *
474
+ * Resolution order for each localized field:
475
+ * 1. Try the requested locale
476
+ * 2. Try each locale in the fallback chain (in order)
477
+ * 3. Try the default locale
478
+ * 4. Return first available locale as last resort
479
+ *
480
+ * @param entry - The content entry to resolve (with raw localized data)
481
+ * @param options - Locale resolution options
482
+ * @returns The entry with resolved data and metadata about resolution
483
+ *
484
+ * @example
485
+ * ```typescript
486
+ * // Get an entry
487
+ * const entry = await cms.contentEntries.get(ctx, { id: entryId });
488
+ *
489
+ * // Resolve to Spanish with English fallback
490
+ * const resolved = cms.contentEntries.resolveLocale(entry, {
491
+ * locale: "es-ES",
492
+ * fallbackChain: ["en-US"],
493
+ * defaultLocale: "en-US",
494
+ * fields: contentType.fields,
495
+ * });
496
+ *
497
+ * // Access resolved data
498
+ * console.log(resolved.data.title); // "Hola" (Spanish) or "Hello" (English fallback)
499
+ *
500
+ * // Check which fields used fallback
501
+ * if (resolved.localeResolution.fieldsFromFallback.includes("title")) {
502
+ * console.log("Title was not translated to Spanish");
503
+ * }
504
+ *
505
+ * // See which locale each field was resolved from
506
+ * console.log(resolved.localeResolution.fieldResolutions);
507
+ * // { content: "en-US" } - content was resolved from English
508
+ * ```
509
+ */
510
+ resolveLocale(entry, options) {
511
+ return resolveLocaleContent(entry, options);
512
+ }
513
+ /**
514
+ * Resolve locale content for multiple content entries.
515
+ *
516
+ * Convenience method for batch-resolving a list of entries.
517
+ * Useful after fetching a paginated list of content entries.
518
+ *
519
+ * @param entries - Array of content entries to resolve
520
+ * @param options - Locale resolution options (applied to all entries)
521
+ * @returns Array of entries with resolved locale data
522
+ *
523
+ * @example
524
+ * ```typescript
525
+ * // Fetch published blog posts
526
+ * const { page } = await cms.contentEntries.list(ctx, {
527
+ * contentTypeName: "blog_post",
528
+ * status: "published",
529
+ * paginationOpts: { numItems: 10 },
530
+ * });
531
+ *
532
+ * // Resolve all entries to Spanish
533
+ * const resolvedPosts = cms.contentEntries.resolveLocaleBatch(page, {
534
+ * locale: "es-ES",
535
+ * fallbackChain: cms.getLocaleFallbackChain("es-ES"),
536
+ * defaultLocale: cms.config.defaultLocale,
537
+ * fields: blogPostType.fields,
538
+ * });
539
+ *
540
+ * // Use resolved data
541
+ * for (const post of resolvedPosts) {
542
+ * console.log(post.data.title); // Resolved title in Spanish or fallback
543
+ * }
544
+ * ```
545
+ */
546
+ resolveLocaleBatch(entries, options) {
547
+ return resolveLocaleContentBatch(entries, options);
548
+ }
549
+ /**
550
+ * List content entries with automatic locale resolution.
551
+ *
552
+ * This is a convenience method that combines `list()` and `resolveLocaleBatch()`
553
+ * into a single call. It fetches content entries and automatically resolves
554
+ * all localized fields to the requested locale with fallback support.
555
+ *
556
+ * Note: This method requires the content type's field definitions to properly
557
+ * resolve localized fields. You can either pass them explicitly or let the
558
+ * method fetch them automatically (requires an extra query).
559
+ *
560
+ * @param ctx - Convex query context
561
+ * @param args - Query options with pagination
562
+ * @param localeOptions - Locale resolution options
563
+ * @returns Paginated result with locale-resolved entries
564
+ *
565
+ * @example
566
+ * ```typescript
567
+ * // List with locale resolution
568
+ * const { page, continueCursor, isDone } = await cms.contentEntries.listWithLocale(
569
+ * ctx,
570
+ * {
571
+ * contentTypeName: "blog_post",
572
+ * status: "published",
573
+ * paginationOpts: { numItems: 10 },
574
+ * },
575
+ * {
576
+ * locale: "es-ES",
577
+ * fields: blogPostType.fields, // Required for resolution
578
+ * }
579
+ * );
580
+ *
581
+ * // All entries have resolved locale data
582
+ * for (const post of page) {
583
+ * console.log(post.data.title); // Resolved title
584
+ * console.log(post.localeResolution.fieldsFromFallback); // Which fields used fallback
585
+ * }
586
+ * ```
587
+ */
588
+ async listWithLocale(ctx, args, localeOptions) {
589
+ // Fetch raw entries
590
+ const result = await this.list(ctx, args);
591
+ // Resolve locale for all entries
592
+ const resolvedPage = this.resolveLocaleBatch(result.page, localeOptions);
593
+ return {
594
+ page: resolvedPage,
595
+ continueCursor: result.continueCursor,
596
+ isDone: result.isDone,
597
+ };
598
+ }
599
+ /**
600
+ * Get a content entry by ID with automatic locale resolution.
601
+ *
602
+ * Fetches the entry and resolves all localized fields to the requested locale.
603
+ *
604
+ * @param ctx - Convex query context
605
+ * @param args - Get arguments
606
+ * @param localeOptions - Locale resolution options
607
+ * @returns The entry with resolved locale data, or null if not found
608
+ *
609
+ * @example
610
+ * ```typescript
611
+ * const post = await cms.contentEntries.getWithLocale(
612
+ * ctx,
613
+ * { id: entryId },
614
+ * {
615
+ * locale: "es-ES",
616
+ * fallbackChain: ["en-US"],
617
+ * defaultLocale: "en-US",
618
+ * fields: blogPostType.fields,
619
+ * }
620
+ * );
621
+ *
622
+ * if (post) {
623
+ * console.log(post.data.title); // Resolved title
624
+ * }
625
+ * ```
626
+ */
627
+ async getWithLocale(ctx, args, localeOptions) {
628
+ const entry = await this.get(ctx, args);
629
+ if (!entry)
630
+ return null;
631
+ return this.resolveLocale(entry, localeOptions);
632
+ }
633
+ /**
634
+ * Get a content entry by slug with automatic locale resolution.
635
+ *
636
+ * Fetches the entry by slug and resolves all localized fields to the requested locale.
637
+ *
638
+ * @param ctx - Convex query context
639
+ * @param args - Get by slug arguments
640
+ * @param localeOptions - Locale resolution options
641
+ * @returns The entry with resolved locale data, or null if not found
642
+ *
643
+ * @example
644
+ * ```typescript
645
+ * const post = await cms.contentEntries.getBySlugWithLocale(
646
+ * ctx,
647
+ * {
648
+ * contentTypeName: "blog_post",
649
+ * slug: "hello-world",
650
+ * },
651
+ * {
652
+ * locale: "es-ES",
653
+ * fields: blogPostType.fields,
654
+ * }
655
+ * );
656
+ * ```
657
+ */
658
+ async getBySlugWithLocale(ctx, args, localeOptions) {
659
+ const entry = await this.getBySlug(ctx, args);
660
+ if (!entry)
661
+ return null;
662
+ return this.resolveLocale(entry, localeOptions);
663
+ }
664
+ // ===========================================================================
665
+ // Duplicate Entry
666
+ // ===========================================================================
667
+ /**
668
+ * Duplicate a content entry.
669
+ *
670
+ * Creates a copy of an existing content entry with a new unique slug.
671
+ * The duplicate always starts as a draft, regardless of the source entry's status.
672
+ * Media references are copied by default but can be cleared.
673
+ *
674
+ * @param ctx - Convex mutation context
675
+ * @param args - Duplicate arguments
676
+ * @returns The duplicated entry
677
+ *
678
+ * @example
679
+ * ```typescript
680
+ * // Simple duplication with auto-generated slug
681
+ * const copy = await cms.contentEntries.duplicate(ctx, {
682
+ * sourceEntryId: originalPost._id,
683
+ * createdBy: currentUserId,
684
+ * });
685
+ *
686
+ * // Duplicate with custom slug
687
+ * const copy = await cms.contentEntries.duplicate(ctx, {
688
+ * sourceEntryId: templateId,
689
+ * slug: "new-post-from-template",
690
+ * createdBy: currentUserId,
691
+ * });
692
+ *
693
+ * // Duplicate without media references (for a fresh start)
694
+ * const copy = await cms.contentEntries.duplicate(ctx, {
695
+ * sourceEntryId: originalPost._id,
696
+ * copyMediaReferences: false,
697
+ * createdBy: currentUserId,
698
+ * });
699
+ * ```
700
+ */
701
+ async duplicate(ctx, args) {
702
+ // Authorization check - duplicating is similar to create
703
+ await this.authorize(ctx, "contentEntries.create", args.createdBy);
704
+ // Rate limit check
705
+ await this.rateLimit(ctx, "contentEntries.create", args.createdBy);
706
+ return ctx.runMutation(this.api.contentEntryMutations.duplicateEntry, args);
707
+ }
708
+ // ===========================================================================
709
+ // Bulk Operations
710
+ // ===========================================================================
711
+ /**
712
+ * Publish multiple content entries in a single transaction.
713
+ *
714
+ * This is more efficient than publishing entries one by one. Each entry that
715
+ * is already published will be skipped (idempotent behavior). Deleted or
716
+ * archived entries will fail with an error message.
717
+ *
718
+ * @param ctx - Convex mutation context
719
+ * @param args - Bulk publish arguments
720
+ * @returns Bulk operation result with success/failure details for each entry
721
+ *
722
+ * @example
723
+ * ```typescript
724
+ * const result = await cms.contentEntries.bulkPublish(ctx, {
725
+ * ids: [entry1._id, entry2._id, entry3._id],
726
+ * changeDescription: "Publishing launch content",
727
+ * updatedBy: currentUserId,
728
+ * });
729
+ * console.log(`Published ${result.succeeded} of ${result.total} entries`);
730
+ * if (result.failed > 0) {
731
+ * result.results.filter(r => !r.success).forEach(r => {
732
+ * console.error(`Failed to publish ${r.id}: ${r.error}`);
733
+ * });
734
+ * }
735
+ * ```
736
+ */
737
+ async bulkPublish(ctx, args) {
738
+ // Authorization check for each entry (bulk check)
739
+ await this.authorize(ctx, "contentEntries.publish", args.updatedBy);
740
+ // Rate limit check
741
+ await this.rateLimit(ctx, "contentEntries.publish", args.updatedBy);
742
+ return ctx.runMutation(this.api.bulkOperations.bulkPublish, args);
743
+ }
744
+ /**
745
+ * Unpublish multiple content entries in a single transaction.
746
+ *
747
+ * Reverts published entries to draft status. Non-published entries are
748
+ * skipped (idempotent behavior).
749
+ *
750
+ * @param ctx - Convex mutation context
751
+ * @param args - Bulk unpublish arguments
752
+ * @returns Bulk operation result with success/failure details for each entry
753
+ *
754
+ * @example
755
+ * ```typescript
756
+ * const result = await cms.contentEntries.bulkUnpublish(ctx, {
757
+ * ids: [entry1._id, entry2._id],
758
+ * updatedBy: currentUserId,
759
+ * });
760
+ * ```
761
+ */
762
+ async bulkUnpublish(ctx, args) {
763
+ // Authorization check
764
+ await this.authorize(ctx, "contentEntries.unpublish", args.updatedBy);
765
+ // Rate limit check
766
+ await this.rateLimit(ctx, "contentEntries.unpublish", args.updatedBy);
767
+ return ctx.runMutation(this.api.bulkOperations.bulkUnpublish, args);
768
+ }
769
+ /**
770
+ * Delete multiple content entries in a single transaction.
771
+ *
772
+ * By default, performs soft delete (entries can be restored later).
773
+ * When hardDelete is true, permanently removes entries and all their versions.
774
+ *
775
+ * @param ctx - Convex mutation context
776
+ * @param args - Bulk delete arguments
777
+ * @returns Bulk operation result with success/failure details for each entry
778
+ *
779
+ * @example
780
+ * ```typescript
781
+ * // Soft delete (default)
782
+ * const result = await cms.contentEntries.bulkDelete(ctx, {
783
+ * ids: [entry1._id, entry2._id],
784
+ * deletedBy: currentUserId,
785
+ * });
786
+ *
787
+ * // Hard delete (permanent)
788
+ * const result = await cms.contentEntries.bulkDelete(ctx, {
789
+ * ids: [entry1._id, entry2._id],
790
+ * deletedBy: currentUserId,
791
+ * hardDelete: true,
792
+ * });
793
+ * ```
794
+ */
795
+ async bulkDelete(ctx, args) {
796
+ // Authorization check
797
+ await this.authorize(ctx, "contentEntries.delete", args.deletedBy);
798
+ // Rate limit check
799
+ await this.rateLimit(ctx, "contentEntries.delete", args.deletedBy);
800
+ return ctx.runMutation(this.api.bulkOperations.bulkDelete, args);
801
+ }
802
+ /**
803
+ * Update multiple content entries with the same changes in a single transaction.
804
+ *
805
+ * Applies the same data updates and/or status change to all specified entries.
806
+ * Data is merged with existing data for each entry (partial updates).
807
+ * Each entry is validated against its content type schema.
808
+ *
809
+ * @param ctx - Convex mutation context
810
+ * @param args - Bulk update arguments
811
+ * @returns Bulk operation result with success/failure details for each entry
812
+ *
813
+ * @example
814
+ * ```typescript
815
+ * // Update data for multiple entries
816
+ * const result = await cms.contentEntries.bulkUpdate(ctx, {
817
+ * ids: [entry1._id, entry2._id, entry3._id],
818
+ * data: { featured: true, category: "news" },
819
+ * updatedBy: currentUserId,
820
+ * });
821
+ *
822
+ * // Change status for multiple entries
823
+ * const result = await cms.contentEntries.bulkUpdate(ctx, {
824
+ * ids: [entry1._id, entry2._id],
825
+ * status: "archived",
826
+ * updatedBy: currentUserId,
827
+ * });
828
+ * ```
829
+ */
830
+ async bulkUpdate(ctx, args) {
831
+ // Authorization check
832
+ await this.authorize(ctx, "contentEntries.update", args.updatedBy);
833
+ // Rate limit check
834
+ await this.rateLimit(ctx, "contentEntries.update", args.updatedBy);
835
+ return ctx.runMutation(this.api.bulkOperations.bulkUpdate, args);
836
+ }
837
+ /**
838
+ * Restore multiple soft-deleted content entries in a single transaction.
839
+ *
840
+ * Removes the deletedAt marker from entries, making them active again.
841
+ * Only works for soft-deleted entries. Non-deleted entries are skipped
842
+ * (idempotent behavior).
843
+ *
844
+ * @param ctx - Convex mutation context
845
+ * @param args - Bulk restore arguments
846
+ * @returns Bulk operation result with success/failure details for each entry
847
+ *
848
+ * @example
849
+ * ```typescript
850
+ * const result = await cms.contentEntries.bulkRestore(ctx, {
851
+ * ids: [deletedEntry1._id, deletedEntry2._id],
852
+ * restoredBy: currentUserId,
853
+ * });
854
+ * ```
855
+ */
856
+ async bulkRestore(ctx, args) {
857
+ if (!this.config.features.softDelete) {
858
+ throw new Error("Soft delete feature is not enabled");
859
+ }
860
+ // Authorization check
861
+ await this.authorize(ctx, "contentEntries.restore", args.restoredBy);
862
+ // Rate limit check
863
+ await this.rateLimit(ctx, "contentEntries.restore", args.restoredBy);
864
+ return ctx.runMutation(this.api.bulkOperations.bulkRestore, args);
865
+ }
866
+ }
867
+ // =============================================================================
868
+ // Versions API Wrapper
869
+ // =============================================================================
870
+ /**
871
+ * Wrapper for content version operations.
872
+ *
873
+ * Provides comprehensive version management including:
874
+ * - Version history retrieval with pagination
875
+ * - Getting specific versions by ID or number
876
+ * - Version comparison and diff generation
877
+ * - Rollback functionality
878
+ * - Finding latest and published versions
879
+ *
880
+ * @example
881
+ * ```typescript
882
+ * // Get version history for an entry
883
+ * const history = await cms.versions.getHistory(ctx, {
884
+ * entryId: entry._id,
885
+ * paginationOpts: { numItems: 10 },
886
+ * });
887
+ *
888
+ * // Compare two versions
889
+ * const diff = await cms.versions.compare(ctx, {
890
+ * entryId: entry._id,
891
+ * fromVersionNumber: 1,
892
+ * toVersionNumber: 5,
893
+ * });
894
+ *
895
+ * // Rollback to a previous version
896
+ * await cms.versions.rollback(ctx, {
897
+ * entryId: entry._id,
898
+ * versionNumber: 3,
899
+ * });
900
+ * ```
901
+ */
902
+ export class VersionsApi {
903
+ api;
904
+ config;
905
+ authHelper;
906
+ rateLimitHelper;
907
+ constructor(api, config, authHelper, rateLimitHelper) {
908
+ this.api = api;
909
+ this.config = config;
910
+ this.authHelper = authHelper;
911
+ this.rateLimitHelper = rateLimitHelper;
912
+ }
913
+ /**
914
+ * Check if versioning feature is enabled.
915
+ * @throws Error if versioning is not enabled
916
+ */
917
+ ensureVersioningEnabled() {
918
+ if (!this.config.features.versioning) {
919
+ throw new Error("Versioning feature is not enabled");
920
+ }
921
+ }
922
+ /**
923
+ * Perform authorization check for version operations.
924
+ * @param ctx - The Convex context (passed to authorization hooks for database access)
925
+ * @param operation - The CMS operation being performed
926
+ * @param userId - The user performing the operation
927
+ * @param resourceId - Optional resource ID (entry ID for version operations)
928
+ * @throws AuthorizationNotConfiguredError if authorization is not configured and permissiveMode is false
929
+ */
930
+ async authorize(ctx, operation, userId, resourceId) {
931
+ if (!this.authHelper) {
932
+ if (this.config.permissiveMode) {
933
+ console.warn(`[ConvexCMS] Authorization not configured for "${operation}". ` +
934
+ "Operations are allowed in permissiveMode, but this should NOT be used in production.");
935
+ return;
936
+ }
937
+ throw new AuthorizationNotConfiguredError(operation);
938
+ }
939
+ if (this.authHelper.skipRbac) {
940
+ return;
941
+ }
942
+ if (!userId) {
943
+ if (this.config.permissiveMode) {
944
+ console.warn(`[ConvexCMS] Anonymous operation attempted for "${operation}".`);
945
+ return;
946
+ }
947
+ throw new AuthorizationNotConfiguredError(`${operation} (no userId provided - anonymous operations require permissiveMode)`);
948
+ }
949
+ const role = await this.authHelper.getUserRole(ctx, userId);
950
+ await this.authHelper.requireAuthorization(ctx, {
951
+ operation,
952
+ userId,
953
+ role,
954
+ resourceId,
955
+ });
956
+ }
957
+ /**
958
+ * Enforce rate limit for version operations.
959
+ * @param ctx - The Convex context (for database access)
960
+ * @param operation - The CMS operation being performed
961
+ * @param userId - The user performing the operation
962
+ */
963
+ async rateLimit(ctx, operation, userId) {
964
+ // Skip if no rate limit helper configured
965
+ if (!this.rateLimitHelper) {
966
+ return;
967
+ }
968
+ const role = userId ? await this.rateLimitHelper.getUserRole(ctx, userId) : null;
969
+ await this.rateLimitHelper.requireRateLimit(operation, {
970
+ userId,
971
+ role,
972
+ });
973
+ }
974
+ /**
975
+ * Get version history with standard Convex pagination.
976
+ *
977
+ * Returns versions in reverse chronological order (newest first).
978
+ * Compatible with `usePaginatedQuery` React hook.
979
+ *
980
+ * @param ctx - Convex query context
981
+ * @param args - History query arguments with pagination
982
+ * @returns Paginated version history or null if entry not found
983
+ *
984
+ * @example
985
+ * ```typescript
986
+ * // Get first page of version history
987
+ * const { page, continueCursor, isDone } = await cms.versions.getHistory(ctx, {
988
+ * entryId: entry._id,
989
+ * paginationOpts: { numItems: 10 },
990
+ * });
991
+ *
992
+ * // Get next page
993
+ * if (!isDone && continueCursor) {
994
+ * const nextPage = await cms.versions.getHistory(ctx, {
995
+ * entryId: entry._id,
996
+ * paginationOpts: { numItems: 10, cursor: continueCursor },
997
+ * });
998
+ * }
999
+ * ```
1000
+ */
1001
+ async getHistory(ctx, args) {
1002
+ this.ensureVersioningEnabled();
1003
+ return ctx.runQuery(this.api.contentEntries.getVersionHistory, args);
1004
+ }
1005
+ /**
1006
+ * Get a specific version by ID or version number.
1007
+ *
1008
+ * @param ctx - Convex query context
1009
+ * @param args - Get arguments (entryId required, plus versionId or versionNumber)
1010
+ * @returns The version or null if not found
1011
+ *
1012
+ * @example
1013
+ * ```typescript
1014
+ * // Get by version number
1015
+ * const version = await cms.versions.get(ctx, {
1016
+ * entryId: entry._id,
1017
+ * versionNumber: 3,
1018
+ * });
1019
+ *
1020
+ * // Get by version ID
1021
+ * const version = await cms.versions.get(ctx, {
1022
+ * entryId: entry._id,
1023
+ * versionId: "abc123",
1024
+ * });
1025
+ * ```
1026
+ */
1027
+ async get(ctx, args) {
1028
+ this.ensureVersioningEnabled();
1029
+ return ctx.runQuery(this.api.contentEntries.getVersion, args);
1030
+ }
1031
+ /**
1032
+ * Get a version by its version number (convenience method).
1033
+ *
1034
+ * @param ctx - Convex query context
1035
+ * @param entryId - The content entry ID
1036
+ * @param versionNumber - The version number to retrieve
1037
+ * @returns The version or null if not found
1038
+ *
1039
+ * @example
1040
+ * ```typescript
1041
+ * const version3 = await cms.versions.getByNumber(ctx, entry._id, 3);
1042
+ * ```
1043
+ */
1044
+ async getByNumber(ctx, entryId, versionNumber) {
1045
+ return this.get(ctx, { entryId, versionNumber });
1046
+ }
1047
+ /**
1048
+ * Get a version by its document ID (convenience method).
1049
+ *
1050
+ * @param ctx - Convex query context
1051
+ * @param entryId - The content entry ID
1052
+ * @param versionId - The version document ID
1053
+ * @returns The version or null if not found
1054
+ *
1055
+ * @example
1056
+ * ```typescript
1057
+ * const version = await cms.versions.getById(ctx, entry._id, versionDocId);
1058
+ * ```
1059
+ */
1060
+ async getById(ctx, entryId, versionId) {
1061
+ return this.get(ctx, { entryId, versionId });
1062
+ }
1063
+ /**
1064
+ * Get the latest (most recent) version snapshot for an entry.
1065
+ *
1066
+ * @param ctx - Convex query context
1067
+ * @param entryId - The content entry ID
1068
+ * @returns The latest version or null if no versions exist
1069
+ *
1070
+ * @example
1071
+ * ```typescript
1072
+ * const latest = await cms.versions.getLatest(ctx, entry._id);
1073
+ * console.log(`Current version: ${latest?.versionNumber}`);
1074
+ * ```
1075
+ */
1076
+ async getLatest(ctx, entryId) {
1077
+ this.ensureVersioningEnabled();
1078
+ const history = await this.getHistory(ctx, {
1079
+ entryId,
1080
+ paginationOpts: { numItems: 1, cursor: null },
1081
+ });
1082
+ return history?.page[0] ?? null;
1083
+ }
1084
+ /**
1085
+ * Get the latest published version for an entry.
1086
+ *
1087
+ * Searches through version history to find the most recent version
1088
+ * that was published (wasPublished = true).
1089
+ *
1090
+ * @param ctx - Convex query context
1091
+ * @param entryId - The content entry ID
1092
+ * @returns The latest published version or null if none published
1093
+ *
1094
+ * @example
1095
+ * ```typescript
1096
+ * const published = await cms.versions.getLatestPublished(ctx, entry._id);
1097
+ * if (published) {
1098
+ * console.log(`Published at: ${new Date(published.publishedAt!)}`);
1099
+ * }
1100
+ * ```
1101
+ */
1102
+ async getLatestPublished(ctx, entryId) {
1103
+ this.ensureVersioningEnabled();
1104
+ // Iterate through pages to find the first published version
1105
+ let cursor = null;
1106
+ let isDone = false;
1107
+ while (!isDone) {
1108
+ const history = await this.getHistory(ctx, {
1109
+ entryId,
1110
+ paginationOpts: { numItems: 50, cursor },
1111
+ });
1112
+ if (!history)
1113
+ return null;
1114
+ const publishedVersion = history.page.find((v) => v.wasPublished);
1115
+ if (publishedVersion) {
1116
+ return publishedVersion;
1117
+ }
1118
+ cursor = history.continueCursor;
1119
+ isDone = history.isDone;
1120
+ }
1121
+ return null;
1122
+ }
1123
+ /**
1124
+ * Get all published versions for an entry.
1125
+ *
1126
+ * @param ctx - Convex query context
1127
+ * @param entryId - The content entry ID
1128
+ * @param limit - Maximum number of published versions to return (default: 10)
1129
+ * @returns Array of published versions (newest first)
1130
+ *
1131
+ * @example
1132
+ * ```typescript
1133
+ * const publishedVersions = await cms.versions.getPublishedHistory(ctx, entry._id, 5);
1134
+ * console.log(`Found ${publishedVersions.length} published versions`);
1135
+ * ```
1136
+ */
1137
+ async getPublishedHistory(ctx, entryId, limit = 10) {
1138
+ this.ensureVersioningEnabled();
1139
+ const published = [];
1140
+ let cursor = null;
1141
+ let isDone = false;
1142
+ while (!isDone && published.length < limit) {
1143
+ const history = await this.getHistory(ctx, {
1144
+ entryId,
1145
+ paginationOpts: { numItems: 50, cursor },
1146
+ });
1147
+ if (!history)
1148
+ break;
1149
+ for (const version of history.page) {
1150
+ if (version.wasPublished) {
1151
+ published.push(version);
1152
+ if (published.length >= limit)
1153
+ break;
1154
+ }
1155
+ }
1156
+ cursor = history.continueCursor;
1157
+ isDone = history.isDone;
1158
+ }
1159
+ return published;
1160
+ }
1161
+ /**
1162
+ * Compare two versions and generate a detailed diff.
1163
+ *
1164
+ * Analyzes field-level changes between two versions, identifying:
1165
+ * - Added fields (present in toVersion but not fromVersion)
1166
+ * - Removed fields (present in fromVersion but not toVersion)
1167
+ * - Modified fields (present in both but with different values)
1168
+ *
1169
+ * @param ctx - Convex query context
1170
+ * @param args - Comparison arguments
1171
+ * @returns Detailed version comparison or null if versions not found
1172
+ *
1173
+ * @example
1174
+ * ```typescript
1175
+ * const diff = await cms.versions.compare(ctx, {
1176
+ * entryId: entry._id,
1177
+ * fromVersionNumber: 1,
1178
+ * toVersionNumber: 5,
1179
+ * });
1180
+ *
1181
+ * if (diff) {
1182
+ * console.log(`${diff.summary.totalChanges} changes detected`);
1183
+ * for (const change of diff.changes) {
1184
+ * console.log(`${change.field}: ${change.changeType}`);
1185
+ * }
1186
+ * }
1187
+ * ```
1188
+ */
1189
+ async compare(ctx, args) {
1190
+ this.ensureVersioningEnabled();
1191
+ // Get the fromVersion
1192
+ const fromVersion = await this.getByNumber(ctx, args.entryId, args.fromVersionNumber);
1193
+ if (!fromVersion)
1194
+ return null;
1195
+ // Get the toVersion
1196
+ const toVersion = await this.getByNumber(ctx, args.entryId, args.toVersionNumber);
1197
+ if (!toVersion)
1198
+ return null;
1199
+ // Generate the comparison
1200
+ return this.generateComparison(fromVersion, toVersion);
1201
+ }
1202
+ /**
1203
+ * Compare the current entry state with a specific version.
1204
+ *
1205
+ * Useful for seeing what has changed since a particular point in time.
1206
+ *
1207
+ * @param ctx - Convex query context
1208
+ * @param entryId - The content entry ID
1209
+ * @param versionNumber - The version number to compare against
1210
+ * @returns Comparison between the version and current state, or null
1211
+ *
1212
+ * @example
1213
+ * ```typescript
1214
+ * // See what changed since version 3
1215
+ * const diff = await cms.versions.compareWithCurrent(ctx, entry._id, 3);
1216
+ * ```
1217
+ */
1218
+ async compareWithCurrent(ctx, entryId, versionNumber) {
1219
+ this.ensureVersioningEnabled();
1220
+ const fromVersion = await this.getByNumber(ctx, entryId, versionNumber);
1221
+ if (!fromVersion)
1222
+ return null;
1223
+ const latest = await this.getLatest(ctx, entryId);
1224
+ if (!latest)
1225
+ return null;
1226
+ return this.generateComparison(fromVersion, latest);
1227
+ }
1228
+ /**
1229
+ * Check if a specific version exists.
1230
+ *
1231
+ * @param ctx - Convex query context
1232
+ * @param entryId - The content entry ID
1233
+ * @param versionNumber - The version number to check
1234
+ * @returns true if the version exists
1235
+ *
1236
+ * @example
1237
+ * ```typescript
1238
+ * if (await cms.versions.exists(ctx, entry._id, 5)) {
1239
+ * // Version 5 exists
1240
+ * }
1241
+ * ```
1242
+ */
1243
+ async exists(ctx, entryId, versionNumber) {
1244
+ const version = await this.getByNumber(ctx, entryId, versionNumber);
1245
+ return version !== null;
1246
+ }
1247
+ /**
1248
+ * Count total number of versions for an entry.
1249
+ *
1250
+ * @param ctx - Convex query context
1251
+ * @param entryId - The content entry ID
1252
+ * @returns Total number of version snapshots
1253
+ *
1254
+ * @example
1255
+ * ```typescript
1256
+ * const count = await cms.versions.count(ctx, entry._id);
1257
+ * console.log(`Entry has ${count} versions`);
1258
+ * ```
1259
+ */
1260
+ async count(ctx, entryId) {
1261
+ this.ensureVersioningEnabled();
1262
+ let total = 0;
1263
+ let cursor = null;
1264
+ let isDone = false;
1265
+ while (!isDone) {
1266
+ const history = await this.getHistory(ctx, {
1267
+ entryId,
1268
+ paginationOpts: { numItems: 100, cursor },
1269
+ });
1270
+ if (!history)
1271
+ break;
1272
+ total += history.page.length;
1273
+ cursor = history.continueCursor;
1274
+ isDone = history.isDone;
1275
+ }
1276
+ return total;
1277
+ }
1278
+ /**
1279
+ * Rollback a content entry to a previous version.
1280
+ *
1281
+ * This is a non-destructive operation that:
1282
+ * 1. Creates a snapshot of the current state (for undo capability)
1283
+ * 2. Restores data and slug from the target version
1284
+ * 3. Increments the version number
1285
+ * 4. Creates a new snapshot documenting the rollback
1286
+ *
1287
+ * The entry's status, scheduled publish time, and publishing timestamps
1288
+ * are preserved (not restored from the target version).
1289
+ *
1290
+ * @param ctx - Convex mutation context
1291
+ * @param args - Rollback arguments
1292
+ * @returns The updated entry with rolled back content
1293
+ *
1294
+ * @example
1295
+ * ```typescript
1296
+ * // Rollback to version 3
1297
+ * const entry = await cms.versions.rollback(ctx, {
1298
+ * entryId: entry._id,
1299
+ * versionNumber: 3,
1300
+ * updatedBy: currentUserId,
1301
+ * });
1302
+ *
1303
+ * // The entry is now at a new version number (e.g., 7)
1304
+ * // but with content from version 3
1305
+ * console.log(`Rolled back, now at version ${entry.version}`);
1306
+ * ```
1307
+ */
1308
+ async rollback(ctx, args) {
1309
+ this.ensureVersioningEnabled();
1310
+ // Authorization check - versions.rollback
1311
+ await this.authorize(ctx, "versions.rollback", args.updatedBy, args.entryId);
1312
+ // Rate limit check - versions.rollback
1313
+ await this.rateLimit(ctx, "versions.rollback", args.updatedBy);
1314
+ return ctx.runMutation(this.api.versionMutations.rollbackVersion, args);
1315
+ }
1316
+ // =========================================================================
1317
+ // Private Helper Methods
1318
+ // =========================================================================
1319
+ /**
1320
+ * Generate a detailed comparison between two versions.
1321
+ */
1322
+ generateComparison(fromVersion, toVersion) {
1323
+ const changes = [];
1324
+ const fromData = fromVersion.data;
1325
+ const toData = toVersion.data;
1326
+ // Get all unique field names from both versions
1327
+ const allFields = new Set([
1328
+ ...Object.keys(fromData),
1329
+ ...Object.keys(toData),
1330
+ ]);
1331
+ let fieldsAdded = 0;
1332
+ let fieldsRemoved = 0;
1333
+ let fieldsModified = 0;
1334
+ for (const field of allFields) {
1335
+ const oldValue = fromData[field];
1336
+ const newValue = toData[field];
1337
+ const inOld = field in fromData;
1338
+ const inNew = field in toData;
1339
+ let changeType;
1340
+ if (!inOld && inNew) {
1341
+ changeType = "added";
1342
+ fieldsAdded++;
1343
+ }
1344
+ else if (inOld && !inNew) {
1345
+ changeType = "removed";
1346
+ fieldsRemoved++;
1347
+ }
1348
+ else if (!this.deepEqual(oldValue, newValue)) {
1349
+ changeType = "modified";
1350
+ fieldsModified++;
1351
+ }
1352
+ else {
1353
+ changeType = "unchanged";
1354
+ }
1355
+ // Only include changes (skip unchanged fields)
1356
+ if (changeType !== "unchanged") {
1357
+ changes.push({
1358
+ field,
1359
+ changeType,
1360
+ oldValue: inOld ? oldValue : undefined,
1361
+ newValue: inNew ? newValue : undefined,
1362
+ });
1363
+ }
1364
+ }
1365
+ return {
1366
+ fromVersion,
1367
+ toVersion,
1368
+ changes,
1369
+ slugChanged: fromVersion.slug !== toVersion.slug,
1370
+ statusChanged: fromVersion.status !== toVersion.status,
1371
+ summary: {
1372
+ fieldsAdded,
1373
+ fieldsRemoved,
1374
+ fieldsModified,
1375
+ totalChanges: fieldsAdded + fieldsRemoved + fieldsModified,
1376
+ },
1377
+ };
1378
+ }
1379
+ /**
1380
+ * Deep equality check for comparing field values.
1381
+ */
1382
+ deepEqual(a, b) {
1383
+ if (a === b)
1384
+ return true;
1385
+ if (a === null || b === null)
1386
+ return false;
1387
+ if (typeof a !== typeof b)
1388
+ return false;
1389
+ if (typeof a === "object") {
1390
+ if (Array.isArray(a) && Array.isArray(b)) {
1391
+ if (a.length !== b.length)
1392
+ return false;
1393
+ return a.every((item, index) => this.deepEqual(item, b[index]));
1394
+ }
1395
+ if (!Array.isArray(a) && !Array.isArray(b)) {
1396
+ const aObj = a;
1397
+ const bObj = b;
1398
+ const aKeys = Object.keys(aObj);
1399
+ const bKeys = Object.keys(bObj);
1400
+ if (aKeys.length !== bKeys.length)
1401
+ return false;
1402
+ return aKeys.every((key) => this.deepEqual(aObj[key], bObj[key]));
1403
+ }
1404
+ }
1405
+ return false;
1406
+ }
1407
+ }
1408
+ // =============================================================================
1409
+ // Media Assets API Wrapper
1410
+ // =============================================================================
1411
+ /**
1412
+ * Wrapper for media asset operations.
1413
+ */
1414
+ export class MediaAssetsApi {
1415
+ api;
1416
+ config;
1417
+ authHelper;
1418
+ rateLimitHelper;
1419
+ constructor(api, config, authHelper, rateLimitHelper) {
1420
+ this.api = api;
1421
+ this.config = config;
1422
+ this.authHelper = authHelper;
1423
+ this.rateLimitHelper = rateLimitHelper;
1424
+ }
1425
+ /**
1426
+ * Perform authorization check for media asset operations.
1427
+ * @param ctx - The Convex context (passed to authorization hooks for database access)
1428
+ * @param operation - The CMS operation being performed
1429
+ * @param userId - The user performing the operation
1430
+ * @param resourceId - Optional resource ID (for update/delete operations)
1431
+ * @param resourceOwnerId - Optional owner ID for ownership-based permissions
1432
+ * @throws AuthorizationNotConfiguredError if authorization is not configured and permissiveMode is false
1433
+ */
1434
+ async authorize(ctx, operation, userId, resourceId, resourceOwnerId) {
1435
+ if (!this.authHelper) {
1436
+ if (this.config.permissiveMode) {
1437
+ console.warn(`[ConvexCMS] Authorization not configured for "${operation}". ` +
1438
+ "Operations are allowed in permissiveMode, but this should NOT be used in production.");
1439
+ return;
1440
+ }
1441
+ throw new AuthorizationNotConfiguredError(operation);
1442
+ }
1443
+ if (this.authHelper.skipRbac) {
1444
+ return;
1445
+ }
1446
+ if (!userId) {
1447
+ if (this.config.permissiveMode) {
1448
+ console.warn(`[ConvexCMS] Anonymous operation attempted for "${operation}".`);
1449
+ return;
1450
+ }
1451
+ throw new AuthorizationNotConfiguredError(`${operation} (no userId provided - anonymous operations require permissiveMode)`);
1452
+ }
1453
+ const role = await this.authHelper.getUserRole(ctx, userId);
1454
+ await this.authHelper.requireAuthorization(ctx, {
1455
+ operation,
1456
+ userId,
1457
+ role,
1458
+ resourceId,
1459
+ resourceOwnerId,
1460
+ });
1461
+ }
1462
+ /**
1463
+ * Enforce rate limit for media asset operations.
1464
+ * @param ctx - The Convex context (for database access)
1465
+ * @param operation - The CMS operation being performed
1466
+ * @param userId - The user performing the operation
1467
+ */
1468
+ async rateLimit(ctx, operation, userId) {
1469
+ // Skip if no rate limit helper configured
1470
+ if (!this.rateLimitHelper) {
1471
+ return;
1472
+ }
1473
+ const role = userId ? await this.rateLimitHelper.getUserRole(ctx, userId) : null;
1474
+ await this.rateLimitHelper.requireRateLimit(operation, {
1475
+ userId,
1476
+ role,
1477
+ });
1478
+ }
1479
+ /**
1480
+ * Create a new media asset record.
1481
+ *
1482
+ * @param ctx - Convex mutation context
1483
+ * @param args - Asset creation arguments
1484
+ * @returns The created asset
1485
+ *
1486
+ * @example
1487
+ * ```typescript
1488
+ * // After uploading to Convex storage
1489
+ * const asset = await cms.mediaAssets.create(ctx, {
1490
+ * storageId: storageId,
1491
+ * filename: "photo.jpg",
1492
+ * mimeType: "image/jpeg",
1493
+ * size: 102400,
1494
+ * type: "image",
1495
+ * width: 1920,
1496
+ * height: 1080,
1497
+ * });
1498
+ * ```
1499
+ */
1500
+ async create(ctx, args) {
1501
+ if (!this.config.features.mediaManagement) {
1502
+ throw new Error("Media management feature is not enabled");
1503
+ }
1504
+ // Authorization check - mediaAssets.create
1505
+ await this.authorize(ctx, "mediaItems.create", args.createdBy);
1506
+ // Rate limit check - mediaAssets.create (media uploads are high-frequency operations)
1507
+ await this.rateLimit(ctx, "mediaItems.create", args.createdBy);
1508
+ // Validate file size
1509
+ if (args.size && args.size > this.config.maxMediaFileSize) {
1510
+ throw new Error(`File size ${args.size} exceeds maximum allowed size of ${this.config.maxMediaFileSize} bytes`);
1511
+ }
1512
+ // Cast safe: createMediaAsset always returns kind="asset"
1513
+ return ctx.runMutation(this.api.mediaAssetMutations.createMediaAsset, args);
1514
+ }
1515
+ /**
1516
+ * Update media asset metadata.
1517
+ *
1518
+ * @param ctx - Convex mutation context
1519
+ * @param args - Asset update arguments
1520
+ * @returns The updated asset
1521
+ */
1522
+ async update(ctx, args) {
1523
+ if (!this.config.features.mediaManagement) {
1524
+ throw new Error("Media management feature is not enabled");
1525
+ }
1526
+ // Fetch asset for ownership-based authorization
1527
+ const asset = await ctx.runQuery(this.api.mediaAssets.get, { id: args.id });
1528
+ if (!asset) {
1529
+ throw new Error(`Media asset not found: ${args.id}`);
1530
+ }
1531
+ // Authorization check - mediaAssets.update (with ownership info)
1532
+ await this.authorize(ctx, "mediaItems.update", args.updatedBy, args.id, asset.createdBy);
1533
+ // Rate limit check - mediaAssets.update
1534
+ await this.rateLimit(ctx, "mediaItems.update", args.updatedBy);
1535
+ // Cast safe: updateMediaAsset always returns kind="asset"
1536
+ return ctx.runMutation(this.api.mediaAssetMutations.updateMediaAsset, args);
1537
+ }
1538
+ /**
1539
+ * Soft delete a media asset.
1540
+ *
1541
+ * @param ctx - Convex mutation context
1542
+ * @param args - Delete arguments
1543
+ * @returns The deleted asset
1544
+ */
1545
+ async delete(ctx, args) {
1546
+ if (!this.config.features.mediaManagement) {
1547
+ throw new Error("Media management feature is not enabled");
1548
+ }
1549
+ // Fetch asset for ownership-based authorization
1550
+ const asset = await ctx.runQuery(this.api.mediaAssets.get, { id: args.id });
1551
+ if (!asset) {
1552
+ throw new Error(`Media asset not found: ${args.id}`);
1553
+ }
1554
+ // Authorization check - mediaAssets.delete (with ownership info)
1555
+ await this.authorize(ctx, "mediaItems.delete", args.deletedBy, args.id, asset.createdBy);
1556
+ // Rate limit check - mediaAssets.delete
1557
+ await this.rateLimit(ctx, "mediaItems.delete", args.deletedBy);
1558
+ // Cast safe: deleteMediaAsset always returns kind="asset"
1559
+ return ctx.runMutation(this.api.mediaAssetMutations.deleteMediaAsset, args);
1560
+ }
1561
+ /**
1562
+ * Get a media asset by ID.
1563
+ *
1564
+ * @param ctx - Convex query context
1565
+ * @param args - Get arguments
1566
+ * @returns The asset or null if not found
1567
+ */
1568
+ async get(ctx, args) {
1569
+ if (!this.config.features.mediaManagement) {
1570
+ throw new Error("Media management feature is not enabled");
1571
+ }
1572
+ // Cast safe: mediaAssets.get filters for kind="asset"
1573
+ return ctx.runQuery(this.api.mediaAssets.get, args);
1574
+ }
1575
+ /**
1576
+ * List media assets with optional filters.
1577
+ *
1578
+ * @param ctx - Convex query context
1579
+ * @param args - Query options
1580
+ * @returns Paginated list of assets
1581
+ */
1582
+ async list(ctx, args = {}) {
1583
+ if (!this.config.features.mediaManagement) {
1584
+ throw new Error("Media management feature is not enabled");
1585
+ }
1586
+ return await callQuery(ctx, this.api.mediaAssets.list, args);
1587
+ }
1588
+ /**
1589
+ * Generate a temporary upload URL for client-side file uploads.
1590
+ *
1591
+ * The upload flow works as follows:
1592
+ * 1. Call this method to get a temporary upload URL
1593
+ * 2. POST the file to the URL with Content-Type header set to the file's MIME type
1594
+ * 3. The response contains a `storageId` that references the uploaded file
1595
+ * 4. Call create() to save metadata and link the storageId
1596
+ *
1597
+ * @param ctx - Convex mutation context
1598
+ * @param args - Upload configuration options
1599
+ * @returns Upload URL and constraints
1600
+ *
1601
+ * @example
1602
+ * ```typescript
1603
+ * // Generate URL for image uploads
1604
+ * const { uploadUrl, expiresAt, maxFileSize } = await cms.mediaAssets.generateUploadUrl(ctx, {
1605
+ * maxFileSize: 10 * 1024 * 1024, // 10 MB
1606
+ * allowedMimeTypes: ["image/jpeg", "image/png", "image/webp"],
1607
+ * });
1608
+ *
1609
+ * // Client-side upload:
1610
+ * const response = await fetch(uploadUrl, {
1611
+ * method: "POST",
1612
+ * headers: { "Content-Type": file.type },
1613
+ * body: file,
1614
+ * });
1615
+ * const { storageId } = await response.json();
1616
+ *
1617
+ * // Then save metadata
1618
+ * const asset = await cms.mediaAssets.create(ctx, {
1619
+ * storageId,
1620
+ * filename: file.name,
1621
+ * mimeType: file.type,
1622
+ * size: file.size,
1623
+ * type: "image",
1624
+ * });
1625
+ * ```
1626
+ */
1627
+ async generateUploadUrl(ctx, args = {}) {
1628
+ if (!this.config.features.mediaManagement) {
1629
+ throw new Error("Media management feature is not enabled");
1630
+ }
1631
+ // Rate limit check - mediaAssets.create (upload URL generation precedes asset creation)
1632
+ await this.rateLimit(ctx, "mediaItems.create", args.requestedBy);
1633
+ return ctx.runMutation(this.api.mediaUploadMutations.generateUploadUrl, args);
1634
+ }
1635
+ /**
1636
+ * Restore a soft-deleted media asset.
1637
+ *
1638
+ * @param ctx - Convex mutation context
1639
+ * @param args - Restore arguments
1640
+ * @returns The restored asset
1641
+ *
1642
+ * @example
1643
+ * ```typescript
1644
+ * // Restore a previously deleted asset
1645
+ * const restoredAsset = await cms.mediaAssets.restore(ctx, {
1646
+ * id: assetId,
1647
+ * });
1648
+ * ```
1649
+ */
1650
+ async restore(ctx, args) {
1651
+ if (!this.config.features.mediaManagement) {
1652
+ throw new Error("Media management feature is not enabled");
1653
+ }
1654
+ // Cast safe: restoreMediaAsset always returns kind="asset"
1655
+ return ctx.runMutation(this.api.mediaAssetMutations.restoreMediaAsset, args);
1656
+ }
1657
+ /**
1658
+ * Find content entries that reference a media asset.
1659
+ *
1660
+ * Useful for checking references before deletion or for understanding asset usage.
1661
+ *
1662
+ * @param ctx - Convex query context
1663
+ * @param args - Query arguments
1664
+ * @returns Array of references with entry and field information
1665
+ *
1666
+ * @example
1667
+ * ```typescript
1668
+ * // Check if asset is used before deleting
1669
+ * const references = await cms.mediaAssets.findReferences(ctx, {
1670
+ * id: assetId,
1671
+ * });
1672
+ *
1673
+ * if (references.length > 0) {
1674
+ * console.log(`Asset is used in ${references.length} entries`);
1675
+ * // Maybe show a warning to the user
1676
+ * }
1677
+ * ```
1678
+ */
1679
+ async findReferences(ctx, args) {
1680
+ if (!this.config.features.mediaManagement) {
1681
+ throw new Error("Media management feature is not enabled");
1682
+ }
1683
+ // Map the wrapper's args structure to the generated API's expected structure
1684
+ return callQuery(ctx, this.api.mediaAssetMutations.findMediaAssetReferences, { mediaAssetId: args.id, limit: args.limit });
1685
+ }
1686
+ // ===========================================================================
1687
+ // Taxonomy Methods
1688
+ // ===========================================================================
1689
+ /**
1690
+ * Get taxonomy terms associated with a media asset.
1691
+ *
1692
+ * @param ctx - Convex query context
1693
+ * @param args - Query arguments
1694
+ * @returns Array of terms associated with the media asset
1695
+ *
1696
+ * @example
1697
+ * ```typescript
1698
+ * const tags = await cms.mediaAssets.getTerms(ctx, {
1699
+ * mediaId: imageId,
1700
+ * });
1701
+ * ```
1702
+ */
1703
+ async getTerms(ctx, args) {
1704
+ if (!this.config.features.mediaManagement) {
1705
+ throw new Error("Media management feature is not enabled");
1706
+ }
1707
+ return callQuery(ctx, this.api.taxonomies.getTermsByMedia, args);
1708
+ }
1709
+ /**
1710
+ * Set terms for a media asset in a taxonomy (replaces existing terms).
1711
+ *
1712
+ * @param ctx - Convex mutation context
1713
+ * @param args - Mutation arguments
1714
+ *
1715
+ * @example
1716
+ * ```typescript
1717
+ * await cms.mediaAssets.setTerms(ctx, {
1718
+ * mediaId: imageId,
1719
+ * taxonomyId: categoriesTaxonomyId,
1720
+ * termIds: [landscapeId, natureId],
1721
+ * userId: currentUserId,
1722
+ * });
1723
+ * ```
1724
+ */
1725
+ async setTerms(ctx, args) {
1726
+ if (!this.config.features.mediaManagement) {
1727
+ throw new Error("Media management feature is not enabled");
1728
+ }
1729
+ await this.authorize(ctx, "mediaItems.update", args.userId, args.mediaId);
1730
+ await ctx.runMutation(this.api.taxonomyMutations.setMediaTerms, {
1731
+ mediaId: args.mediaId,
1732
+ taxonomyId: args.taxonomyId,
1733
+ termIds: args.termIds,
1734
+ });
1735
+ }
1736
+ /**
1737
+ * Add a single term to a media asset.
1738
+ *
1739
+ * @param ctx - Convex mutation context
1740
+ * @param args - Mutation arguments
1741
+ *
1742
+ * @example
1743
+ * ```typescript
1744
+ * await cms.mediaAssets.addTerm(ctx, {
1745
+ * mediaId: imageId,
1746
+ * termId: landscapeId,
1747
+ * userId: currentUserId,
1748
+ * });
1749
+ * ```
1750
+ */
1751
+ async addTerm(ctx, args) {
1752
+ if (!this.config.features.mediaManagement) {
1753
+ throw new Error("Media management feature is not enabled");
1754
+ }
1755
+ await this.authorize(ctx, "mediaItems.update", args.userId, args.mediaId);
1756
+ await ctx.runMutation(this.api.taxonomyMutations.addTermToMedia, {
1757
+ mediaId: args.mediaId,
1758
+ termId: args.termId,
1759
+ });
1760
+ }
1761
+ /**
1762
+ * Remove a term from a media asset.
1763
+ *
1764
+ * @param ctx - Convex mutation context
1765
+ * @param args - Mutation arguments
1766
+ *
1767
+ * @example
1768
+ * ```typescript
1769
+ * await cms.mediaAssets.removeTerm(ctx, {
1770
+ * mediaId: imageId,
1771
+ * termId: landscapeId,
1772
+ * userId: currentUserId,
1773
+ * });
1774
+ * ```
1775
+ */
1776
+ async removeTerm(ctx, args) {
1777
+ if (!this.config.features.mediaManagement) {
1778
+ throw new Error("Media management feature is not enabled");
1779
+ }
1780
+ await this.authorize(ctx, "mediaItems.update", args.userId, args.mediaId);
1781
+ await ctx.runMutation(this.api.taxonomyMutations.removeTermFromMedia, {
1782
+ mediaId: args.mediaId,
1783
+ termId: args.termId,
1784
+ });
1785
+ }
1786
+ /**
1787
+ * Create a term inline and add it to a media asset.
1788
+ *
1789
+ * @param ctx - Convex mutation context
1790
+ * @param args - Mutation arguments
1791
+ * @returns The created or existing term ID
1792
+ *
1793
+ * @example
1794
+ * ```typescript
1795
+ * const termId = await cms.mediaAssets.createAndAddTerm(ctx, {
1796
+ * taxonomyId: tagsTaxonomyId,
1797
+ * name: "Nature",
1798
+ * mediaId: imageId,
1799
+ * userId: currentUserId,
1800
+ * });
1801
+ * ```
1802
+ */
1803
+ async createAndAddTerm(ctx, args) {
1804
+ if (!this.config.features.mediaManagement) {
1805
+ throw new Error("Media management feature is not enabled");
1806
+ }
1807
+ await this.authorize(ctx, "mediaItems.update", args.userId, args.mediaId);
1808
+ return ctx.runMutation(this.api.taxonomyMutations.createTermAndAddToMedia, args);
1809
+ }
1810
+ }
1811
+ // =============================================================================
1812
+ // Media Folders API Wrapper
1813
+ // =============================================================================
1814
+ /**
1815
+ * Wrapper for media folder operations.
1816
+ */
1817
+ export class MediaFoldersApi {
1818
+ api;
1819
+ config;
1820
+ authHelper;
1821
+ rateLimitHelper;
1822
+ constructor(api, config, authHelper, rateLimitHelper) {
1823
+ this.api = api;
1824
+ this.config = config;
1825
+ this.authHelper = authHelper;
1826
+ this.rateLimitHelper = rateLimitHelper;
1827
+ }
1828
+ /**
1829
+ * Perform authorization check for media folder operations.
1830
+ * @param ctx - The Convex context (passed to authorization hooks for database access)
1831
+ * @param operation - The CMS operation being performed
1832
+ * @param userId - The user performing the operation
1833
+ * @param resourceId - Optional resource ID (for update/delete operations)
1834
+ * @param resourceOwnerId - Optional owner ID for ownership-based permissions
1835
+ */
1836
+ async authorize(ctx, operation, userId, resourceId, resourceOwnerId) {
1837
+ if (!this.authHelper) {
1838
+ if (this.config.permissiveMode) {
1839
+ console.warn(`[ConvexCMS] Authorization not configured for "${operation}". ` +
1840
+ "Operations are allowed in permissiveMode, but this should NOT be used in production.");
1841
+ return;
1842
+ }
1843
+ throw new AuthorizationNotConfiguredError(operation);
1844
+ }
1845
+ if (this.authHelper.skipRbac) {
1846
+ return;
1847
+ }
1848
+ if (!userId) {
1849
+ if (this.config.permissiveMode) {
1850
+ console.warn(`[ConvexCMS] Anonymous operation attempted for "${operation}".`);
1851
+ return;
1852
+ }
1853
+ throw new AuthorizationNotConfiguredError(`${operation} (no userId provided - anonymous operations require permissiveMode)`);
1854
+ }
1855
+ const role = await this.authHelper.getUserRole(ctx, userId);
1856
+ await this.authHelper.requireAuthorization(ctx, {
1857
+ operation,
1858
+ userId,
1859
+ role,
1860
+ resourceId,
1861
+ resourceOwnerId,
1862
+ });
1863
+ }
1864
+ /**
1865
+ * Enforce rate limit for media folder operations.
1866
+ * @param ctx - The Convex context (for database access)
1867
+ * @param operation - The CMS operation being performed
1868
+ * @param userId - The user performing the operation
1869
+ */
1870
+ async rateLimit(ctx, operation, userId) {
1871
+ // Skip if no rate limit helper configured
1872
+ if (!this.rateLimitHelper) {
1873
+ return;
1874
+ }
1875
+ const role = userId ? await this.rateLimitHelper.getUserRole(ctx, userId) : null;
1876
+ await this.rateLimitHelper.requireRateLimit(operation, {
1877
+ userId,
1878
+ role,
1879
+ });
1880
+ }
1881
+ /**
1882
+ * Create a new media folder.
1883
+ *
1884
+ * @param ctx - Convex mutation context
1885
+ * @param args - Folder creation arguments
1886
+ * @returns The created folder
1887
+ */
1888
+ async create(ctx, args) {
1889
+ if (!this.config.features.mediaManagement) {
1890
+ throw new Error("Media management feature is not enabled");
1891
+ }
1892
+ // Authorization check - mediaFolders.create
1893
+ await this.authorize(ctx, "mediaItems.create", args.createdBy);
1894
+ // Rate limit check - mediaFolders.create
1895
+ await this.rateLimit(ctx, "mediaItems.create", args.createdBy);
1896
+ // Cast safe: createMediaFolder always returns kind="folder"
1897
+ return ctx.runMutation(this.api.mediaFolderMutations.createMediaFolder, args);
1898
+ }
1899
+ /**
1900
+ * Update a media folder.
1901
+ *
1902
+ * @param ctx - Convex mutation context
1903
+ * @param args - Folder update arguments
1904
+ * @returns The updated folder
1905
+ */
1906
+ async update(ctx, args) {
1907
+ if (!this.config.features.mediaManagement) {
1908
+ throw new Error("Media management feature is not enabled");
1909
+ }
1910
+ // Fetch folder for ownership-based authorization
1911
+ const folder = await ctx.runQuery(this.api.mediaFolderMutations.getMediaFolder, { id: args.id });
1912
+ if (!folder) {
1913
+ throw new Error(`Media folder not found: ${args.id}`);
1914
+ }
1915
+ // Authorization check - mediaFolders.update (with ownership info)
1916
+ await this.authorize(ctx, "mediaItems.update", args.updatedBy, args.id, folder.createdBy);
1917
+ // Rate limit check - mediaFolders.update
1918
+ await this.rateLimit(ctx, "mediaItems.update", args.updatedBy);
1919
+ // Cast safe: updateMediaFolder always returns kind="folder"
1920
+ return ctx.runMutation(this.api.mediaFolderMutations.updateMediaFolder, args);
1921
+ }
1922
+ /**
1923
+ * Soft delete a media folder.
1924
+ *
1925
+ * @param ctx - Convex mutation context
1926
+ * @param args - Delete arguments
1927
+ * @returns The deleted folder
1928
+ */
1929
+ async delete(ctx, args) {
1930
+ if (!this.config.features.mediaManagement) {
1931
+ throw new Error("Media management feature is not enabled");
1932
+ }
1933
+ // Fetch folder for ownership-based authorization
1934
+ const folder = await ctx.runQuery(this.api.mediaFolderMutations.getMediaFolder, { id: args.id });
1935
+ if (!folder) {
1936
+ throw new Error(`Media folder not found: ${args.id}`);
1937
+ }
1938
+ // Authorization check - mediaFolders.delete (with ownership info)
1939
+ await this.authorize(ctx, "mediaItems.delete", args.deletedBy, args.id, folder.createdBy);
1940
+ // Rate limit check - mediaFolders.delete
1941
+ await this.rateLimit(ctx, "mediaItems.delete", args.deletedBy);
1942
+ // Cast safe: deleteMediaFolder always returns kind="folder"
1943
+ return ctx.runMutation(this.api.mediaFolderMutations.deleteMediaFolder, args);
1944
+ }
1945
+ /**
1946
+ * Get a media folder by ID.
1947
+ *
1948
+ * @param ctx - Convex query context
1949
+ * @param args - Get arguments
1950
+ * @returns The folder or null if not found
1951
+ */
1952
+ async get(ctx, args) {
1953
+ if (!this.config.features.mediaManagement) {
1954
+ throw new Error("Media management feature is not enabled");
1955
+ }
1956
+ // Cast safe: getMediaFolder filters for kind="folder"
1957
+ return ctx.runQuery(this.api.mediaFolderMutations.getMediaFolder, args);
1958
+ }
1959
+ /**
1960
+ * List media folders.
1961
+ *
1962
+ * @param ctx - Convex query context
1963
+ * @param args - Optional filter arguments
1964
+ * @returns Array of folders
1965
+ */
1966
+ async list(ctx, args = {}) {
1967
+ if (!this.config.features.mediaManagement) {
1968
+ throw new Error("Media management feature is not enabled");
1969
+ }
1970
+ // Cast safe: listMediaFolders filters for kind="folder"
1971
+ return ctx.runQuery(this.api.mediaFolderMutations.listMediaFolders, args);
1972
+ }
1973
+ /**
1974
+ * Move a folder to a new parent.
1975
+ *
1976
+ * @param ctx - Convex mutation context
1977
+ * @param args - Move arguments
1978
+ * @returns The moved folder with updated path
1979
+ */
1980
+ async move(ctx, args) {
1981
+ if (!this.config.features.mediaManagement) {
1982
+ throw new Error("Media management feature is not enabled");
1983
+ }
1984
+ // Fetch folder for ownership-based authorization
1985
+ const folder = await ctx.runQuery(this.api.mediaFolderMutations.getMediaFolder, { id: args.id });
1986
+ if (!folder) {
1987
+ throw new Error(`Media folder not found: ${args.id}`);
1988
+ }
1989
+ // Authorization check - mediaFolders.move (with ownership info)
1990
+ await this.authorize(ctx, "mediaItems.move", args.updatedBy, args.id, folder.createdBy);
1991
+ // Rate limit check - mediaFolders.move
1992
+ await this.rateLimit(ctx, "mediaItems.move", args.updatedBy);
1993
+ // Cast safe: moveMediaFolder always returns kind="folder"
1994
+ return ctx.runMutation(this.api.mediaFolderMutations.moveMediaFolder, args);
1995
+ }
1996
+ /**
1997
+ * Restore a soft-deleted media folder.
1998
+ *
1999
+ * @param ctx - Convex mutation context
2000
+ * @param args - Restore arguments
2001
+ * @returns The restored folder
2002
+ *
2003
+ * @example
2004
+ * ```typescript
2005
+ * // Restore a folder and all its contents
2006
+ * const restoredFolder = await cms.mediaFolders.restore(ctx, {
2007
+ * id: folderId,
2008
+ * recursive: true,
2009
+ * });
2010
+ * ```
2011
+ */
2012
+ async restore(ctx, args) {
2013
+ if (!this.config.features.mediaManagement) {
2014
+ throw new Error("Media management feature is not enabled");
2015
+ }
2016
+ // Cast safe: restoreMediaFolder always returns kind="folder"
2017
+ return ctx.runMutation(this.api.mediaFolderMutations.restoreMediaFolder, args);
2018
+ }
2019
+ /**
2020
+ * Get a folder by its path.
2021
+ *
2022
+ * @param ctx - Convex query context
2023
+ * @param args - Query arguments with path
2024
+ * @returns The folder or null if not found
2025
+ *
2026
+ * @example
2027
+ * ```typescript
2028
+ * // Find folder by path
2029
+ * const folder = await cms.mediaFolders.getByPath(ctx, {
2030
+ * path: "/Images/Blog/2026",
2031
+ * });
2032
+ * ```
2033
+ */
2034
+ async getByPath(ctx, args) {
2035
+ if (!this.config.features.mediaManagement) {
2036
+ throw new Error("Media management feature is not enabled");
2037
+ }
2038
+ // Cast safe: getMediaFolderByPath filters for kind="folder"
2039
+ return ctx.runQuery(this.api.mediaFolderMutations.getMediaFolderByPath, args);
2040
+ }
2041
+ /**
2042
+ * Get the entire folder tree as a flat list sorted by path.
2043
+ *
2044
+ * Useful for building folder navigation or selectors.
2045
+ *
2046
+ * @param ctx - Convex query context
2047
+ * @param args - Optional filter arguments
2048
+ * @returns Array of all folders sorted hierarchically by path
2049
+ *
2050
+ * @example
2051
+ * ```typescript
2052
+ * // Get all folders for a tree view
2053
+ * const folders = await cms.mediaFolders.getTree(ctx, {});
2054
+ *
2055
+ * // Build a nested structure
2056
+ * const rootFolders = folders.filter(f => !f.parentId);
2057
+ * ```
2058
+ */
2059
+ async getTree(ctx, args = {}) {
2060
+ if (!this.config.features.mediaManagement) {
2061
+ throw new Error("Media management feature is not enabled");
2062
+ }
2063
+ // Cast safe: getFolderTree filters for kind="folder"
2064
+ return ctx.runQuery(this.api.mediaFolderMutations.getFolderTree, args);
2065
+ }
2066
+ }
2067
+ // =============================================================================
2068
+ // Media Variants API Wrapper
2069
+ // =============================================================================
2070
+ /**
2071
+ * Wrapper for media variant operations.
2072
+ *
2073
+ * Media variants are optimized versions of media assets (thumbnails, responsive
2074
+ * sizes, format conversions). This API provides methods for creating, listing,
2075
+ * and managing variants.
2076
+ *
2077
+ * @example
2078
+ * ```typescript
2079
+ * // Get all variants for an asset
2080
+ * const variants = await cms.mediaVariants.list(ctx, {
2081
+ * assetId: assetId,
2082
+ * status: "completed",
2083
+ * });
2084
+ *
2085
+ * // Get responsive srcset for an image
2086
+ * const srcset = await cms.mediaVariants.getResponsiveSrcset(ctx, {
2087
+ * assetId: assetId,
2088
+ * format: "webp",
2089
+ * });
2090
+ * // Use: <img src={srcset.src} srcset={srcset.srcset} sizes="100vw" />
2091
+ * ```
2092
+ */
2093
+ export class MediaVariantsApi {
2094
+ api;
2095
+ config;
2096
+ constructor(api, config) {
2097
+ this.api = api;
2098
+ this.config = config;
2099
+ }
2100
+ /**
2101
+ * Create a media variant after external processing.
2102
+ *
2103
+ * Use this when variant processing happens externally (e.g., in a serverless
2104
+ * function or image processing service) and you need to register the
2105
+ * completed variant.
2106
+ *
2107
+ * @param ctx - Convex mutation context
2108
+ * @param args - Variant creation arguments
2109
+ * @returns The created variant with URL
2110
+ *
2111
+ * @example
2112
+ * ```typescript
2113
+ * // After processing image externally and uploading result
2114
+ * const variant = await cms.mediaVariants.create(ctx, {
2115
+ * assetId: assetId,
2116
+ * storageId: processedStorageId,
2117
+ * variantType: "responsive",
2118
+ * width: 480,
2119
+ * height: 320,
2120
+ * format: "webp",
2121
+ * mimeType: "image/webp",
2122
+ * size: 25600,
2123
+ * quality: 80,
2124
+ * preset: "small",
2125
+ * });
2126
+ * ```
2127
+ */
2128
+ async create(ctx, args) {
2129
+ if (!this.config.features.mediaManagement) {
2130
+ throw new Error("Media management feature is not enabled");
2131
+ }
2132
+ return ctx.runMutation(this.api.mediaVariantMutations.createMediaVariant, args);
2133
+ }
2134
+ /**
2135
+ * Request async generation of a variant.
2136
+ *
2137
+ * Creates a variant record with "pending" status. An external processing
2138
+ * system should pick up pending variants, process them, and update the status.
2139
+ *
2140
+ * @param ctx - Convex mutation context
2141
+ * @param args - Generation request arguments
2142
+ * @returns The pending variant
2143
+ */
2144
+ async requestGeneration(ctx, args) {
2145
+ if (!this.config.features.mediaManagement) {
2146
+ throw new Error("Media management feature is not enabled");
2147
+ }
2148
+ return ctx.runMutation(this.api.mediaVariantMutations.requestVariantGeneration, args);
2149
+ }
2150
+ /**
2151
+ * Get a variant by ID.
2152
+ *
2153
+ * @param ctx - Convex query context
2154
+ * @param args - Query arguments
2155
+ * @returns The variant with URL or null
2156
+ */
2157
+ async get(ctx, args) {
2158
+ if (!this.config.features.mediaManagement) {
2159
+ throw new Error("Media management feature is not enabled");
2160
+ }
2161
+ return ctx.runQuery(this.api.mediaVariants.get, args);
2162
+ }
2163
+ /**
2164
+ * List variants for an asset.
2165
+ *
2166
+ * @param ctx - Convex query context
2167
+ * @param args - Query arguments with filters
2168
+ * @returns Array of variants with URLs
2169
+ *
2170
+ * @example
2171
+ * ```typescript
2172
+ * // Get all completed responsive variants
2173
+ * const variants = await cms.mediaVariants.list(ctx, {
2174
+ * assetId: assetId,
2175
+ * variantType: "responsive",
2176
+ * status: "completed",
2177
+ * });
2178
+ * ```
2179
+ */
2180
+ async list(ctx, args) {
2181
+ if (!this.config.features.mediaManagement) {
2182
+ throw new Error("Media management feature is not enabled");
2183
+ }
2184
+ return ctx.runQuery(this.api.mediaVariants.list, args);
2185
+ }
2186
+ /**
2187
+ * Find the best matching variant for target dimensions.
2188
+ *
2189
+ * @param ctx - Convex query context
2190
+ * @param args - Target size and preferences
2191
+ * @returns Best matching variant or null
2192
+ *
2193
+ * @example
2194
+ * ```typescript
2195
+ * // Get best variant for 400px wide container
2196
+ * const variant = await cms.mediaVariants.getBestVariant(ctx, {
2197
+ * assetId: assetId,
2198
+ * targetWidth: 400,
2199
+ * preferredFormat: "webp",
2200
+ * });
2201
+ * ```
2202
+ */
2203
+ async getBestVariant(ctx, args) {
2204
+ if (!this.config.features.mediaManagement) {
2205
+ throw new Error("Media management feature is not enabled");
2206
+ }
2207
+ return ctx.runQuery(this.api.mediaVariants.getBestVariant, args);
2208
+ }
2209
+ /**
2210
+ * Get responsive srcset data for HTML img/picture tags.
2211
+ *
2212
+ * @param ctx - Convex query context
2213
+ * @param args - Asset ID and optional format filter
2214
+ * @returns Srcset data for responsive images
2215
+ *
2216
+ * @example
2217
+ * ```typescript
2218
+ * const srcset = await cms.mediaVariants.getResponsiveSrcset(ctx, {
2219
+ * assetId: assetId,
2220
+ * format: "webp",
2221
+ * });
2222
+ *
2223
+ * // In React:
2224
+ * <img src={srcset.src} srcset={srcset.srcset} sizes="100vw" />
2225
+ * ```
2226
+ */
2227
+ async getResponsiveSrcset(ctx, args) {
2228
+ if (!this.config.features.mediaManagement) {
2229
+ throw new Error("Media management feature is not enabled");
2230
+ }
2231
+ return ctx.runQuery(this.api.mediaVariants.getResponsiveSrcset, args);
2232
+ }
2233
+ /**
2234
+ * Get an asset with all its variants organized by type.
2235
+ *
2236
+ * @param ctx - Convex query context
2237
+ * @param args - Asset ID
2238
+ * @returns Asset with variants or null
2239
+ */
2240
+ async getAssetWithVariants(ctx, args) {
2241
+ if (!this.config.features.mediaManagement) {
2242
+ throw new Error("Media management feature is not enabled");
2243
+ }
2244
+ return ctx.runQuery(this.api.mediaVariants.getAssetWithVariants, args);
2245
+ }
2246
+ /**
2247
+ * Get available variant presets.
2248
+ *
2249
+ * @param ctx - Convex query context
2250
+ * @returns Array of preset configurations
2251
+ */
2252
+ async getPresets(ctx) {
2253
+ if (!this.config.features.mediaManagement) {
2254
+ throw new Error("Media management feature is not enabled");
2255
+ }
2256
+ return ctx.runQuery(this.api.mediaVariants.getPresets, {});
2257
+ }
2258
+ /**
2259
+ * Generate variants from preset configurations.
2260
+ *
2261
+ * Queues multiple variants for async processing.
2262
+ *
2263
+ * @param ctx - Convex mutation context
2264
+ * @param args - Asset ID and preset names
2265
+ * @returns Summary of created variant requests
2266
+ *
2267
+ * @example
2268
+ * ```typescript
2269
+ * // Generate standard responsive set
2270
+ * const result = await cms.mediaVariants.generateFromPresets(ctx, {
2271
+ * assetId: assetId,
2272
+ * presets: ["thumbnail", "small", "medium", "large"],
2273
+ * });
2274
+ * console.log(`Queued ${result.succeeded} variants`);
2275
+ * ```
2276
+ */
2277
+ async generateFromPresets(ctx, args) {
2278
+ if (!this.config.features.mediaManagement) {
2279
+ throw new Error("Media management feature is not enabled");
2280
+ }
2281
+ return ctx.runMutation(this.api.mediaVariantMutations.generateFromPresets, args);
2282
+ }
2283
+ /**
2284
+ * Delete a variant.
2285
+ *
2286
+ * @param ctx - Convex mutation context
2287
+ * @param args - Delete arguments
2288
+ * @returns The deleted variant
2289
+ */
2290
+ async delete(ctx, args) {
2291
+ if (!this.config.features.mediaManagement) {
2292
+ throw new Error("Media management feature is not enabled");
2293
+ }
2294
+ return ctx.runMutation(this.api.mediaVariantMutations.deleteMediaVariant, args);
2295
+ }
2296
+ /**
2297
+ * Delete all variants for an asset.
2298
+ *
2299
+ * @param ctx - Convex mutation context
2300
+ * @param args - Asset ID and delete options
2301
+ * @returns Summary of deleted variants
2302
+ */
2303
+ async deleteAllForAsset(ctx, args) {
2304
+ if (!this.config.features.mediaManagement) {
2305
+ throw new Error("Media management feature is not enabled");
2306
+ }
2307
+ return ctx.runMutation(this.api.mediaVariantMutations.deleteAssetVariants, args);
2308
+ }
2309
+ /**
2310
+ * Restore a soft-deleted variant.
2311
+ *
2312
+ * @param ctx - Convex mutation context
2313
+ * @param args - Variant ID to restore
2314
+ * @returns The restored variant
2315
+ */
2316
+ async restore(ctx, args) {
2317
+ if (!this.config.features.mediaManagement) {
2318
+ throw new Error("Media management feature is not enabled");
2319
+ }
2320
+ return ctx.runMutation(this.api.mediaVariantMutations.restoreMediaVariant, args);
2321
+ }
2322
+ }
2323
+ /**
2324
+ * Creates an enhanced CMS client with typed method wrappers.
2325
+ *
2326
+ * This is the main entry point for using the Convex CMS component.
2327
+ * The returned client provides typed methods for all CMS operations.
2328
+ *
2329
+ * @param componentApi - The component API from `components.convexCms`
2330
+ * @param config - Optional configuration options
2331
+ * @returns An enhanced CMS client instance
2332
+ *
2333
+ * @example
2334
+ * ```typescript
2335
+ * import { createCmsClient } from "@convex-cms/core";
2336
+ * import { components } from "./_generated/api";
2337
+ *
2338
+ * // Create with default configuration
2339
+ * export const cms = createCmsClient(components.convexCms);
2340
+ *
2341
+ * // Create with custom configuration
2342
+ * export const cms = createCmsClient(components.convexCms, {
2343
+ * defaultLocale: "en-US",
2344
+ * supportedLocales: ["en-US", "es-ES", "fr-FR"],
2345
+ * features: {
2346
+ * versioning: true,
2347
+ * localization: true,
2348
+ * scheduling: true,
2349
+ * },
2350
+ * maxVersionsPerEntry: 100,
2351
+ * });
2352
+ * ```
2353
+ */
2354
+ export function createCmsClient(componentApi, config) {
2355
+ const resolvedConfig = resolveConfig(config);
2356
+ // Store the getUserRole hook from the original config (not resolved)
2357
+ const getUserRoleHook = config?.getUserRole;
2358
+ // Store authorization hooks from config
2359
+ const authHooks = config?.authorizationHooks;
2360
+ // Store rate limit hooks from config
2361
+ const rateLimitHooks = config?.rateLimitHooks;
2362
+ // Create rate limit helper for API classes (only if rateLimitHooks are configured)
2363
+ const rateLimitHelper = rateLimitHooks
2364
+ ? {
2365
+ async getUserRole(ctx, userId) {
2366
+ if (!getUserRoleHook)
2367
+ return null;
2368
+ return getUserRoleHook(ctx, { userId });
2369
+ },
2370
+ async requireRateLimit(operation, options) {
2371
+ const context = createRateLimitContext(operation, options);
2372
+ return requireRateLimit({
2373
+ hooks: rateLimitHooks,
2374
+ context,
2375
+ });
2376
+ },
2377
+ }
2378
+ : undefined;
2379
+ // Create authorization helper for API classes (only if getUserRole is configured)
2380
+ const authHelper = getUserRoleHook
2381
+ ? {
2382
+ async getUserRole(ctx, userId) {
2383
+ return getUserRoleHook(ctx, { userId });
2384
+ },
2385
+ async requireAuthorization(ctx, context) {
2386
+ const fullContext = {
2387
+ ...context,
2388
+ ctx: ctx,
2389
+ };
2390
+ const rbacOptions = contextToRbacOptions(fullContext);
2391
+ const result = await executeAuthorizationHooks({
2392
+ hooks: authHooks,
2393
+ context: fullContext,
2394
+ rbacOptions: rbacOptions ?? undefined,
2395
+ skipRbac: resolvedConfig.skipRbac,
2396
+ });
2397
+ if (!result.allowed) {
2398
+ const rbacMapping = operationToRbac(fullContext.operation);
2399
+ // Import UnauthorizedError dynamically to avoid circular dependency
2400
+ const { UnauthorizedError } = await import("../component/authorization.js");
2401
+ throw new UnauthorizedError(result.reason ?? "Operation not allowed", {
2402
+ code: result.rbacResult?.allowed === false
2403
+ ? result.rbacResult.code
2404
+ : "PERMISSION_DENIED",
2405
+ resource: rbacMapping?.resource,
2406
+ action: rbacMapping?.action,
2407
+ role: fullContext.role ?? undefined,
2408
+ userId: fullContext.userId,
2409
+ });
2410
+ }
2411
+ return result;
2412
+ },
2413
+ skipRbac: resolvedConfig.skipRbac ?? false,
2414
+ }
2415
+ : undefined;
2416
+ return {
2417
+ config: resolvedConfig,
2418
+ api: componentApi,
2419
+ contentTypes: new ContentTypesApi(componentApi, resolvedConfig, authHelper, rateLimitHelper),
2420
+ contentEntries: new ContentEntriesApi(componentApi, resolvedConfig, authHelper, rateLimitHelper),
2421
+ versions: new VersionsApi(componentApi, resolvedConfig, authHelper, rateLimitHelper),
2422
+ mediaAssets: new MediaAssetsApi(componentApi, resolvedConfig, authHelper, rateLimitHelper),
2423
+ mediaFolders: new MediaFoldersApi(componentApi, resolvedConfig, authHelper, rateLimitHelper),
2424
+ mediaVariants: new MediaVariantsApi(componentApi, resolvedConfig),
2425
+ // Locale fallback chain helpers
2426
+ locale: {
2427
+ getConfig() {
2428
+ return {
2429
+ defaultLocale: resolvedConfig.defaultLocale,
2430
+ fallbackChains: resolvedConfig.localeFallbackChains,
2431
+ autoGenerateFallbacks: resolvedConfig.autoGenerateLocaleFallbacks,
2432
+ supportedLocales: resolvedConfig.supportedLocales,
2433
+ };
2434
+ },
2435
+ getFallbackChain(locale) {
2436
+ const fallbackConfig = this.getConfig();
2437
+ return getFallbackChain(locale, fallbackConfig);
2438
+ },
2439
+ resolve(locale) {
2440
+ const fallbackConfig = this.getConfig();
2441
+ return resolveFallbackChain(locale, fallbackConfig);
2442
+ },
2443
+ },
2444
+ isFeatureEnabled(feature) {
2445
+ return resolvedConfig.features[feature] ?? false;
2446
+ },
2447
+ isLocaleSupported(locale) {
2448
+ return resolvedConfig.supportedLocales.includes(locale);
2449
+ },
2450
+ hasUserRoleHook() {
2451
+ return getUserRoleHook !== undefined;
2452
+ },
2453
+ hasAuthorizationHooks() {
2454
+ if (!authHooks)
2455
+ return false;
2456
+ return !!(authHooks.beforeRbac ||
2457
+ authHooks.afterRbac ||
2458
+ authHooks.onDeny ||
2459
+ (authHooks.operationHooks && Object.keys(authHooks.operationHooks).length > 0));
2460
+ },
2461
+ async getUserRole(ctx, userId) {
2462
+ if (!getUserRoleHook) {
2463
+ throw new Error("No getUserRole hook configured. " +
2464
+ "Configure a getUserRole function in createCmsClient options to map user IDs to CMS roles.");
2465
+ }
2466
+ return await getUserRoleHook(ctx, { userId });
2467
+ },
2468
+ async hasPermissionForUser(ctx, userId, permission, options) {
2469
+ if (!getUserRoleHook) {
2470
+ throw new Error("No getUserRole hook configured. " +
2471
+ "Configure a getUserRole function in createCmsClient options to map user IDs to CMS roles.");
2472
+ }
2473
+ const role = await getUserRoleHook(ctx, { userId });
2474
+ // If user has no role, they have no permissions
2475
+ if (role === null) {
2476
+ return {
2477
+ allowed: false,
2478
+ role: null,
2479
+ permission,
2480
+ };
2481
+ }
2482
+ // Check if the role has the requested permission
2483
+ const allowed = hasPermission(role, permission, options?.customRoles);
2484
+ return {
2485
+ allowed,
2486
+ role,
2487
+ permission,
2488
+ };
2489
+ },
2490
+ async authorize(context) {
2491
+ // Build RBAC options from context
2492
+ const rbacOptions = contextToRbacOptions(context);
2493
+ return executeAuthorizationHooks({
2494
+ hooks: authHooks,
2495
+ context,
2496
+ rbacOptions: rbacOptions ?? undefined,
2497
+ skipRbac: resolvedConfig.skipRbac,
2498
+ });
2499
+ },
2500
+ async requireAuthorization(context) {
2501
+ const result = await this.authorize(context);
2502
+ if (!result.allowed) {
2503
+ const rbacMapping = operationToRbac(context.operation);
2504
+ // Import UnauthorizedError dynamically to avoid circular dependency
2505
+ const { UnauthorizedError } = await import("../component/authorization.js");
2506
+ throw new UnauthorizedError(result.reason ?? "Operation not allowed", {
2507
+ code: result.rbacResult?.allowed === false
2508
+ ? result.rbacResult.code
2509
+ : "PERMISSION_DENIED",
2510
+ resource: rbacMapping?.resource,
2511
+ action: rbacMapping?.action,
2512
+ role: context.role ?? undefined,
2513
+ userId: context.userId,
2514
+ });
2515
+ }
2516
+ return result;
2517
+ },
2518
+ // ==========================================================================
2519
+ // Custom Roles Methods
2520
+ // ==========================================================================
2521
+ getCustomRoles() {
2522
+ return resolvedConfig.customRoles;
2523
+ },
2524
+ getCustomRole(roleName) {
2525
+ return resolvedConfig.customRoles[roleName];
2526
+ },
2527
+ isCustomRole(roleName) {
2528
+ return roleName in resolvedConfig.customRoles;
2529
+ },
2530
+ async hasContentTypePermissionForUser(ctx, userId, permission, contentTypeName) {
2531
+ if (!getUserRoleHook) {
2532
+ throw new Error("No getUserRole hook configured. " +
2533
+ "Configure a getUserRole function in createCmsClient options to map user IDs to CMS roles.");
2534
+ }
2535
+ const role = await getUserRoleHook(ctx, { userId });
2536
+ if (role === null) {
2537
+ return {
2538
+ allowed: false,
2539
+ role: null,
2540
+ permission,
2541
+ };
2542
+ }
2543
+ // Use the content-type-aware permission check
2544
+ const allowed = hasContentTypePermission(role, permission, {
2545
+ customRoles: resolvedConfig.customRoles,
2546
+ contentTypeName,
2547
+ });
2548
+ return {
2549
+ allowed,
2550
+ role,
2551
+ permission,
2552
+ };
2553
+ },
2554
+ async getPermittedContentTypesForUser(ctx, userId, action) {
2555
+ if (!getUserRoleHook) {
2556
+ throw new Error("No getUserRole hook configured. " +
2557
+ "Configure a getUserRole function in createCmsClient options to map user IDs to CMS roles.");
2558
+ }
2559
+ const role = await getUserRoleHook(ctx, { userId });
2560
+ if (role === null) {
2561
+ return [];
2562
+ }
2563
+ return getPermittedContentTypes(role, action, {
2564
+ customRoles: resolvedConfig.customRoles,
2565
+ });
2566
+ },
2567
+ getAllRoles() {
2568
+ return {
2569
+ ...DEFAULT_ROLES,
2570
+ ...resolvedConfig.customRoles,
2571
+ };
2572
+ },
2573
+ // ==========================================================================
2574
+ // Resource Ownership Methods
2575
+ // ==========================================================================
2576
+ async canUserPerformOnResource(ctx, userId, resource, action, resourceOwnerId) {
2577
+ if (!getUserRoleHook) {
2578
+ throw new Error("No getUserRole hook configured. " +
2579
+ "Configure a getUserRole function in createCmsClient options to map user IDs to CMS roles.");
2580
+ }
2581
+ const role = await getUserRoleHook(ctx, { userId });
2582
+ // If user has no role, they have no permissions
2583
+ if (role === null) {
2584
+ return {
2585
+ allowed: false,
2586
+ role: null,
2587
+ reason: "No role assigned to user",
2588
+ code: "NO_ROLE",
2589
+ };
2590
+ }
2591
+ // Use the core checkPermission function for comprehensive RBAC check
2592
+ const { checkPermission } = await import("../component/authorization.js");
2593
+ const result = checkPermission({
2594
+ userId,
2595
+ role,
2596
+ resource,
2597
+ action,
2598
+ resourceOwnerId,
2599
+ customRoles: resolvedConfig.customRoles,
2600
+ });
2601
+ if (result.allowed === true) {
2602
+ return {
2603
+ allowed: true,
2604
+ role,
2605
+ grantedScope: result.grantedScope,
2606
+ ownershipVerified: result.ownershipVerified,
2607
+ };
2608
+ }
2609
+ else {
2610
+ // TypeScript narrows result to PermissionDenied when allowed === false
2611
+ const denied = result;
2612
+ return {
2613
+ allowed: false,
2614
+ role,
2615
+ reason: denied.reason,
2616
+ code: denied.code,
2617
+ ownershipRequired: denied.code === "OWNERSHIP_REQUIRED",
2618
+ };
2619
+ }
2620
+ },
2621
+ async requireUserCanPerformOnResource(ctx, userId, resource, action, resourceOwnerId) {
2622
+ const result = await this.canUserPerformOnResource(ctx, userId, resource, action, resourceOwnerId);
2623
+ if (!result.allowed) {
2624
+ // Import UnauthorizedError dynamically to avoid circular dependency
2625
+ const { UnauthorizedError } = await import("../component/authorization.js");
2626
+ throw new UnauthorizedError(result.reason ?? "Operation not allowed", {
2627
+ code: (result.code ?? "PERMISSION_DENIED"),
2628
+ resource,
2629
+ action,
2630
+ role: result.role ?? undefined,
2631
+ userId,
2632
+ requiredScope: result.ownershipRequired ? "own" : undefined,
2633
+ });
2634
+ }
2635
+ return {
2636
+ allowed: true,
2637
+ role: result.role,
2638
+ grantedScope: result.grantedScope,
2639
+ ownershipVerified: result.ownershipVerified ?? false,
2640
+ };
2641
+ },
2642
+ isOwner(userId, resourceOwnerId) {
2643
+ // Import the helper synchronously (it's a simple comparison)
2644
+ if (userId === undefined || resourceOwnerId === undefined) {
2645
+ return false;
2646
+ }
2647
+ return userId === resourceOwnerId;
2648
+ },
2649
+ };
2650
+ }
2651
+ //# sourceMappingURL=wrapper.js.map