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,969 @@
1
+ /**
2
+ * Content Type Mutation Functions
3
+ *
4
+ * Provides mutation functions for creating, updating, and managing content types.
5
+ * Content types define the schema/blueprint for content entries in the CMS.
6
+ */
7
+
8
+ import { v } from "convex/values";
9
+ import { isDeleted } from "./lib/softDelete.js";
10
+ import { mutation } from "./_generated/server.js";
11
+ import {
12
+ createContentTypeArgs,
13
+ updateContentTypeArgs,
14
+ deleteContentTypeArgs,
15
+ contentTypeDoc,
16
+ type FieldType,
17
+ mutationAuthContext,
18
+ } from "./validators.js";
19
+ import type { FieldDefinition } from "./validation.js";
20
+ import {
21
+ emitEvent,
22
+ contentTypeEventType,
23
+ ContentTypeEventPayload,
24
+ } from "./eventEmitter.js";
25
+ import { fieldTypes } from "./schema.js";
26
+ import {
27
+ contentTypeNotFound,
28
+ contentTypeDeleted,
29
+ contentTypeNameInvalid,
30
+ contentTypeNameDuplicate,
31
+ contentTypeFieldValidationFailed,
32
+ contentTypeSlugFieldInvalid,
33
+ contentTypeTitleFieldInvalid,
34
+ contentTypeHasEntries,
35
+ contentTypeBreakingChange,
36
+ // batchSizeExceeded,
37
+ internalError,
38
+ } from "./lib/errors.js";
39
+ import { requireMutationAuth } from "./lib/mutationAuth.js";
40
+
41
+ // =============================================================================
42
+ // Breaking Change Detection Types
43
+ // =============================================================================
44
+
45
+ /**
46
+ * Describes a potential breaking change when updating a content type.
47
+ */
48
+ interface BreakingChange {
49
+ /** Type of breaking change detected */
50
+ type:
51
+ | "FIELD_REMOVED"
52
+ | "FIELD_TYPE_CHANGED"
53
+ | "FIELD_MADE_REQUIRED"
54
+ | "SELECT_OPTIONS_REMOVED"
55
+ | "REFERENCE_TYPES_RESTRICTED"
56
+ | "VALIDATION_TIGHTENED";
57
+ /** The field name affected */
58
+ fieldName: string;
59
+ /** Human-readable description of the breaking change */
60
+ message: string;
61
+ /** Number of entries affected by this change */
62
+ affectedEntriesCount: number;
63
+ }
64
+
65
+ /**
66
+ * Validation error for content type field definitions.
67
+ */
68
+ interface FieldValidationError {
69
+ /** The field name that has the error */
70
+ fieldName: string;
71
+ /** Human-readable error message */
72
+ message: string;
73
+ /** Error code for programmatic handling */
74
+ code:
75
+ | "DUPLICATE_FIELD_NAME"
76
+ | "INVALID_FIELD_TYPE"
77
+ | "MISSING_REQUIRED_PROPERTY"
78
+ | "INVALID_FIELD_NAME"
79
+ | "INVALID_SELECT_OPTIONS";
80
+ }
81
+
82
+ /**
83
+ * Validates the name format for content types and fields.
84
+ * Names must be valid identifiers: lowercase letters, numbers, and underscores.
85
+ * Must start with a letter and be 1-64 characters.
86
+ */
87
+ function isValidName(name: string): boolean {
88
+ const namePattern = /^[a-z][a-z0-9_]{0,63}$/;
89
+ return namePattern.test(name);
90
+ }
91
+
92
+ /**
93
+ * Detects breaking changes between old and new field definitions.
94
+ * Returns an array of breaking changes that would affect existing content entries.
95
+ *
96
+ * @param oldFields - Current field definitions
97
+ * @param newFields - Proposed new field definitions
98
+ * @param existingEntries - Existing content entries to check for impact
99
+ * @returns Array of detected breaking changes with affected entry counts
100
+ */
101
+ function detectBreakingChanges(
102
+ oldFields: FieldDefinition[],
103
+ newFields: FieldDefinition[],
104
+ existingEntries: Array<{ data: Record<string, unknown> }>
105
+ ): BreakingChange[] {
106
+ const breakingChanges: BreakingChange[] = [];
107
+ const oldFieldMap = new Map(oldFields.map((f) => [f.name, f]));
108
+ const newFieldMap = new Map(newFields.map((f) => [f.name, f]));
109
+
110
+ // Check for removed fields that have data in existing entries
111
+ for (const oldField of oldFields) {
112
+ if (!newFieldMap.has(oldField.name)) {
113
+ // Field is being removed - count entries with data in this field
114
+ const affectedCount = existingEntries.filter((entry) => {
115
+ const value = entry.data[oldField.name];
116
+ return value !== undefined && value !== null && value !== "";
117
+ }).length;
118
+
119
+ if (affectedCount > 0) {
120
+ breakingChanges.push({
121
+ type: "FIELD_REMOVED",
122
+ fieldName: oldField.name,
123
+ message: `Removing field "${oldField.name}" will delete data from ${affectedCount} existing entries`,
124
+ affectedEntriesCount: affectedCount,
125
+ });
126
+ }
127
+ }
128
+ }
129
+
130
+ // Check for changes to existing fields
131
+ for (const newField of newFields) {
132
+ const oldField = oldFieldMap.get(newField.name);
133
+ if (!oldField) continue; // New field, no breaking change possible
134
+
135
+ // Check for type changes
136
+ if (oldField.type !== newField.type) {
137
+ const affectedCount = existingEntries.filter((entry) => {
138
+ const value = entry.data[newField.name];
139
+ return value !== undefined && value !== null;
140
+ }).length;
141
+
142
+ if (affectedCount > 0) {
143
+ breakingChanges.push({
144
+ type: "FIELD_TYPE_CHANGED",
145
+ fieldName: newField.name,
146
+ message: `Changing field "${newField.name}" type from "${oldField.type}" to "${newField.type}" may invalidate ${affectedCount} existing entries`,
147
+ affectedEntriesCount: affectedCount,
148
+ });
149
+ }
150
+ }
151
+
152
+ // Check for optional -> required changes
153
+ if (!oldField.required && newField.required) {
154
+ const affectedCount = existingEntries.filter((entry) => {
155
+ const value = entry.data[newField.name];
156
+ return value === undefined || value === null || value === "";
157
+ }).length;
158
+
159
+ if (affectedCount > 0) {
160
+ breakingChanges.push({
161
+ type: "FIELD_MADE_REQUIRED",
162
+ fieldName: newField.name,
163
+ message: `Making field "${newField.name}" required will invalidate ${affectedCount} entries with missing values`,
164
+ affectedEntriesCount: affectedCount,
165
+ });
166
+ }
167
+ }
168
+
169
+ // Check for removed select/multiSelect options
170
+ if (
171
+ (oldField.type === "select" || oldField.type === "multiSelect") &&
172
+ oldField.options?.options &&
173
+ newField.options?.options
174
+ ) {
175
+ const oldOptions = new Set(oldField.options.options.map((o) => o.value));
176
+ const newOptions = new Set(newField.options.options.map((o) => o.value));
177
+ const removedOptions = [...oldOptions].filter((o) => !newOptions.has(o));
178
+
179
+ if (removedOptions.length > 0) {
180
+ const affectedCount = existingEntries.filter((entry) => {
181
+ const value = entry.data[newField.name];
182
+ if (oldField.type === "select") {
183
+ return removedOptions.includes(value as string);
184
+ } else {
185
+ // multiSelect - check if any values are in removed options
186
+ const values = value as string[] | undefined;
187
+ return values?.some((v) => removedOptions.includes(v));
188
+ }
189
+ }).length;
190
+
191
+ if (affectedCount > 0) {
192
+ breakingChanges.push({
193
+ type: "SELECT_OPTIONS_REMOVED",
194
+ fieldName: newField.name,
195
+ message: `Removing options [${removedOptions.join(", ")}] from "${newField.name}" will invalidate ${affectedCount} entries using those values`,
196
+ affectedEntriesCount: affectedCount,
197
+ });
198
+ }
199
+ }
200
+ }
201
+
202
+ // Check for restricted reference content types
203
+ if (
204
+ oldField.type === "reference" &&
205
+ newField.type === "reference" &&
206
+ oldField.options?.allowedContentTypes &&
207
+ newField.options?.allowedContentTypes
208
+ ) {
209
+ const oldAllowed = new Set(oldField.options.allowedContentTypes);
210
+ const newAllowed = new Set(newField.options.allowedContentTypes);
211
+ const removedTypes = [...oldAllowed].filter((t) => !newAllowed.has(t));
212
+
213
+ // Note: We can't easily check if existing references point to removed types
214
+ // without resolving references. This is a warning-level change.
215
+ if (removedTypes.length > 0) {
216
+ breakingChanges.push({
217
+ type: "REFERENCE_TYPES_RESTRICTED",
218
+ fieldName: newField.name,
219
+ message: `Restricting allowed content types for "${newField.name}" by removing [${removedTypes.join(", ")}] may invalidate existing references`,
220
+ affectedEntriesCount: existingEntries.length, // Potentially all entries
221
+ });
222
+ }
223
+ }
224
+
225
+ // Check for tightened validation (minLength increased, maxLength decreased, etc.)
226
+ if (
227
+ oldField.type === "text" &&
228
+ newField.type === "text" &&
229
+ oldField.options &&
230
+ newField.options
231
+ ) {
232
+ const violations: string[] = [];
233
+
234
+ // Check if minLength was increased
235
+ if (
236
+ newField.options.minLength !== undefined &&
237
+ (oldField.options.minLength === undefined ||
238
+ newField.options.minLength > oldField.options.minLength)
239
+ ) {
240
+ violations.push(
241
+ `minLength increased to ${newField.options.minLength}`
242
+ );
243
+ }
244
+
245
+ // Check if maxLength was decreased
246
+ if (
247
+ newField.options.maxLength !== undefined &&
248
+ oldField.options.maxLength !== undefined &&
249
+ newField.options.maxLength < oldField.options.maxLength
250
+ ) {
251
+ violations.push(
252
+ `maxLength decreased to ${newField.options.maxLength}`
253
+ );
254
+ }
255
+
256
+ if (violations.length > 0) {
257
+ const affectedCount = existingEntries.filter((entry) => {
258
+ const value = entry.data[newField.name];
259
+ if (typeof value !== "string") return false;
260
+
261
+ if (
262
+ newField.options?.minLength !== undefined &&
263
+ value.length < newField.options.minLength
264
+ ) {
265
+ return true;
266
+ }
267
+ if (
268
+ newField.options?.maxLength !== undefined &&
269
+ value.length > newField.options.maxLength
270
+ ) {
271
+ return true;
272
+ }
273
+ return false;
274
+ }).length;
275
+
276
+ if (affectedCount > 0) {
277
+ breakingChanges.push({
278
+ type: "VALIDATION_TIGHTENED",
279
+ fieldName: newField.name,
280
+ message: `Tightening validation for "${newField.name}" (${violations.join(", ")}) will invalidate ${affectedCount} entries`,
281
+ affectedEntriesCount: affectedCount,
282
+ });
283
+ }
284
+ }
285
+ }
286
+ }
287
+
288
+ return breakingChanges;
289
+ }
290
+
291
+ /**
292
+ * Validates field definitions for a content type.
293
+ * Checks for:
294
+ * - Unique field names
295
+ * - Valid field types
296
+ * - Required properties (name, label, type, required)
297
+ * - Valid field name format
298
+ * - Select/multiSelect fields have options defined
299
+ */
300
+ function validateFieldDefinitions(
301
+ fields: FieldDefinition[]
302
+ ): FieldValidationError[] {
303
+ const errors: FieldValidationError[] = [];
304
+ const seenNames = new Set<string>();
305
+
306
+ for (const field of fields) {
307
+ // Check for missing required properties
308
+ if (!field.name || typeof field.name !== "string") {
309
+ errors.push({
310
+ fieldName: field.name || "(unnamed)",
311
+ message: "Field must have a name property",
312
+ code: "MISSING_REQUIRED_PROPERTY",
313
+ });
314
+ continue;
315
+ }
316
+
317
+ if (!field.label || typeof field.label !== "string") {
318
+ errors.push({
319
+ fieldName: field.name,
320
+ message: `Field "${field.name}" must have a label property`,
321
+ code: "MISSING_REQUIRED_PROPERTY",
322
+ });
323
+ }
324
+
325
+ if (!field.type || typeof field.type !== "string") {
326
+ errors.push({
327
+ fieldName: field.name,
328
+ message: `Field "${field.name}" must have a type property`,
329
+ code: "MISSING_REQUIRED_PROPERTY",
330
+ });
331
+ }
332
+
333
+ if (typeof field.required !== "boolean") {
334
+ errors.push({
335
+ fieldName: field.name,
336
+ message: `Field "${field.name}" must have a required property (boolean)`,
337
+ code: "MISSING_REQUIRED_PROPERTY",
338
+ });
339
+ }
340
+
341
+ // Validate field name format
342
+ if (field.name && !isValidName(field.name)) {
343
+ errors.push({
344
+ fieldName: field.name,
345
+ message: `Field name "${field.name}" must start with a lowercase letter and contain only lowercase letters, numbers, and underscores (max 64 chars)`,
346
+ code: "INVALID_FIELD_NAME",
347
+ });
348
+ }
349
+
350
+ // Check for duplicate field names
351
+ if (seenNames.has(field.name)) {
352
+ errors.push({
353
+ fieldName: field.name,
354
+ message: `Duplicate field name: "${field.name}"`,
355
+ code: "DUPLICATE_FIELD_NAME",
356
+ });
357
+ }
358
+ seenNames.add(field.name);
359
+
360
+ // Validate field type is one of the supported types
361
+ if (field.type && !fieldTypes.includes(field.type as FieldType)) {
362
+ errors.push({
363
+ fieldName: field.name,
364
+ message: `Invalid field type "${field.type}". Must be one of: ${fieldTypes.join(", ")}`,
365
+ code: "INVALID_FIELD_TYPE",
366
+ });
367
+ }
368
+
369
+ // Validate select/multiSelect fields have options
370
+ if (
371
+ (field.type === "select" || field.type === "multiSelect") &&
372
+ (!field.options?.options || field.options.options.length === 0)
373
+ ) {
374
+ errors.push({
375
+ fieldName: field.name,
376
+ message: `${field.type} field "${field.name}" must have options defined`,
377
+ code: "INVALID_SELECT_OPTIONS",
378
+ });
379
+ }
380
+ }
381
+
382
+ return errors;
383
+ }
384
+
385
+ /**
386
+ * Mutation to create a new content type.
387
+ *
388
+ * Creates a content type definition with a unique name, display name, and
389
+ * field definitions. The content type can then be used to create content entries.
390
+ *
391
+ * @param name - Unique machine-readable name (e.g., "blog_post")
392
+ * @param displayName - Human-readable name (e.g., "Blog Post")
393
+ * @param description - Optional description of the content type
394
+ * @param fields - Array of field definitions
395
+ * @param icon - Optional icon identifier for UI
396
+ * @param singleton - If true, only one entry of this type can exist
397
+ * @param slugField - Field name to use for slug generation (defaults to first text field)
398
+ * @param titleField - Field name to use as display title (defaults to first text field)
399
+ * @param sortOrder - Custom sort order for admin UI
400
+ * @param createdBy - User ID who is creating this content type
401
+ *
402
+ * @returns The created content type document
403
+ *
404
+ * @throws Error if the name is not unique
405
+ * @throws Error if the name format is invalid
406
+ * @throws Error if field definitions are invalid
407
+ *
408
+ * @example
409
+ * ```typescript
410
+ * const blogPost = await ctx.runMutation(api.contentTypeMutations.createContentType, {
411
+ * name: "blog_post",
412
+ * displayName: "Blog Post",
413
+ * description: "Articles for the company blog",
414
+ * fields: [
415
+ * { name: "title", label: "Title", type: "text", required: true },
416
+ * { name: "content", label: "Content", type: "richText", required: true },
417
+ * { name: "published_date", label: "Published Date", type: "date", required: false },
418
+ * ],
419
+ * slugField: "title",
420
+ * titleField: "title",
421
+ * createdBy: currentUserId,
422
+ * });
423
+ * ```
424
+ */
425
+ export const createContentType = mutation({
426
+ args: {
427
+ ...createContentTypeArgs.fields,
428
+ /** Optional auth context for mutation-level authorization */
429
+ _auth: v.optional(mutationAuthContext),
430
+ },
431
+ returns: contentTypeDoc,
432
+ handler: async (ctx, args) => {
433
+ const {
434
+ name,
435
+ displayName,
436
+ description,
437
+ fields,
438
+ icon,
439
+ singleton,
440
+ slugField,
441
+ titleField,
442
+ sortOrder,
443
+ createdBy,
444
+ _auth,
445
+ } = args;
446
+
447
+ // Authorization check - contentTypes.create permission
448
+ requireMutationAuth(_auth, "contentTypes", "create");
449
+
450
+ // Validate content type name format
451
+ if (!isValidName(name)) {
452
+ throw contentTypeNameInvalid(name);
453
+ }
454
+
455
+ // Check if name is already taken (must be unique)
456
+ const existingType = await ctx.db
457
+ .query("contentTypes")
458
+ .withIndex("by_name", (q) => q.eq("name", name))
459
+ .first();
460
+
461
+ if (existingType) {
462
+ throw contentTypeNameDuplicate(name);
463
+ }
464
+
465
+ // Validate field definitions
466
+ const fieldErrors = validateFieldDefinitions(fields as FieldDefinition[]);
467
+ if (fieldErrors.length > 0) {
468
+ throw contentTypeFieldValidationFailed(fieldErrors);
469
+ }
470
+
471
+ // Validate slugField references an existing field if provided
472
+ const fieldNames = fields.map((f) => f.name);
473
+ if (slugField) {
474
+ const slugFieldExists = fields.some((f) => f.name === slugField);
475
+ if (!slugFieldExists) {
476
+ throw contentTypeSlugFieldInvalid(slugField, fieldNames);
477
+ }
478
+ }
479
+
480
+ // Validate titleField references an existing field if provided
481
+ if (titleField) {
482
+ const titleFieldExists = fields.some((f) => f.name === titleField);
483
+ if (!titleFieldExists) {
484
+ throw contentTypeTitleFieldInvalid(titleField, fieldNames);
485
+ }
486
+ }
487
+
488
+ // Insert the new content type
489
+ const id = await ctx.db.insert("contentTypes", {
490
+ name,
491
+ displayName,
492
+ description,
493
+ fields,
494
+ icon,
495
+ singleton,
496
+ slugField,
497
+ titleField,
498
+ sortOrder,
499
+ isActive: true,
500
+ createdBy,
501
+ });
502
+
503
+ // Retrieve and return the created document
504
+ const created = await ctx.db.get(id);
505
+ if (!created) {
506
+ throw internalError("Failed to retrieve created content type");
507
+ }
508
+
509
+ // Emit content type created event
510
+ await emitEvent(ctx, {
511
+ eventType: contentTypeEventType("created"),
512
+ resourceType: "contentType",
513
+ resourceId: id as unknown as string,
514
+ action: "created",
515
+ payload: {
516
+ name,
517
+ displayName,
518
+ fieldCount: fields.length,
519
+ isActive: true,
520
+ } as ContentTypeEventPayload,
521
+ userId: createdBy,
522
+ });
523
+
524
+ return created;
525
+ },
526
+ });
527
+
528
+ // =============================================================================
529
+ // Update Content Type Mutation
530
+ // =============================================================================
531
+
532
+ /**
533
+ * Validator for breaking change information returned by the mutation.
534
+ */
535
+ const breakingChangeValidator = v.object({
536
+ type: v.union(
537
+ v.literal("FIELD_REMOVED"),
538
+ v.literal("FIELD_TYPE_CHANGED"),
539
+ v.literal("FIELD_MADE_REQUIRED"),
540
+ v.literal("SELECT_OPTIONS_REMOVED"),
541
+ v.literal("REFERENCE_TYPES_RESTRICTED"),
542
+ v.literal("VALIDATION_TIGHTENED")
543
+ ),
544
+ fieldName: v.string(),
545
+ message: v.string(),
546
+ affectedEntriesCount: v.number(),
547
+ });
548
+
549
+ /**
550
+ * Extended return type that includes breaking change warnings.
551
+ */
552
+ const updateContentTypeResult = v.object({
553
+ ...contentTypeDoc.fields,
554
+ /** Breaking changes that were detected (only populated if force=true was used) */
555
+ breakingChanges: v.optional(v.array(breakingChangeValidator)),
556
+ });
557
+
558
+ /**
559
+ * Mutation to update an existing content type's fields and configuration.
560
+ *
561
+ * Includes validation to prevent breaking changes to fields with existing content.
562
+ * When breaking changes are detected and `force` is not set to true, the mutation
563
+ * will throw an error with details about the breaking changes.
564
+ *
565
+ * **Breaking Change Detection:**
566
+ * - Removing fields that have data in existing entries
567
+ * - Changing field types (e.g., text → number)
568
+ * - Making optional fields required when entries have empty values
569
+ * - Removing select/multiSelect options that are in use
570
+ * - Restricting allowed reference content types
571
+ * - Tightening validation rules (increased minLength, decreased maxLength)
572
+ *
573
+ * @param id - The content type ID to update
574
+ * @param displayName - Optional new display name
575
+ * @param description - Optional new description
576
+ * @param fields - Optional new field definitions (replaces all existing fields)
577
+ * @param icon - Optional new icon
578
+ * @param singleton - Optional singleton flag
579
+ * @param slugField - Optional field name for slug generation
580
+ * @param titleField - Optional field name for display title
581
+ * @param sortOrder - Optional new sort order
582
+ * @param isActive - Optional active status
583
+ * @param updatedBy - User ID making the update (for audit trail)
584
+ * @param force - If true, allow breaking changes (default: false)
585
+ *
586
+ * @returns The updated content type, with breakingChanges if force was used
587
+ *
588
+ * @throws Error if the content type does not exist
589
+ * @throws Error if breaking changes are detected and force is not true
590
+ * @throws Error if field definitions are invalid
591
+ *
592
+ * @example
593
+ * ```typescript
594
+ * // Simple update (no breaking changes)
595
+ * const updated = await ctx.runMutation(api.contentTypeMutations.updateContentType, {
596
+ * id: contentTypeId,
597
+ * displayName: "Updated Blog Post",
598
+ * description: "New description",
599
+ * updatedBy: currentUserId,
600
+ * });
601
+ *
602
+ * // Update fields (will check for breaking changes)
603
+ * const updated = await ctx.runMutation(api.contentTypeMutations.updateContentType, {
604
+ * id: contentTypeId,
605
+ * fields: [
606
+ * { name: "title", label: "Title", type: "text", required: true },
607
+ * { name: "content", label: "Content", type: "richText", required: true },
608
+ * { name: "author", label: "Author", type: "text", required: false }, // New field
609
+ * ],
610
+ * updatedBy: currentUserId,
611
+ * });
612
+ *
613
+ * // Force update with breaking changes
614
+ * const updated = await ctx.runMutation(api.contentTypeMutations.updateContentType, {
615
+ * id: contentTypeId,
616
+ * fields: newFields,
617
+ * force: true, // Acknowledge potential data loss
618
+ * updatedBy: currentUserId,
619
+ * });
620
+ * ```
621
+ */
622
+ export const updateContentType = mutation({
623
+ args: {
624
+ ...updateContentTypeArgs.fields,
625
+ /** If true, allow breaking changes that may affect existing content entries */
626
+ force: v.optional(v.boolean()),
627
+ /** Optional auth context for mutation-level authorization */
628
+ _auth: v.optional(mutationAuthContext),
629
+ },
630
+ returns: updateContentTypeResult,
631
+ handler: async (ctx, args) => {
632
+ const {
633
+ id,
634
+ displayName,
635
+ description,
636
+ fields,
637
+ icon,
638
+ singleton,
639
+ slugField,
640
+ titleField,
641
+ sortOrder,
642
+ isActive,
643
+ updatedBy,
644
+ force = false,
645
+ _auth,
646
+ } = args;
647
+
648
+ // Authorization check - contentTypes.update permission
649
+ requireMutationAuth(_auth, "contentTypes", "update");
650
+
651
+ const existingType = await ctx.db.get(id);
652
+ if (!existingType) {
653
+ throw contentTypeNotFound(id as unknown as string);
654
+ }
655
+ if (isDeleted(existingType)) {
656
+ throw contentTypeDeleted(id as unknown as string, existingType.name);
657
+ }
658
+
659
+ // Build the update object with only provided fields
660
+ const updates: Record<string, unknown> = {
661
+ updatedBy,
662
+ };
663
+
664
+ // Handle simple field updates
665
+ if (displayName !== undefined) {
666
+ updates.displayName = displayName;
667
+ }
668
+ if (description !== undefined) {
669
+ updates.description = description;
670
+ }
671
+ if (icon !== undefined) {
672
+ updates.icon = icon;
673
+ }
674
+ if (singleton !== undefined) {
675
+ updates.singleton = singleton;
676
+ }
677
+ if (sortOrder !== undefined) {
678
+ updates.sortOrder = sortOrder;
679
+ }
680
+ if (isActive !== undefined) {
681
+ updates.isActive = isActive;
682
+ }
683
+
684
+ // Track breaking changes if fields are being updated
685
+ let detectedBreakingChanges: BreakingChange[] = [];
686
+
687
+ // Handle field updates with breaking change detection
688
+ if (fields !== undefined) {
689
+ // Validate the new field definitions
690
+ const fieldErrors = validateFieldDefinitions(fields as FieldDefinition[]);
691
+ if (fieldErrors.length > 0) {
692
+ throw contentTypeFieldValidationFailed(fieldErrors);
693
+ }
694
+
695
+ // Get all existing content entries for this content type
696
+ const existingEntries = await ctx.db
697
+ .query("contentEntries")
698
+ .withIndex("by_content_type", (q) => q.eq("contentTypeId", id))
699
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
700
+ .collect();
701
+
702
+ // Only check for breaking changes if there are existing entries
703
+ if (existingEntries.length > 0) {
704
+ detectedBreakingChanges = detectBreakingChanges(
705
+ existingType.fields as FieldDefinition[],
706
+ fields as FieldDefinition[],
707
+ existingEntries.map((e) => ({ data: e.data as Record<string, unknown> }))
708
+ );
709
+
710
+ // If breaking changes detected and force is not true, throw error
711
+ if (detectedBreakingChanges.length > 0 && !force) {
712
+ throw contentTypeBreakingChange(detectedBreakingChanges);
713
+ }
714
+ }
715
+
716
+ updates.fields = fields;
717
+ }
718
+
719
+ // Validate slugField references an existing field if provided
720
+ const effectiveFields = (fields ?? existingType.fields) as FieldDefinition[];
721
+ const effectiveSlugField = slugField !== undefined ? slugField : existingType.slugField;
722
+ const effectiveTitleField = titleField !== undefined ? titleField : existingType.titleField;
723
+
724
+ const availableFieldNames = effectiveFields.map((f) => f.name);
725
+
726
+ if (effectiveSlugField) {
727
+ const slugFieldExists = effectiveFields.some((f) => f.name === effectiveSlugField);
728
+ if (!slugFieldExists) {
729
+ throw contentTypeSlugFieldInvalid(effectiveSlugField, availableFieldNames);
730
+ }
731
+ if (slugField !== undefined) {
732
+ updates.slugField = slugField;
733
+ }
734
+ } else if (slugField !== undefined) {
735
+ updates.slugField = slugField;
736
+ }
737
+
738
+ if (effectiveTitleField) {
739
+ const titleFieldExists = effectiveFields.some((f) => f.name === effectiveTitleField);
740
+ if (!titleFieldExists) {
741
+ throw contentTypeTitleFieldInvalid(effectiveTitleField, availableFieldNames);
742
+ }
743
+ if (titleField !== undefined) {
744
+ updates.titleField = titleField;
745
+ }
746
+ } else if (titleField !== undefined) {
747
+ updates.titleField = titleField;
748
+ }
749
+
750
+ // Apply the updates
751
+ await ctx.db.patch(id, updates);
752
+
753
+ // Retrieve and return the updated document
754
+ const updated = await ctx.db.get(id);
755
+ if (!updated) {
756
+ throw internalError("Failed to retrieve updated content type");
757
+ }
758
+
759
+ // Emit content type updated event
760
+ const changedFields = Object.keys(updates).filter((k) => k !== "updatedBy");
761
+ await emitEvent(ctx, {
762
+ eventType: contentTypeEventType("updated"),
763
+ resourceType: "contentType",
764
+ resourceId: id as unknown as string,
765
+ action: "updated",
766
+ payload: {
767
+ name: updated.name,
768
+ displayName: updated.displayName,
769
+ fieldCount: updated.fields.length,
770
+ isActive: updated.isActive,
771
+ changedFields,
772
+ } as ContentTypeEventPayload,
773
+ userId: updatedBy,
774
+ });
775
+
776
+ // Include breaking changes in the result if force was used
777
+ return {
778
+ ...updated,
779
+ breakingChanges:
780
+ detectedBreakingChanges.length > 0 ? detectedBreakingChanges : undefined,
781
+ };
782
+ },
783
+ });
784
+
785
+ // =============================================================================
786
+ // Delete Content Type Mutation
787
+ // =============================================================================
788
+
789
+ /**
790
+ * Result type for the delete content type mutation.
791
+ * Includes information about any cascade-deleted entries.
792
+ */
793
+ const deleteContentTypeResult = v.object({
794
+ /** Whether the deletion was successful */
795
+ success: v.boolean(),
796
+ /** The ID of the deleted content type */
797
+ deletedId: v.id("contentTypes"),
798
+ /** Number of content entries that were deleted (when cascade=true) */
799
+ deletedEntriesCount: v.number(),
800
+ /** Number of content versions that were deleted (when cascade=true and hardDelete=true) */
801
+ deletedVersionsCount: v.number(),
802
+ /** Whether this was a hard delete (permanent) or soft delete */
803
+ wasHardDelete: v.boolean(),
804
+ });
805
+
806
+ /**
807
+ * Mutation to delete a content type.
808
+ *
809
+ * Supports two deletion strategies via the `cascade` flag:
810
+ * 1. **Cascade delete** (`cascade: true`): Deletes all content entries of this type
811
+ * before deleting the content type itself.
812
+ * 2. **Prevent if entries exist** (`cascade: false` or not specified): Fails the
813
+ * deletion if any content entries exist for this type.
814
+ *
815
+ * Also supports two deletion modes via the `hardDelete` flag:
816
+ * - **Soft delete** (default): Sets `deletedAt` timestamp, entries remain in database
817
+ * - **Hard delete** (`hardDelete: true`): Permanently removes from database
818
+ *
819
+ * @param id - The content type ID to delete
820
+ * @param cascade - If true, delete all entries of this type first. Default: false
821
+ * @param hardDelete - If true, permanently delete. Default: false (soft delete)
822
+ * @param deletedBy - User ID performing the deletion (for audit trail)
823
+ *
824
+ * @returns Object with deletion results including counts of deleted entries/versions
825
+ *
826
+ * @throws Error if content type does not exist
827
+ * @throws Error if content type is already deleted (soft deleted)
828
+ * @throws Error if cascade is false and content entries exist
829
+ *
830
+ * @example
831
+ * ```typescript
832
+ * // Soft delete - fails if entries exist
833
+ * const result = await ctx.runMutation(api.contentTypeMutations.deleteContentType, {
834
+ * id: contentTypeId,
835
+ * deletedBy: currentUserId,
836
+ * });
837
+ *
838
+ * // Cascade soft delete - deletes all entries too
839
+ * const result = await ctx.runMutation(api.contentTypeMutations.deleteContentType, {
840
+ * id: contentTypeId,
841
+ * cascade: true,
842
+ * deletedBy: currentUserId,
843
+ * });
844
+ *
845
+ * // Hard delete with cascade - permanently removes everything
846
+ * const result = await ctx.runMutation(api.contentTypeMutations.deleteContentType, {
847
+ * id: contentTypeId,
848
+ * cascade: true,
849
+ * hardDelete: true,
850
+ * deletedBy: currentUserId,
851
+ * });
852
+ * ```
853
+ */
854
+ export const deleteContentType = mutation({
855
+ args: {
856
+ ...deleteContentTypeArgs.fields,
857
+ /** Optional auth context for mutation-level authorization */
858
+ _auth: v.optional(mutationAuthContext),
859
+ },
860
+ returns: deleteContentTypeResult,
861
+ handler: async (ctx, args) => {
862
+ const { id, cascade = false, hardDelete = false, deletedBy, _auth } = args;
863
+
864
+ // Authorization check - contentTypes.delete permission
865
+ requireMutationAuth(_auth, "contentTypes", "delete");
866
+
867
+ const contentType = await ctx.db.get(id);
868
+ if (!contentType) {
869
+ throw contentTypeNotFound(id as unknown as string);
870
+ }
871
+
872
+ // Check if already soft-deleted
873
+ if (isDeleted(contentType)) {
874
+ throw contentTypeDeleted(id as unknown as string, contentType.name);
875
+ }
876
+
877
+ // Get all content entries for this type (excluding already soft-deleted ones)
878
+ const existingEntries = await ctx.db
879
+ .query("contentEntries")
880
+ .withIndex("by_content_type", (q) => q.eq("contentTypeId", id))
881
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
882
+ .collect();
883
+
884
+ const entryCount = existingEntries.length;
885
+
886
+ // If entries exist and cascade is false, prevent deletion
887
+ if (entryCount > 0 && !cascade) {
888
+ throw contentTypeHasEntries(id as unknown as string, contentType.name, entryCount);
889
+ }
890
+
891
+ let deletedEntriesCount = 0;
892
+ let deletedVersionsCount = 0;
893
+ const now = Date.now();
894
+
895
+ // If cascade is true, delete all entries first
896
+ if (cascade && entryCount > 0) {
897
+ if (hardDelete) {
898
+ // Hard delete: permanently remove entries and their versions
899
+ for (const entry of existingEntries) {
900
+ // Delete all versions for this entry
901
+ const versions = await ctx.db
902
+ .query("contentVersions")
903
+ .withIndex("by_entry", (q) => q.eq("entryId", entry._id))
904
+ .collect();
905
+
906
+ for (const version of versions) {
907
+ await ctx.db.delete(version._id);
908
+ deletedVersionsCount++;
909
+ }
910
+
911
+ // Delete the entry
912
+ await ctx.db.delete(entry._id);
913
+ deletedEntriesCount++;
914
+ }
915
+ } else {
916
+ // Soft delete: set deletedAt on all entries
917
+ for (const entry of existingEntries) {
918
+ await ctx.db.patch(entry._id, {
919
+ deletedAt: now,
920
+ updatedBy: deletedBy,
921
+ });
922
+ deletedEntriesCount++;
923
+ }
924
+ }
925
+ }
926
+
927
+ // Delete the content type itself
928
+ if (hardDelete) {
929
+ // Hard delete: permanently remove
930
+ await ctx.db.delete(id);
931
+ } else {
932
+ // Soft delete: set deletedAt
933
+ await ctx.db.patch(id, {
934
+ deletedAt: now,
935
+ isActive: false,
936
+ updatedBy: deletedBy,
937
+ });
938
+ }
939
+
940
+ // Emit content type deleted event
941
+ await emitEvent(ctx, {
942
+ eventType: contentTypeEventType("deleted"),
943
+ resourceType: "contentType",
944
+ resourceId: id as unknown as string,
945
+ action: "deleted",
946
+ payload: {
947
+ name: contentType.name,
948
+ displayName: contentType.displayName,
949
+ fieldCount: contentType.fields.length,
950
+ isActive: false,
951
+ } as ContentTypeEventPayload,
952
+ userId: deletedBy,
953
+ metadata: {
954
+ hardDelete,
955
+ cascade,
956
+ deletedEntriesCount,
957
+ deletedVersionsCount,
958
+ },
959
+ });
960
+
961
+ return {
962
+ success: true,
963
+ deletedId: id,
964
+ deletedEntriesCount,
965
+ deletedVersionsCount,
966
+ wasHardDelete: hardDelete,
967
+ };
968
+ },
969
+ });