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,1009 @@
1
+ /**
2
+ * Content Entry Mutation Functions
3
+ *
4
+ * Provides mutation functions for creating, updating, and deleting content entries.
5
+ * Content entries are instances of content types that hold the actual content data.
6
+ *
7
+ * Content Lifecycle:
8
+ * 1. Content starts as "draft" status by default
9
+ * 2. Draft content can be edited freely without affecting any published version
10
+ * 3. Publishing changes status to "published" and records publish timestamps
11
+ * 4. Unpublishing reverts status to "draft" for further editing
12
+ */
13
+ import { v } from "convex/values";
14
+ import { mutation } from "./_generated/server.js";
15
+ import { contentEntryDoc, createContentEntryArgs, updateContentEntryArgs, publishEntryArgs, deleteContentEntryArgs, duplicateContentEntryArgs, mutationAuthContext, } from "./validators.js";
16
+ import { generateSlug } from "./lib/slugGenerator.js";
17
+ import { ensureUniqueSlug } from "./lib/slugUniqueness.js";
18
+ import { validateContentData, } from "./validation.js";
19
+ import { validateLockForUpdate } from "./contentLock.js";
20
+ import { emitEvent, contentEntryEventType, } from "./eventEmitter.js";
21
+ import { contentTypeNotFound, contentTypeDeleted, contentTypeInactive, contentEntryNotFound, contentEntryDeleted, contentEntryNotDeleted, contentEntryAlreadyPublished, contentEntryNotPublished, contentEntryArchived, contentEntryValidationFailed, contentEntryLocked, contentEntryCreateFailed, contentEntryUpdateFailed, } from "./lib/errors.js";
22
+ import { requireMutationAuth, withResourceOwner } from "./lib/mutationAuth.js";
23
+ import { isDeleted } from "./lib/softDelete.js";
24
+ // =============================================================================
25
+ // Create Entry Mutation
26
+ // =============================================================================
27
+ /**
28
+ * Mutation to create a new content entry.
29
+ *
30
+ * Content entries are created with "draft" status by default. This allows
31
+ * content to be edited and refined before being published to the live site.
32
+ *
33
+ * The mutation will:
34
+ * 1. Validate that the content type exists
35
+ * 2. Generate a slug from the title field (or use provided slug)
36
+ * 3. Ensure the slug is unique within the content type
37
+ * 4. Create the entry with draft status (unless specified otherwise)
38
+ *
39
+ * @param contentTypeId - The ID of the content type this entry belongs to
40
+ * @param data - The content data (validated against content type schema at runtime)
41
+ * @param slug - Optional custom slug (auto-generated from title if not provided)
42
+ * @param locale - Optional locale code for localized content
43
+ * @param primaryEntryId - Reference to primary entry if this is a localized variant
44
+ * @param status - Initial status (defaults to "draft")
45
+ * @param createdBy - Optional user ID for audit trail
46
+ *
47
+ * @returns The created content entry
48
+ *
49
+ * @throws Error if the content type does not exist
50
+ * @throws Error if the content type is not active
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * // Create a new blog post (starts as draft)
55
+ * const post = await ctx.runMutation(api.contentEntryMutations.createEntry, {
56
+ * contentTypeId: blogTypeId,
57
+ * data: {
58
+ * title: "My First Post",
59
+ * content: "<p>Hello world!</p>",
60
+ * },
61
+ * createdBy: currentUserId,
62
+ * });
63
+ *
64
+ * // Create with explicit status
65
+ * const scheduledPost = await ctx.runMutation(api.contentEntryMutations.createEntry, {
66
+ * contentTypeId: blogTypeId,
67
+ * data: { title: "Scheduled Post" },
68
+ * status: "scheduled",
69
+ * scheduledPublishAt: Date.now() + 86400000, // Tomorrow
70
+ * });
71
+ * ```
72
+ */
73
+ export const createEntry = mutation({
74
+ args: {
75
+ ...createContentEntryArgs.fields,
76
+ /** Optional auth context for mutation-level authorization */
77
+ _auth: v.optional(mutationAuthContext),
78
+ },
79
+ returns: contentEntryDoc,
80
+ handler: async (ctx, args) => {
81
+ const { contentTypeId, data, locale, primaryEntryId, createdBy, _auth, } = args;
82
+ // Authorization check - contentEntries.create permission
83
+ requireMutationAuth(_auth, "contentEntries", "create");
84
+ // Validate content type exists and is active
85
+ const contentType = await ctx.db.get(contentTypeId);
86
+ if (!contentType) {
87
+ throw contentTypeNotFound(contentTypeId);
88
+ }
89
+ if (!contentType.isActive) {
90
+ throw contentTypeInactive(contentTypeId, contentType.name);
91
+ }
92
+ if (isDeleted(contentType)) {
93
+ throw contentTypeDeleted(contentTypeId, contentType.name);
94
+ }
95
+ // Determine which field to use for slug generation
96
+ const slugField = contentType.slugField ?? "title";
97
+ const contentData = data;
98
+ // Build the schema for validation
99
+ const schema = {
100
+ name: contentType.name,
101
+ displayName: contentType.displayName,
102
+ description: contentType.description,
103
+ fields: contentType.fields,
104
+ titleField: contentType.titleField,
105
+ slugField: contentType.slugField,
106
+ singleton: contentType.singleton,
107
+ };
108
+ // Validate content data against the content type schema
109
+ const validationResult = validateContentData(contentData, schema);
110
+ if (!validationResult.valid) {
111
+ throw contentEntryValidationFailed(validationResult.errors);
112
+ }
113
+ // Generate or validate slug
114
+ let slug = args.slug;
115
+ if (!slug) {
116
+ // Generate slug from the slug field value
117
+ const slugSource = contentData[slugField];
118
+ if (typeof slugSource === "string" && slugSource.trim()) {
119
+ slug = generateSlug(slugSource);
120
+ }
121
+ else {
122
+ // Fallback to "untitled" if no suitable field value
123
+ slug = "untitled";
124
+ }
125
+ }
126
+ // Ensure slug is unique within this content type
127
+ const queryFn = async (candidateSlug) => {
128
+ return await ctx.db
129
+ .query("contentEntries")
130
+ .withIndex("by_content_type_and_slug", (q) => q.eq("contentTypeId", contentTypeId).eq("slug", candidateSlug))
131
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
132
+ .first();
133
+ };
134
+ const uniqueSlug = await ensureUniqueSlug(slug, queryFn);
135
+ // Default to draft status - content should start unpublished
136
+ const status = args.status ?? "draft";
137
+ // Generate searchable text from text fields
138
+ let searchText = "";
139
+ for (const field of contentType.fields) {
140
+ if (field.searchable && contentData[field.name]) {
141
+ const value = contentData[field.name];
142
+ if (typeof value === "string") {
143
+ searchText += ` ${value}`;
144
+ }
145
+ }
146
+ }
147
+ searchText = searchText.trim() || undefined;
148
+ // Create the entry
149
+ const _now = Date.now();
150
+ const entryId = await ctx.db.insert("contentEntries", {
151
+ contentTypeId,
152
+ slug: uniqueSlug,
153
+ status,
154
+ data,
155
+ locale,
156
+ primaryEntryId,
157
+ version: 1,
158
+ createdBy,
159
+ updatedBy: createdBy,
160
+ searchText,
161
+ });
162
+ // Retrieve and return the created entry
163
+ const entry = await ctx.db.get(entryId);
164
+ if (!entry) {
165
+ throw contentEntryCreateFailed(contentTypeId);
166
+ }
167
+ // Emit content entry created event
168
+ await emitEvent(ctx, {
169
+ eventType: contentEntryEventType("created"),
170
+ resourceType: "contentEntry",
171
+ resourceId: entryId,
172
+ action: "created",
173
+ payload: {
174
+ slug: uniqueSlug,
175
+ contentTypeName: contentType.name,
176
+ contentTypeId: contentTypeId,
177
+ status,
178
+ version: 1,
179
+ locale,
180
+ },
181
+ userId: createdBy,
182
+ });
183
+ return entry;
184
+ },
185
+ });
186
+ // =============================================================================
187
+ // Update Entry Mutation
188
+ // =============================================================================
189
+ /**
190
+ * Mutation to update an existing content entry.
191
+ *
192
+ * Re-validates content against the type schema, optionally regenerates slug,
193
+ * and updates the modification timestamp. Updates are allowed regardless of status:
194
+ * - Draft entries: All fields can be updated freely
195
+ * - Published entries: Updates create a new "working draft" that doesn't
196
+ * affect the live version until republished
197
+ * - Scheduled entries: Updates modify the scheduled content
198
+ *
199
+ * Key behaviors:
200
+ * 1. **Content Validation**: When data is provided, it's merged with existing data
201
+ * and validated against the content type schema. Invalid data throws an error.
202
+ * 2. **Slug Handling**: Explicit slug takes precedence. If `regenerateSlug` is true
203
+ * and data is updated, the slug is regenerated from the slugField value.
204
+ * 3. **Search Text**: Automatically regenerated from searchable fields when data changes.
205
+ * 4. **Version Tracking**: Version number is incremented on every update.
206
+ *
207
+ * @param id - The content entry ID to update
208
+ * @param slug - Optional new slug (uniqueness will be validated)
209
+ * @param data - Optional new content data (merged with existing, then validated)
210
+ * @param status - Optional new status
211
+ * @param scheduledPublishAt - Optional scheduled publish time (for "scheduled" status)
212
+ * @param updatedBy - Optional user ID for audit trail
213
+ * @param regenerateSlug - If true, regenerates slug from slugField when data is updated
214
+ *
215
+ * @returns The updated content entry
216
+ *
217
+ * @throws Error if the entry does not exist
218
+ * @throws Error if the entry has been deleted
219
+ * @throws Error if the content type has been deleted
220
+ * @throws Error if content validation fails
221
+ * @throws Error if the new slug is not unique
222
+ *
223
+ * @example
224
+ * ```typescript
225
+ * // Update content data (validates against schema)
226
+ * await ctx.runMutation(api.contentEntryMutations.updateEntry, {
227
+ * id: entryId,
228
+ * data: { title: "Updated Title", content: "<p>New content</p>" },
229
+ * updatedBy: currentUserId,
230
+ * });
231
+ *
232
+ * // Change slug explicitly
233
+ * await ctx.runMutation(api.contentEntryMutations.updateEntry, {
234
+ * id: entryId,
235
+ * slug: "new-url-slug",
236
+ * });
237
+ *
238
+ * // Update title and regenerate slug from it
239
+ * await ctx.runMutation(api.contentEntryMutations.updateEntry, {
240
+ * id: entryId,
241
+ * data: { title: "My New Blog Post Title" },
242
+ * regenerateSlug: true,
243
+ * updatedBy: currentUserId,
244
+ * });
245
+ * ```
246
+ */
247
+ export const updateEntry = mutation({
248
+ args: {
249
+ ...updateContentEntryArgs.fields,
250
+ /** Optional auth context for mutation-level authorization */
251
+ _auth: v.optional(mutationAuthContext),
252
+ },
253
+ returns: contentEntryDoc,
254
+ handler: async (ctx, args) => {
255
+ const { id, slug, data, status, scheduledPublishAt, updatedBy, regenerateSlug, _auth, } = args;
256
+ const entry = await ctx.db.get(id);
257
+ if (!entry) {
258
+ throw contentEntryNotFound(id);
259
+ }
260
+ if (isDeleted(entry)) {
261
+ throw contentEntryDeleted(id);
262
+ }
263
+ // Authorization check - contentEntries.update permission (with ownership check)
264
+ requireMutationAuth(withResourceOwner(_auth, entry.createdBy), "contentEntries", "update");
265
+ // Check lock status - only the lock holder can update a locked entry
266
+ const lockValidation = validateLockForUpdate(entry, updatedBy);
267
+ if (!lockValidation.isAllowed) {
268
+ // Extract lock info from entry for detailed error
269
+ if (entry.lockedBy && entry.lockExpiresAt) {
270
+ throw contentEntryLocked(id, entry.lockedBy, entry.lockExpiresAt, updatedBy);
271
+ }
272
+ throw contentEntryLocked(id, "unknown", Date.now(), updatedBy);
273
+ }
274
+ const contentType = await ctx.db.get(entry.contentTypeId);
275
+ if (!contentType) {
276
+ throw contentTypeNotFound(entry.contentTypeId);
277
+ }
278
+ if (isDeleted(contentType)) {
279
+ throw contentTypeDeleted(entry.contentTypeId, contentType.name);
280
+ }
281
+ // Build the update object
282
+ const updates = {
283
+ updatedBy,
284
+ };
285
+ // Merge data if provided, otherwise use existing data
286
+ let mergedData;
287
+ if (data !== undefined) {
288
+ mergedData = { ...entry.data, ...data };
289
+ }
290
+ else {
291
+ mergedData = entry.data;
292
+ }
293
+ // Validate content data against the content type schema
294
+ if (data !== undefined) {
295
+ const schema = {
296
+ name: contentType.name,
297
+ displayName: contentType.displayName,
298
+ description: contentType.description,
299
+ fields: contentType.fields,
300
+ titleField: contentType.titleField,
301
+ slugField: contentType.slugField,
302
+ singleton: contentType.singleton,
303
+ };
304
+ const validationResult = validateContentData(mergedData, schema);
305
+ if (!validationResult.valid) {
306
+ throw contentEntryValidationFailed(validationResult.errors);
307
+ }
308
+ updates.data = mergedData;
309
+ }
310
+ // Helper function for slug uniqueness queries
311
+ const slugQueryFn = async (candidateSlug) => {
312
+ const existing = await ctx.db
313
+ .query("contentEntries")
314
+ .withIndex("by_content_type_and_slug", (q) => q.eq("contentTypeId", entry.contentTypeId).eq("slug", candidateSlug))
315
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
316
+ .first();
317
+ // Exclude current entry from uniqueness check
318
+ if (existing && existing._id !== id) {
319
+ return existing;
320
+ }
321
+ return null;
322
+ };
323
+ // Handle slug: explicit slug takes precedence, then regeneration if requested
324
+ if (slug !== undefined && slug !== entry.slug) {
325
+ // Explicit slug provided - validate and ensure uniqueness
326
+ const uniqueSlug = await ensureUniqueSlug(slug, slugQueryFn, {
327
+ excludeEntryId: id,
328
+ });
329
+ updates.slug = uniqueSlug;
330
+ }
331
+ else if (regenerateSlug && data !== undefined) {
332
+ // Regenerate slug from the slug field value
333
+ const slugField = contentType.slugField ?? "title";
334
+ const slugSource = mergedData[slugField];
335
+ if (typeof slugSource === "string" && slugSource.trim()) {
336
+ const newSlug = generateSlug(slugSource);
337
+ // Only update if the regenerated slug is different from current
338
+ if (newSlug !== entry.slug) {
339
+ const uniqueSlug = await ensureUniqueSlug(newSlug, slugQueryFn, {
340
+ excludeEntryId: id,
341
+ });
342
+ updates.slug = uniqueSlug;
343
+ }
344
+ }
345
+ }
346
+ // Update search text if data changed
347
+ if (data !== undefined) {
348
+ let searchText = "";
349
+ for (const field of contentType.fields) {
350
+ if (field.searchable && mergedData[field.name]) {
351
+ const value = mergedData[field.name];
352
+ if (typeof value === "string") {
353
+ searchText += ` ${value}`;
354
+ }
355
+ }
356
+ }
357
+ updates.searchText = searchText.trim() || undefined;
358
+ }
359
+ // Handle status update
360
+ if (status !== undefined) {
361
+ updates.status = status;
362
+ }
363
+ // Handle scheduled publish time
364
+ if (scheduledPublishAt !== undefined) {
365
+ updates.scheduledPublishAt = scheduledPublishAt;
366
+ }
367
+ // Check if content has changed to determine if we need a version snapshot
368
+ const hasDataChanges = data !== undefined &&
369
+ JSON.stringify(entry.data) !== JSON.stringify(mergedData);
370
+ const hasSlugChanges = updates.slug !== undefined;
371
+ // Create a version snapshot before updating if content changed
372
+ if (hasDataChanges || hasSlugChanges) {
373
+ await ctx.db.insert("contentVersions", {
374
+ entryId: id,
375
+ versionNumber: entry.version,
376
+ data: entry.data,
377
+ slug: entry.slug,
378
+ status: entry.status,
379
+ changeDescription: "Draft saved",
380
+ createdBy: updatedBy,
381
+ wasPublished: false,
382
+ });
383
+ }
384
+ // Increment version number
385
+ updates.version = entry.version + 1;
386
+ // Apply updates
387
+ await ctx.db.patch(id, updates);
388
+ const updatedEntry = await ctx.db.get(id);
389
+ if (!updatedEntry) {
390
+ throw contentEntryUpdateFailed(id);
391
+ }
392
+ // Emit content entry updated event
393
+ await emitEvent(ctx, {
394
+ eventType: contentEntryEventType("updated"),
395
+ resourceType: "contentEntry",
396
+ resourceId: id,
397
+ action: "updated",
398
+ payload: {
399
+ slug: updatedEntry.slug,
400
+ contentTypeName: contentType.name,
401
+ contentTypeId: entry.contentTypeId,
402
+ status: updatedEntry.status,
403
+ version: updatedEntry.version,
404
+ locale: updatedEntry.locale,
405
+ },
406
+ userId: updatedBy,
407
+ });
408
+ return updatedEntry;
409
+ },
410
+ });
411
+ // =============================================================================
412
+ // Publish Entry Mutation
413
+ // =============================================================================
414
+ /**
415
+ * Mutation to publish a content entry.
416
+ *
417
+ * Publishing transitions an entry from "draft" (or "scheduled") to "published"
418
+ * status, making it visible on the live site.
419
+ *
420
+ * When publishing:
421
+ * - Status is set to "published"
422
+ * - firstPublishedAt is set if this is the first publication
423
+ * - lastPublishedAt is updated to current timestamp
424
+ * - Version is incremented
425
+ * - A version snapshot can be created (if versioning is enabled)
426
+ *
427
+ * @param id - The content entry ID to publish
428
+ * @param changeDescription - Optional description of changes (for version history)
429
+ * @param updatedBy - Optional user ID for audit trail
430
+ *
431
+ * @returns The published content entry
432
+ *
433
+ * @throws Error if the entry does not exist
434
+ * @throws Error if the entry has been deleted
435
+ * @throws Error if the entry is already published
436
+ *
437
+ * @example
438
+ * ```typescript
439
+ * const published = await ctx.runMutation(api.contentEntryMutations.publishEntry, {
440
+ * id: entryId,
441
+ * changeDescription: "Initial publication",
442
+ * updatedBy: currentUserId,
443
+ * });
444
+ * ```
445
+ */
446
+ export const publishEntry = mutation({
447
+ args: {
448
+ ...publishEntryArgs.fields,
449
+ /** Optional auth context for mutation-level authorization */
450
+ _auth: v.optional(mutationAuthContext),
451
+ },
452
+ returns: contentEntryDoc,
453
+ handler: async (ctx, args) => {
454
+ const { id, changeDescription, updatedBy, _auth } = args;
455
+ const entry = await ctx.db.get(id);
456
+ if (!entry) {
457
+ throw contentEntryNotFound(id);
458
+ }
459
+ if (isDeleted(entry)) {
460
+ throw contentEntryDeleted(id);
461
+ }
462
+ // Authorization check - contentEntries.publish permission (with ownership check)
463
+ requireMutationAuth(withResourceOwner(_auth, entry.createdBy), "contentEntries", "publish");
464
+ if (entry.status === "published") {
465
+ throw contentEntryAlreadyPublished(id);
466
+ }
467
+ if (entry.status === "archived") {
468
+ throw contentEntryArchived(id);
469
+ }
470
+ const now = Date.now();
471
+ // Create a version snapshot before publishing
472
+ await ctx.db.insert("contentVersions", {
473
+ entryId: id,
474
+ versionNumber: entry.version,
475
+ data: entry.data,
476
+ slug: entry.slug,
477
+ status: entry.status,
478
+ changeDescription,
479
+ createdBy: updatedBy,
480
+ wasPublished: true,
481
+ publishedAt: now,
482
+ });
483
+ // Update the entry to published status
484
+ const updates = {
485
+ status: "published",
486
+ lastPublishedAt: now,
487
+ version: entry.version + 1,
488
+ updatedBy,
489
+ // Clear scheduled publish time if it was set
490
+ scheduledPublishAt: undefined,
491
+ };
492
+ // Set firstPublishedAt only on first publication
493
+ if (entry.firstPublishedAt === undefined) {
494
+ updates.firstPublishedAt = now;
495
+ }
496
+ await ctx.db.patch(id, updates);
497
+ const publishedEntry = await ctx.db.get(id);
498
+ if (!publishedEntry) {
499
+ throw contentEntryUpdateFailed(id);
500
+ }
501
+ const contentType = await ctx.db.get(entry.contentTypeId);
502
+ // Emit content entry published event
503
+ await emitEvent(ctx, {
504
+ eventType: contentEntryEventType("published"),
505
+ resourceType: "contentEntry",
506
+ resourceId: id,
507
+ action: "published",
508
+ payload: {
509
+ slug: publishedEntry.slug,
510
+ contentTypeName: contentType?.name ?? "unknown",
511
+ contentTypeId: entry.contentTypeId,
512
+ status: "published",
513
+ version: publishedEntry.version,
514
+ locale: publishedEntry.locale,
515
+ changeDescription,
516
+ },
517
+ userId: updatedBy,
518
+ });
519
+ return publishedEntry;
520
+ },
521
+ });
522
+ // =============================================================================
523
+ // Unpublish Entry Mutation
524
+ // =============================================================================
525
+ /**
526
+ * Mutation to unpublish a content entry (revert to draft).
527
+ *
528
+ * Unpublishing transitions an entry from "published" back to "draft" status,
529
+ * removing it from the live site while preserving all content for further editing.
530
+ *
531
+ * This is useful for:
532
+ * - Taking content offline temporarily
533
+ * - Making significant changes before republishing
534
+ * - Seasonal content that needs to be hidden
535
+ *
536
+ * @param id - The content entry ID to unpublish
537
+ * @param updatedBy - Optional user ID for audit trail
538
+ *
539
+ * @returns The unpublished content entry (now in draft status)
540
+ *
541
+ * @throws Error if the entry does not exist
542
+ * @throws Error if the entry has been deleted
543
+ * @throws Error if the entry is not currently published
544
+ *
545
+ * @example
546
+ * ```typescript
547
+ * const draft = await ctx.runMutation(api.contentEntryMutations.unpublishEntry, {
548
+ * id: entryId,
549
+ * updatedBy: currentUserId,
550
+ * });
551
+ * console.log(draft.status); // "draft"
552
+ * ```
553
+ */
554
+ export const unpublishEntry = mutation({
555
+ args: {
556
+ /** The ID of the content entry to unpublish */
557
+ id: v.id("contentEntries"),
558
+ /** User ID performing the unpublish (for audit trail) */
559
+ updatedBy: v.optional(v.string()),
560
+ /** Optional auth context for mutation-level authorization */
561
+ _auth: v.optional(mutationAuthContext),
562
+ },
563
+ returns: contentEntryDoc,
564
+ handler: async (ctx, args) => {
565
+ const { id, updatedBy, _auth } = args;
566
+ const entry = await ctx.db.get(id);
567
+ if (!entry) {
568
+ throw contentEntryNotFound(id);
569
+ }
570
+ if (isDeleted(entry)) {
571
+ throw contentEntryDeleted(id);
572
+ }
573
+ // Authorization check - contentEntries.unpublish permission (with ownership check)
574
+ requireMutationAuth(withResourceOwner(_auth, entry.createdBy), "contentEntries", "unpublish");
575
+ if (entry.status !== "published") {
576
+ throw contentEntryNotPublished(id, entry.status);
577
+ }
578
+ await ctx.db.patch(id, {
579
+ status: "draft",
580
+ version: entry.version + 1,
581
+ updatedBy,
582
+ });
583
+ const unpublishedEntry = await ctx.db.get(id);
584
+ if (!unpublishedEntry) {
585
+ throw contentEntryUpdateFailed(id);
586
+ }
587
+ const contentType = await ctx.db.get(entry.contentTypeId);
588
+ // Emit content entry unpublished event
589
+ await emitEvent(ctx, {
590
+ eventType: contentEntryEventType("unpublished"),
591
+ resourceType: "contentEntry",
592
+ resourceId: id,
593
+ action: "unpublished",
594
+ payload: {
595
+ slug: unpublishedEntry.slug,
596
+ contentTypeName: contentType?.name ?? "unknown",
597
+ contentTypeId: entry.contentTypeId,
598
+ status: "draft",
599
+ version: unpublishedEntry.version,
600
+ locale: unpublishedEntry.locale,
601
+ },
602
+ userId: updatedBy,
603
+ });
604
+ return unpublishedEntry;
605
+ },
606
+ });
607
+ // =============================================================================
608
+ // Delete Entry Mutation
609
+ // =============================================================================
610
+ /**
611
+ * Result type for delete operations.
612
+ * Returns the deleted entry with updated deletedAt timestamp.
613
+ */
614
+ const deleteResultDoc = v.object({
615
+ ...contentEntryDoc.fields,
616
+ /** Number of associated versions that were cleaned up */
617
+ deletedVersionsCount: v.optional(v.number()),
618
+ });
619
+ /**
620
+ * Mutation to delete a content entry.
621
+ *
622
+ * By default, performs a soft delete by setting the `deletedAt` timestamp.
623
+ * This allows the entry to be recovered later if needed.
624
+ *
625
+ * When `hardDelete` is true, permanently removes the entry and all
626
+ * associated version snapshots from the database.
627
+ *
628
+ * @param id - The content entry ID to delete
629
+ * @param deletedBy - Optional user ID for audit trail
630
+ * @param hardDelete - If true, permanently deletes entry and versions (default: false)
631
+ *
632
+ * @returns The deleted content entry (with deletedAt set for soft deletes)
633
+ *
634
+ * @throws Error if the entry does not exist
635
+ * @throws Error if the entry has already been deleted (for soft deletes)
636
+ *
637
+ * @example
638
+ * ```typescript
639
+ * // Soft delete (default) - entry can be recovered
640
+ * const deleted = await ctx.runMutation(api.contentEntryMutations.deleteEntry, {
641
+ * id: entryId,
642
+ * deletedBy: currentUserId,
643
+ * });
644
+ *
645
+ * // Hard delete - permanently removes entry and all versions
646
+ * await ctx.runMutation(api.contentEntryMutations.deleteEntry, {
647
+ * id: entryId,
648
+ * deletedBy: currentUserId,
649
+ * hardDelete: true,
650
+ * });
651
+ * ```
652
+ */
653
+ export const deleteEntry = mutation({
654
+ args: {
655
+ ...deleteContentEntryArgs.fields,
656
+ /** Optional auth context for mutation-level authorization */
657
+ _auth: v.optional(mutationAuthContext),
658
+ },
659
+ returns: deleteResultDoc,
660
+ handler: async (ctx, args) => {
661
+ const { id, deletedBy, hardDelete = false, _auth } = args;
662
+ const entry = await ctx.db.get(id);
663
+ if (!entry) {
664
+ throw contentEntryNotFound(id);
665
+ }
666
+ // Authorization check - contentEntries.delete permission (with ownership check)
667
+ requireMutationAuth(withResourceOwner(_auth, entry.createdBy), "contentEntries", "delete");
668
+ // For soft delete, check if already deleted
669
+ if (!hardDelete && isDeleted(entry)) {
670
+ throw contentEntryDeleted(id);
671
+ }
672
+ // Get all associated versions for this entry
673
+ const versions = await ctx.db
674
+ .query("contentVersions")
675
+ .withIndex("by_entry", (q) => q.eq("entryId", id))
676
+ .collect();
677
+ const deletedVersionsCount = versions.length;
678
+ const contentType = await ctx.db.get(entry.contentTypeId);
679
+ if (hardDelete) {
680
+ // Hard delete: permanently remove all versions
681
+ for (const version of versions) {
682
+ await ctx.db.delete(version._id);
683
+ }
684
+ // Permanently delete the entry itself
685
+ await ctx.db.delete(id);
686
+ // Emit content entry deleted event (for hard delete)
687
+ await emitEvent(ctx, {
688
+ eventType: contentEntryEventType("deleted"),
689
+ resourceType: "contentEntry",
690
+ resourceId: id,
691
+ action: "deleted",
692
+ payload: {
693
+ slug: entry.slug,
694
+ contentTypeName: contentType?.name ?? "unknown",
695
+ contentTypeId: entry.contentTypeId,
696
+ status: entry.status,
697
+ version: entry.version,
698
+ locale: entry.locale,
699
+ },
700
+ userId: deletedBy,
701
+ metadata: { hardDelete: true },
702
+ });
703
+ // Return the entry as it was before deletion
704
+ return {
705
+ ...entry,
706
+ deletedAt: Date.now(),
707
+ updatedBy: deletedBy,
708
+ deletedVersionsCount,
709
+ };
710
+ }
711
+ else {
712
+ // Soft delete: set deletedAt timestamp
713
+ const now = Date.now();
714
+ await ctx.db.patch(id, {
715
+ deletedAt: now,
716
+ updatedBy: deletedBy,
717
+ });
718
+ // Emit content entry deleted event (for soft delete)
719
+ await emitEvent(ctx, {
720
+ eventType: contentEntryEventType("deleted"),
721
+ resourceType: "contentEntry",
722
+ resourceId: id,
723
+ action: "deleted",
724
+ payload: {
725
+ slug: entry.slug,
726
+ contentTypeName: contentType?.name ?? "unknown",
727
+ contentTypeId: entry.contentTypeId,
728
+ status: entry.status,
729
+ version: entry.version,
730
+ locale: entry.locale,
731
+ },
732
+ userId: deletedBy,
733
+ metadata: { hardDelete: false },
734
+ });
735
+ return {
736
+ ...entry,
737
+ deletedAt: now,
738
+ updatedBy: deletedBy ?? entry.updatedBy,
739
+ deletedVersionsCount,
740
+ };
741
+ }
742
+ },
743
+ });
744
+ /**
745
+ * Mutation to restore a soft-deleted content entry.
746
+ *
747
+ * Removes the `deletedAt` timestamp from the entry, making it active again.
748
+ * Only works for soft-deleted entries; hard-deleted entries cannot be recovered.
749
+ *
750
+ * @param id - The content entry ID to restore
751
+ * @param restoredBy - Optional user ID for audit trail
752
+ *
753
+ * @returns The restored content entry
754
+ *
755
+ * @throws Error if the entry does not exist
756
+ * @throws Error if the entry is not soft-deleted
757
+ *
758
+ * @example
759
+ * ```typescript
760
+ * const restored = await ctx.runMutation(api.contentEntryMutations.restoreEntry, {
761
+ * id: entryId,
762
+ * restoredBy: currentUserId,
763
+ * });
764
+ * ```
765
+ */
766
+ export const restoreEntry = mutation({
767
+ args: {
768
+ /** The ID of the content entry to restore */
769
+ id: v.id("contentEntries"),
770
+ /** User ID performing the restoration (for audit trail) */
771
+ restoredBy: v.optional(v.string()),
772
+ /** Optional auth context for mutation-level authorization */
773
+ _auth: v.optional(mutationAuthContext),
774
+ },
775
+ returns: contentEntryDoc,
776
+ handler: async (ctx, args) => {
777
+ const { id, restoredBy, _auth } = args;
778
+ const entry = await ctx.db.get(id);
779
+ if (!entry) {
780
+ throw contentEntryNotFound(id);
781
+ }
782
+ // Authorization check - contentEntries.restore permission (with ownership check)
783
+ requireMutationAuth(withResourceOwner(_auth, entry.createdBy), "contentEntries", "restore");
784
+ if (!isDeleted(entry)) {
785
+ throw contentEntryNotDeleted(id);
786
+ }
787
+ // Remove the deletedAt marker to restore the entry
788
+ await ctx.db.patch(id, {
789
+ deletedAt: undefined,
790
+ updatedBy: restoredBy,
791
+ });
792
+ const contentType = await ctx.db.get(entry.contentTypeId);
793
+ // Emit content entry restored event
794
+ await emitEvent(ctx, {
795
+ eventType: contentEntryEventType("restored"),
796
+ resourceType: "contentEntry",
797
+ resourceId: id,
798
+ action: "restored",
799
+ payload: {
800
+ slug: entry.slug,
801
+ contentTypeName: contentType?.name ?? "unknown",
802
+ contentTypeId: entry.contentTypeId,
803
+ status: entry.status,
804
+ version: entry.version,
805
+ locale: entry.locale,
806
+ },
807
+ userId: restoredBy,
808
+ });
809
+ return {
810
+ ...entry,
811
+ deletedAt: undefined,
812
+ updatedBy: restoredBy ?? entry.updatedBy,
813
+ };
814
+ },
815
+ });
816
+ // =============================================================================
817
+ // Duplicate Entry Mutation
818
+ // =============================================================================
819
+ /**
820
+ * Mutation to duplicate (clone) an existing content entry.
821
+ *
822
+ * Creates a new content entry with the same data as the source entry,
823
+ * but with a new unique slug. The duplicated entry is always created
824
+ * as a draft, regardless of the source entry's status.
825
+ *
826
+ * This is useful for:
827
+ * - Content templating workflows (copy a template to create new content)
828
+ * - Creating localized variants of content
829
+ * - Quick duplication of similar content pieces
830
+ *
831
+ * Key behaviors:
832
+ * 1. **Data Cloning**: All content data is deep-copied to the new entry
833
+ * 2. **Media References**: By default, media references (IDs) are copied,
834
+ * pointing to the same media assets. Set `copyMediaReferences: false`
835
+ * to clear media fields in the duplicate.
836
+ * 3. **Slug Generation**: A new unique slug is generated from the source
837
+ * entry's slug (e.g., "my-post" → "my-post-1") unless a custom slug
838
+ * is provided.
839
+ * 4. **Status Reset**: The duplicate always starts as "draft" with version 1
840
+ * 5. **Timestamps Reset**: Publishing timestamps are cleared in the duplicate
841
+ *
842
+ * @param sourceEntryId - The ID of the content entry to duplicate
843
+ * @param slug - Optional custom slug (auto-generated if not provided)
844
+ * @param copyMediaReferences - Whether to copy media IDs (default: true)
845
+ * @param locale - Optional locale for the duplicated entry
846
+ * @param createdBy - Optional user ID for audit trail
847
+ *
848
+ * @returns The newly created duplicate content entry
849
+ *
850
+ * @throws Error if the source entry does not exist
851
+ * @throws Error if the source entry has been deleted
852
+ * @throws Error if the content type does not exist or is not active
853
+ *
854
+ * @example
855
+ * ```typescript
856
+ * // Simple duplication (keeps all media references)
857
+ * const duplicate = await ctx.runMutation(api.contentEntryMutations.duplicateEntry, {
858
+ * sourceEntryId: originalPostId,
859
+ * createdBy: currentUserId,
860
+ * });
861
+ *
862
+ * // Duplicate with custom slug
863
+ * const duplicate = await ctx.runMutation(api.contentEntryMutations.duplicateEntry, {
864
+ * sourceEntryId: templateId,
865
+ * slug: "new-post-from-template",
866
+ * createdBy: currentUserId,
867
+ * });
868
+ *
869
+ * // Duplicate without media references (for a fresh start)
870
+ * const duplicate = await ctx.runMutation(api.contentEntryMutations.duplicateEntry, {
871
+ * sourceEntryId: originalPostId,
872
+ * copyMediaReferences: false,
873
+ * createdBy: currentUserId,
874
+ * });
875
+ * ```
876
+ */
877
+ export const duplicateEntry = mutation({
878
+ args: {
879
+ ...duplicateContentEntryArgs.fields,
880
+ /** Optional auth context for mutation-level authorization */
881
+ _auth: v.optional(mutationAuthContext),
882
+ },
883
+ returns: contentEntryDoc,
884
+ handler: async (ctx, args) => {
885
+ const { sourceEntryId, slug, copyMediaReferences = true, locale, createdBy, _auth, } = args;
886
+ // Authorization check - contentEntries.create permission (duplicate creates a new entry)
887
+ requireMutationAuth(_auth, "contentEntries", "create");
888
+ const sourceEntry = await ctx.db.get(sourceEntryId);
889
+ if (!sourceEntry) {
890
+ throw contentEntryNotFound(sourceEntryId);
891
+ }
892
+ if (isDeleted(sourceEntry)) {
893
+ throw contentEntryDeleted(sourceEntryId);
894
+ }
895
+ // Retrieve and validate the content type
896
+ const contentType = await ctx.db.get(sourceEntry.contentTypeId);
897
+ if (!contentType) {
898
+ throw contentTypeNotFound(sourceEntry.contentTypeId);
899
+ }
900
+ if (!contentType.isActive) {
901
+ throw contentTypeInactive(sourceEntry.contentTypeId, contentType.name);
902
+ }
903
+ if (isDeleted(contentType)) {
904
+ throw contentTypeDeleted(sourceEntry.contentTypeId, contentType.name);
905
+ }
906
+ // Deep copy the content data
907
+ const newData = JSON.parse(JSON.stringify(sourceEntry.data));
908
+ // Optionally clear media references
909
+ if (!copyMediaReferences) {
910
+ const fields = contentType.fields;
911
+ for (const field of fields) {
912
+ if (field.type === "media" && newData[field.name] !== undefined) {
913
+ // Clear media field - set to null for single, empty array for multiple
914
+ const isMultiple = field.options?.multiple;
915
+ newData[field.name] = isMultiple ? [] : null;
916
+ }
917
+ }
918
+ }
919
+ // Build the schema for validation
920
+ const schema = {
921
+ name: contentType.name,
922
+ displayName: contentType.displayName,
923
+ description: contentType.description,
924
+ fields: contentType.fields,
925
+ titleField: contentType.titleField,
926
+ slugField: contentType.slugField,
927
+ singleton: contentType.singleton,
928
+ };
929
+ // Validate the cloned data against the content type schema
930
+ const validationResult = validateContentData(newData, schema);
931
+ if (!validationResult.valid) {
932
+ throw contentEntryValidationFailed(validationResult.errors);
933
+ }
934
+ // Generate or validate slug
935
+ let targetSlug = slug;
936
+ if (!targetSlug) {
937
+ // Generate a slug based on the source entry's slug
938
+ // This will result in something like "original-slug-1" if "original-slug" exists
939
+ targetSlug = sourceEntry.slug;
940
+ }
941
+ // Ensure slug is unique within this content type
942
+ const queryFn = async (candidateSlug) => {
943
+ return await ctx.db
944
+ .query("contentEntries")
945
+ .withIndex("by_content_type_and_slug", (q) => q
946
+ .eq("contentTypeId", sourceEntry.contentTypeId)
947
+ .eq("slug", candidateSlug))
948
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
949
+ .first();
950
+ };
951
+ const uniqueSlug = await ensureUniqueSlug(targetSlug, queryFn);
952
+ // Generate searchable text from text fields
953
+ let searchText = "";
954
+ for (const field of contentType.fields) {
955
+ if (field.searchable && newData[field.name]) {
956
+ const value = newData[field.name];
957
+ if (typeof value === "string") {
958
+ searchText += ` ${value}`;
959
+ }
960
+ }
961
+ }
962
+ searchText = searchText.trim() || undefined;
963
+ // Create the duplicate entry (always as draft with version 1)
964
+ const entryId = await ctx.db.insert("contentEntries", {
965
+ contentTypeId: sourceEntry.contentTypeId,
966
+ slug: uniqueSlug,
967
+ status: "draft",
968
+ data: newData,
969
+ locale: locale ?? sourceEntry.locale,
970
+ // Don't copy primaryEntryId - this is a new independent entry
971
+ version: 1,
972
+ // Reset publishing timestamps - this is a new entry
973
+ firstPublishedAt: undefined,
974
+ lastPublishedAt: undefined,
975
+ scheduledPublishAt: undefined,
976
+ // Don't copy locks
977
+ lockedBy: undefined,
978
+ lockExpiresAt: undefined,
979
+ // Set new audit trail
980
+ createdBy,
981
+ updatedBy: createdBy,
982
+ searchText,
983
+ });
984
+ // Retrieve and return the created entry
985
+ const entry = await ctx.db.get(entryId);
986
+ if (!entry) {
987
+ throw contentEntryCreateFailed(sourceEntry.contentTypeId);
988
+ }
989
+ // Emit content entry duplicated event
990
+ await emitEvent(ctx, {
991
+ eventType: contentEntryEventType("duplicated"),
992
+ resourceType: "contentEntry",
993
+ resourceId: entryId,
994
+ action: "duplicated",
995
+ payload: {
996
+ slug: uniqueSlug,
997
+ contentTypeName: contentType.name,
998
+ contentTypeId: sourceEntry.contentTypeId,
999
+ status: "draft",
1000
+ version: 1,
1001
+ locale: locale ?? sourceEntry.locale,
1002
+ sourceEntryId: sourceEntryId,
1003
+ },
1004
+ userId: createdBy,
1005
+ });
1006
+ return entry;
1007
+ },
1008
+ });
1009
+ //# sourceMappingURL=contentEntryMutations.js.map