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,1001 @@
1
+ /**
2
+ * Taxonomy Mutation Functions
3
+ *
4
+ * Provides mutation functions for managing taxonomies and terms.
5
+ *
6
+ * Available mutations:
7
+ * - Taxonomies: create, update, delete (soft), restore
8
+ * - Terms: create, update, delete (soft), restore, reorder
9
+ * - Entry Tags: setEntryTerms, addTermToEntry, removeTermFromEntry
10
+ */
11
+ import { v } from "convex/values";
12
+ import { isDeleted } from "./lib/softDelete.js";
13
+ import { mutation } from "./_generated/server.js";
14
+ import { generateSlug } from "./lib/slugGenerator.js";
15
+ // =============================================================================
16
+ // Helper Functions
17
+ // =============================================================================
18
+ /**
19
+ * Build the full path for a term in a hierarchy.
20
+ */
21
+ async function buildTermPath(ctx, parentId, slug) {
22
+ if (!parentId) {
23
+ return `/${slug}`;
24
+ }
25
+ const parent = await ctx.db.get(parentId);
26
+ if (!parent || isDeleted(parent)) {
27
+ return `/${slug}`;
28
+ }
29
+ const parentPath = parent.path ?? `/${parent.slug}`;
30
+ return `${parentPath}/${slug}`;
31
+ }
32
+ /**
33
+ * Calculate depth based on parent.
34
+ */
35
+ async function calculateDepth(ctx, parentId) {
36
+ if (!parentId) {
37
+ return 0;
38
+ }
39
+ const parent = await ctx.db.get(parentId);
40
+ if (!parent || isDeleted(parent)) {
41
+ return 0;
42
+ }
43
+ return parent.depth + 1;
44
+ }
45
+ /**
46
+ * Update all descendant paths when a term is moved.
47
+ */
48
+ async function updateDescendantPaths(ctx, termId, oldPath, newPath) {
49
+ // Get all terms that start with the old path
50
+ const descendants = await ctx.db
51
+ .query("taxonomyTerms")
52
+ .filter((q) => q.gte(q.field("path"), oldPath))
53
+ .collect();
54
+ for (const desc of descendants) {
55
+ if (desc._id !== termId && desc.path?.startsWith(oldPath + "/")) {
56
+ const updatedPath = desc.path.replace(oldPath, newPath);
57
+ const updatedDepth = updatedPath.split("/").filter((p) => p).length - 1;
58
+ await ctx.db.patch(desc._id, {
59
+ path: updatedPath,
60
+ depth: updatedDepth,
61
+ });
62
+ }
63
+ }
64
+ }
65
+ // =============================================================================
66
+ // Taxonomy Mutations
67
+ // =============================================================================
68
+ /**
69
+ * Create a new taxonomy.
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * const taxonomyId = await ctx.runMutation(api.taxonomyMutations.createTaxonomy, {
74
+ * name: "tags",
75
+ * displayName: "Tags",
76
+ * isHierarchical: false,
77
+ * allowInlineCreation: true,
78
+ * });
79
+ * ```
80
+ */
81
+ export const createTaxonomy = mutation({
82
+ args: {
83
+ name: v.string(),
84
+ displayName: v.string(),
85
+ description: v.optional(v.string()),
86
+ isHierarchical: v.boolean(),
87
+ allowInlineCreation: v.boolean(),
88
+ icon: v.optional(v.string()),
89
+ sortOrder: v.optional(v.number()),
90
+ userId: v.optional(v.string()),
91
+ },
92
+ returns: v.id("taxonomies"),
93
+ handler: async (ctx, args) => {
94
+ const { name, displayName, description, isHierarchical, allowInlineCreation, icon, sortOrder, userId, } = args;
95
+ // Check for duplicate name
96
+ const existing = await ctx.db
97
+ .query("taxonomies")
98
+ .withIndex("by_name", (q) => q.eq("name", name))
99
+ .first();
100
+ if (existing && !isDeleted(existing)) {
101
+ throw new Error(`Taxonomy with name "${name}" already exists`);
102
+ }
103
+ // If there was a soft-deleted taxonomy with this name, restore and update it
104
+ if (existing) {
105
+ await ctx.db.patch(existing._id, {
106
+ displayName,
107
+ description,
108
+ isHierarchical,
109
+ allowInlineCreation,
110
+ icon,
111
+ sortOrder,
112
+ isActive: true,
113
+ deletedAt: undefined,
114
+ updatedBy: userId,
115
+ });
116
+ return existing._id;
117
+ }
118
+ const taxonomyId = await ctx.db.insert("taxonomies", {
119
+ name,
120
+ displayName,
121
+ description,
122
+ isHierarchical,
123
+ allowInlineCreation,
124
+ icon,
125
+ sortOrder,
126
+ isActive: true,
127
+ createdBy: userId,
128
+ });
129
+ return taxonomyId;
130
+ },
131
+ });
132
+ /**
133
+ * Update an existing taxonomy.
134
+ */
135
+ export const updateTaxonomy = mutation({
136
+ args: {
137
+ id: v.id("taxonomies"),
138
+ displayName: v.optional(v.string()),
139
+ description: v.optional(v.string()),
140
+ allowInlineCreation: v.optional(v.boolean()),
141
+ icon: v.optional(v.string()),
142
+ sortOrder: v.optional(v.number()),
143
+ isActive: v.optional(v.boolean()),
144
+ userId: v.optional(v.string()),
145
+ },
146
+ returns: v.id("taxonomies"),
147
+ handler: async (ctx, args) => {
148
+ const { id, userId, ...updates } = args;
149
+ const taxonomy = await ctx.db.get(id);
150
+ if (!taxonomy) {
151
+ throw new Error("Taxonomy not found");
152
+ }
153
+ if (isDeleted(taxonomy)) {
154
+ throw new Error("Cannot update deleted taxonomy");
155
+ }
156
+ // Build update object
157
+ const updateFields = { updatedBy: userId };
158
+ if (updates.displayName !== undefined)
159
+ updateFields.displayName = updates.displayName;
160
+ if (updates.description !== undefined)
161
+ updateFields.description = updates.description;
162
+ if (updates.allowInlineCreation !== undefined)
163
+ updateFields.allowInlineCreation = updates.allowInlineCreation;
164
+ if (updates.icon !== undefined)
165
+ updateFields.icon = updates.icon;
166
+ if (updates.sortOrder !== undefined)
167
+ updateFields.sortOrder = updates.sortOrder;
168
+ if (updates.isActive !== undefined)
169
+ updateFields.isActive = updates.isActive;
170
+ await ctx.db.patch(id, updateFields);
171
+ return id;
172
+ },
173
+ });
174
+ /**
175
+ * Soft delete a taxonomy.
176
+ */
177
+ export const deleteTaxonomy = mutation({
178
+ args: {
179
+ id: v.id("taxonomies"),
180
+ userId: v.optional(v.string()),
181
+ },
182
+ returns: v.null(),
183
+ handler: async (ctx, args) => {
184
+ const { id, userId } = args;
185
+ const taxonomy = await ctx.db.get(id);
186
+ if (!taxonomy) {
187
+ throw new Error("Taxonomy not found");
188
+ }
189
+ if (isDeleted(taxonomy)) {
190
+ return null; // Already deleted
191
+ }
192
+ // Soft delete the taxonomy
193
+ await ctx.db.patch(id, {
194
+ deletedAt: Date.now(),
195
+ isActive: false,
196
+ updatedBy: userId,
197
+ });
198
+ // Also soft delete all terms in this taxonomy
199
+ const terms = await ctx.db
200
+ .query("taxonomyTerms")
201
+ .withIndex("by_taxonomy", (q) => q.eq("taxonomyId", id))
202
+ .collect();
203
+ for (const term of terms) {
204
+ if (!isDeleted(term)) {
205
+ await ctx.db.patch(term._id, {
206
+ deletedAt: Date.now(),
207
+ updatedBy: userId,
208
+ });
209
+ }
210
+ }
211
+ return null;
212
+ },
213
+ });
214
+ /**
215
+ * Restore a soft-deleted taxonomy.
216
+ */
217
+ export const restoreTaxonomy = mutation({
218
+ args: {
219
+ id: v.id("taxonomies"),
220
+ userId: v.optional(v.string()),
221
+ },
222
+ returns: v.id("taxonomies"),
223
+ handler: async (ctx, args) => {
224
+ const { id, userId } = args;
225
+ const taxonomy = await ctx.db.get(id);
226
+ if (!taxonomy) {
227
+ throw new Error("Taxonomy not found");
228
+ }
229
+ if (!isDeleted(taxonomy)) {
230
+ return id; // Not deleted
231
+ }
232
+ await ctx.db.patch(id, {
233
+ deletedAt: undefined,
234
+ isActive: true,
235
+ updatedBy: userId,
236
+ });
237
+ return id;
238
+ },
239
+ });
240
+ // =============================================================================
241
+ // Term Mutations
242
+ // =============================================================================
243
+ /**
244
+ * Create a new taxonomy term.
245
+ *
246
+ * @example
247
+ * ```typescript
248
+ * // Create a flat tag
249
+ * const tagId = await ctx.runMutation(api.taxonomyMutations.createTerm, {
250
+ * taxonomyId: tagsTaxonomyId,
251
+ * name: "JavaScript",
252
+ * });
253
+ *
254
+ * // Create a hierarchical category
255
+ * const categoryId = await ctx.runMutation(api.taxonomyMutations.createTerm, {
256
+ * taxonomyId: categoriesTaxonomyId,
257
+ * name: "Web Development",
258
+ * parentId: techCategoryId,
259
+ * });
260
+ * ```
261
+ */
262
+ export const createTerm = mutation({
263
+ args: {
264
+ taxonomyId: v.id("taxonomies"),
265
+ name: v.string(),
266
+ slug: v.optional(v.string()),
267
+ description: v.optional(v.string()),
268
+ parentId: v.optional(v.id("taxonomyTerms")),
269
+ color: v.optional(v.string()),
270
+ icon: v.optional(v.string()),
271
+ sortOrder: v.optional(v.number()),
272
+ userId: v.optional(v.string()),
273
+ },
274
+ returns: v.id("taxonomyTerms"),
275
+ handler: async (ctx, args) => {
276
+ const { taxonomyId, name, description, parentId, color, icon, sortOrder, userId, } = args;
277
+ // Verify taxonomy exists
278
+ const taxonomy = await ctx.db.get(taxonomyId);
279
+ if (!taxonomy || isDeleted(taxonomy)) {
280
+ throw new Error("Taxonomy not found");
281
+ }
282
+ // Check if hierarchy is allowed
283
+ if (parentId && !taxonomy.isHierarchical) {
284
+ throw new Error("Cannot create nested terms in a flat taxonomy");
285
+ }
286
+ // Verify parent exists if specified
287
+ if (parentId) {
288
+ const parent = await ctx.db.get(parentId);
289
+ if (!parent || isDeleted(parent)) {
290
+ throw new Error("Parent term not found");
291
+ }
292
+ if (parent.taxonomyId !== taxonomyId) {
293
+ throw new Error("Parent term belongs to a different taxonomy");
294
+ }
295
+ }
296
+ // Generate or validate slug
297
+ const slug = args.slug || generateSlug(name);
298
+ // Check for duplicate slug in taxonomy
299
+ const existing = await ctx.db
300
+ .query("taxonomyTerms")
301
+ .withIndex("by_taxonomy_and_slug", (q) => q.eq("taxonomyId", taxonomyId).eq("slug", slug))
302
+ .first();
303
+ if (existing && !isDeleted(existing)) {
304
+ throw new Error(`Term with slug "${slug}" already exists in this taxonomy`);
305
+ }
306
+ // Calculate path and depth
307
+ const path = await buildTermPath(ctx, parentId, slug);
308
+ const depth = await calculateDepth(ctx, parentId);
309
+ // Build searchText for search index
310
+ const searchText = [name, description].filter(Boolean).join(" ");
311
+ const termId = await ctx.db.insert("taxonomyTerms", {
312
+ taxonomyId,
313
+ slug,
314
+ name,
315
+ description,
316
+ parentId,
317
+ path,
318
+ depth,
319
+ color,
320
+ icon,
321
+ sortOrder,
322
+ usageCount: 0,
323
+ searchText,
324
+ createdBy: userId,
325
+ });
326
+ return termId;
327
+ },
328
+ });
329
+ /**
330
+ * Update an existing term.
331
+ */
332
+ export const updateTerm = mutation({
333
+ args: {
334
+ id: v.id("taxonomyTerms"),
335
+ name: v.optional(v.string()),
336
+ slug: v.optional(v.string()),
337
+ description: v.optional(v.string()),
338
+ parentId: v.optional(v.union(v.id("taxonomyTerms"), v.null())),
339
+ color: v.optional(v.string()),
340
+ icon: v.optional(v.string()),
341
+ sortOrder: v.optional(v.number()),
342
+ userId: v.optional(v.string()),
343
+ },
344
+ returns: v.id("taxonomyTerms"),
345
+ handler: async (ctx, args) => {
346
+ const { id, userId, ...updates } = args;
347
+ const term = await ctx.db.get(id);
348
+ if (!term) {
349
+ throw new Error("Term not found");
350
+ }
351
+ if (isDeleted(term)) {
352
+ throw new Error("Cannot update deleted term");
353
+ }
354
+ const taxonomy = await ctx.db.get(term.taxonomyId);
355
+ if (!taxonomy || isDeleted(taxonomy)) {
356
+ throw new Error("Taxonomy not found");
357
+ }
358
+ // Build update object
359
+ const updateFields = { updatedBy: userId };
360
+ if (updates.name !== undefined) {
361
+ updateFields.name = updates.name;
362
+ // Update searchText
363
+ updateFields.searchText = [
364
+ updates.name,
365
+ updates.description ?? term.description,
366
+ ]
367
+ .filter(Boolean)
368
+ .join(" ");
369
+ }
370
+ if (updates.description !== undefined) {
371
+ updateFields.description = updates.description;
372
+ if (!updates.name) {
373
+ updateFields.searchText = [term.name, updates.description]
374
+ .filter(Boolean)
375
+ .join(" ");
376
+ }
377
+ }
378
+ if (updates.color !== undefined)
379
+ updateFields.color = updates.color;
380
+ if (updates.icon !== undefined)
381
+ updateFields.icon = updates.icon;
382
+ if (updates.sortOrder !== undefined)
383
+ updateFields.sortOrder = updates.sortOrder;
384
+ // Handle slug change
385
+ if (updates.slug !== undefined && updates.slug !== term.slug) {
386
+ // Check for duplicate
387
+ const existing = await ctx.db
388
+ .query("taxonomyTerms")
389
+ .withIndex("by_taxonomy_and_slug", (q) => q.eq("taxonomyId", term.taxonomyId).eq("slug", updates.slug))
390
+ .first();
391
+ if (existing && existing._id !== id && !isDeleted(existing)) {
392
+ throw new Error(`Term with slug "${updates.slug}" already exists`);
393
+ }
394
+ updateFields.slug = updates.slug;
395
+ // Update path for this term and descendants
396
+ const oldPath = term.path ?? `/${term.slug}`;
397
+ const newPath = await buildTermPath(ctx, term.parentId, updates.slug);
398
+ updateFields.path = newPath;
399
+ // Update descendants if this term h
400
+ await updateDescendantPaths(ctx, id, oldPath, newPath);
401
+ }
402
+ // Handle parent change (moving in hierarchy)
403
+ if (updates.parentId !== undefined) {
404
+ const newParentId = updates.parentId === null ? undefined : updates.parentId;
405
+ if (!taxonomy.isHierarchical && newParentId) {
406
+ throw new Error("Cannot create nested terms in a flat taxonomy");
407
+ }
408
+ // Verify new parent if specified
409
+ if (newParentId) {
410
+ const newParent = await ctx.db.get(newParentId);
411
+ if (!newParent || isDeleted(newParent)) {
412
+ throw new Error("New parent term not found");
413
+ }
414
+ if (newParent.taxonomyId !== term.taxonomyId) {
415
+ throw new Error("New parent belongs to a different taxonomy");
416
+ }
417
+ // Check for circular reference
418
+ let current = newParent;
419
+ while (current) {
420
+ if (current._id === id) {
421
+ throw new Error("Cannot move term under its own descendant");
422
+ }
423
+ if (current.parentId) {
424
+ current = await ctx.db.get(current.parentId);
425
+ }
426
+ else {
427
+ break;
428
+ }
429
+ }
430
+ }
431
+ updateFields.parentId = newParentId;
432
+ updateFields.depth = await calculateDepth(ctx, newParentId);
433
+ // Update path
434
+ const slug = updates.slug ?? term.slug;
435
+ const oldPath = term.path ?? `/${term.slug}`;
436
+ const newPath = await buildTermPath(ctx, newParentId, slug);
437
+ updateFields.path = newPath;
438
+ // Update descendants
439
+ await updateDescendantPaths(ctx, id, oldPath, newPath);
440
+ }
441
+ await ctx.db.patch(id, updateFields);
442
+ return id;
443
+ },
444
+ });
445
+ /**
446
+ * Soft delete a term.
447
+ */
448
+ export const deleteTerm = mutation({
449
+ args: {
450
+ id: v.id("taxonomyTerms"),
451
+ cascade: v.optional(v.boolean()),
452
+ userId: v.optional(v.string()),
453
+ },
454
+ returns: v.null(),
455
+ handler: async (ctx, args) => {
456
+ const { id, cascade = true, userId } = args;
457
+ const term = await ctx.db.get(id);
458
+ if (!term) {
459
+ throw new Error("Term not found");
460
+ }
461
+ if (isDeleted(term)) {
462
+ return null;
463
+ }
464
+ // Check for children
465
+ const children = await ctx.db
466
+ .query("taxonomyTerms")
467
+ .withIndex("by_parent", (q) => q.eq("parentId", id))
468
+ .collect();
469
+ const activeChildren = children.filter((c) => !isDeleted(c));
470
+ if (activeChildren.length > 0 && !cascade) {
471
+ throw new Error("Cannot delete term with children. Use cascade=true or delete children first.");
472
+ }
473
+ // Delete this term
474
+ await ctx.db.patch(id, {
475
+ deletedAt: Date.now(),
476
+ updatedBy: userId,
477
+ });
478
+ // Also delete children if cascading
479
+ if (cascade) {
480
+ for (const child of activeChildren) {
481
+ await ctx.db.patch(child._id, {
482
+ deletedAt: Date.now(),
483
+ updatedBy: userId,
484
+ });
485
+ }
486
+ }
487
+ // Remove entry tag associations
488
+ const associations = await ctx.db
489
+ .query("contentEntryTags")
490
+ .withIndex("by_term", (q) => q.eq("termId", id))
491
+ .collect();
492
+ for (const assoc of associations) {
493
+ await ctx.db.delete(assoc._id);
494
+ }
495
+ // Remove media asset tag associations
496
+ const mediaAssociations = await ctx.db
497
+ .query("mediaAssetTags")
498
+ .withIndex("by_term", (q) => q.eq("termId", id))
499
+ .collect();
500
+ for (const assoc of mediaAssociations) {
501
+ await ctx.db.delete(assoc._id);
502
+ }
503
+ return null;
504
+ },
505
+ });
506
+ /**
507
+ * Restore a soft-deleted term.
508
+ */
509
+ export const restoreTerm = mutation({
510
+ args: {
511
+ id: v.id("taxonomyTerms"),
512
+ userId: v.optional(v.string()),
513
+ },
514
+ returns: v.id("taxonomyTerms"),
515
+ handler: async (ctx, args) => {
516
+ const { id, userId } = args;
517
+ const term = await ctx.db.get(id);
518
+ if (!term) {
519
+ throw new Error("Term not found");
520
+ }
521
+ if (!isDeleted(term)) {
522
+ return id;
523
+ }
524
+ // Make sure parent exists if there is one
525
+ if (term.parentId) {
526
+ const parent = await ctx.db.get(term.parentId);
527
+ if (!parent || isDeleted(parent)) {
528
+ // Restore as root term
529
+ await ctx.db.patch(id, {
530
+ deletedAt: undefined,
531
+ parentId: undefined,
532
+ path: `/${term.slug}`,
533
+ depth: 0,
534
+ updatedBy: userId,
535
+ });
536
+ return id;
537
+ }
538
+ }
539
+ await ctx.db.patch(id, {
540
+ deletedAt: undefined,
541
+ updatedBy: userId,
542
+ });
543
+ return id;
544
+ },
545
+ });
546
+ // =============================================================================
547
+ // Entry Tag Mutations
548
+ // =============================================================================
549
+ /**
550
+ * Set the terms for an entry field (replaces all existing terms).
551
+ *
552
+ * @example
553
+ * ```typescript
554
+ * await ctx.runMutation(api.taxonomyMutations.setEntryTerms, {
555
+ * entryId: blogPostId,
556
+ * fieldName: "tags",
557
+ * termIds: [javascriptTagId, reactTagId, typescriptTagId],
558
+ * });
559
+ * ```
560
+ */
561
+ export const setEntryTerms = mutation({
562
+ args: {
563
+ entryId: v.id("contentEntries"),
564
+ fieldName: v.string(),
565
+ termIds: v.array(v.id("taxonomyTerms")),
566
+ },
567
+ returns: v.null(),
568
+ handler: async (ctx, args) => {
569
+ const { entryId, fieldName, termIds } = args;
570
+ // Verify entry exists
571
+ const entry = await ctx.db.get(entryId);
572
+ if (!entry || isDeleted(entry)) {
573
+ throw new Error("Content entry not found");
574
+ }
575
+ // Get existing associations for this field
576
+ const existing = await ctx.db
577
+ .query("contentEntryTags")
578
+ .withIndex("by_entry_and_field", (q) => q.eq("entryId", entryId).eq("fieldName", fieldName))
579
+ .collect();
580
+ const existingTermIds = new Set(existing.map((e) => e.termId));
581
+ const newTermIds = new Set(termIds);
582
+ // Calculate terms to remove and add
583
+ const toRemove = existing.filter((e) => !newTermIds.has(e.termId));
584
+ const toAdd = termIds.filter((id) => !existingTermIds.has(id));
585
+ // Remove old associations and update usage counts
586
+ for (const assoc of toRemove) {
587
+ const term = await ctx.db.get(assoc.termId);
588
+ if (term && term.usageCount > 0) {
589
+ await ctx.db.patch(assoc.termId, {
590
+ usageCount: term.usageCount - 1,
591
+ });
592
+ }
593
+ await ctx.db.delete(assoc._id);
594
+ }
595
+ // Add new associations and update usage counts
596
+ for (let i = 0; i < toAdd.length; i++) {
597
+ const termId = toAdd[i];
598
+ const term = await ctx.db.get(termId);
599
+ if (!term || isDeleted(term)) {
600
+ continue; // Skip invalid terms
601
+ }
602
+ // Update usage count
603
+ await ctx.db.patch(termId, {
604
+ usageCount: term.usageCount + 1,
605
+ });
606
+ // Create association
607
+ await ctx.db.insert("contentEntryTags", {
608
+ entryId,
609
+ termId,
610
+ taxonomyId: term.taxonomyId,
611
+ fieldName,
612
+ sortOrder: i,
613
+ });
614
+ }
615
+ // Update sort order for existing items that weren't removed
616
+ const remainingExisting = existing.filter((e) => newTermIds.has(e.termId));
617
+ for (const assoc of remainingExisting) {
618
+ const newIndex = termIds.indexOf(assoc.termId);
619
+ if (newIndex !== assoc.sortOrder) {
620
+ await ctx.db.patch(assoc._id, { sortOrder: newIndex });
621
+ }
622
+ }
623
+ return null;
624
+ },
625
+ });
626
+ /**
627
+ * Add a single term to an entry field.
628
+ */
629
+ export const addTermToEntry = mutation({
630
+ args: {
631
+ entryId: v.id("contentEntries"),
632
+ fieldName: v.string(),
633
+ termId: v.id("taxonomyTerms"),
634
+ },
635
+ returns: v.null(),
636
+ handler: async (ctx, args) => {
637
+ const { entryId, fieldName, termId } = args;
638
+ // Verify entry exists
639
+ const entry = await ctx.db.get(entryId);
640
+ if (!entry || isDeleted(entry)) {
641
+ throw new Error("Content entry not found");
642
+ }
643
+ // Verify term exists
644
+ const term = await ctx.db.get(termId);
645
+ if (!term || isDeleted(term)) {
646
+ throw new Error("Term not found");
647
+ }
648
+ // Check if already associated
649
+ const existing = await ctx.db
650
+ .query("contentEntryTags")
651
+ .withIndex("by_entry_and_field", (q) => q.eq("entryId", entryId).eq("fieldName", fieldName))
652
+ .collect();
653
+ if (existing.some((e) => e.termId === termId)) {
654
+ return null; // Already associated
655
+ }
656
+ // Update usage count
657
+ await ctx.db.patch(termId, {
658
+ usageCount: term.usageCount + 1,
659
+ });
660
+ // Create association
661
+ await ctx.db.insert("contentEntryTags", {
662
+ entryId,
663
+ termId,
664
+ taxonomyId: term.taxonomyId,
665
+ fieldName,
666
+ sortOrder: existing.length,
667
+ });
668
+ return null;
669
+ },
670
+ });
671
+ /**
672
+ * Remove a single term from an entry field.
673
+ */
674
+ export const removeTermFromEntry = mutation({
675
+ args: {
676
+ entryId: v.id("contentEntries"),
677
+ fieldName: v.string(),
678
+ termId: v.id("taxonomyTerms"),
679
+ },
680
+ returns: v.null(),
681
+ handler: async (ctx, args) => {
682
+ const { entryId, fieldName, termId } = args;
683
+ // Find the association
684
+ const associations = await ctx.db
685
+ .query("contentEntryTags")
686
+ .withIndex("by_entry_and_field", (q) => q.eq("entryId", entryId).eq("fieldName", fieldName))
687
+ .collect();
688
+ const assoc = associations.find((a) => a.termId === termId);
689
+ if (!assoc) {
690
+ return null; // Not associated
691
+ }
692
+ // Update usage count
693
+ const term = await ctx.db.get(termId);
694
+ if (term && term.usageCount > 0) {
695
+ await ctx.db.patch(termId, {
696
+ usageCount: term.usageCount - 1,
697
+ });
698
+ }
699
+ // Delete association
700
+ await ctx.db.delete(assoc._id);
701
+ return null;
702
+ },
703
+ });
704
+ /**
705
+ * Create a term and add it to an entry in one operation.
706
+ * Useful for inline tag creation.
707
+ */
708
+ export const createTermAndAddToEntry = mutation({
709
+ args: {
710
+ taxonomyId: v.id("taxonomies"),
711
+ name: v.string(),
712
+ entryId: v.id("contentEntries"),
713
+ fieldName: v.string(),
714
+ userId: v.optional(v.string()),
715
+ },
716
+ returns: v.id("taxonomyTerms"),
717
+ handler: async (ctx, args) => {
718
+ const { taxonomyId, name, entryId, fieldName, userId } = args;
719
+ // Verify taxonomy allows inline creation
720
+ const taxonomy = await ctx.db.get(taxonomyId);
721
+ if (!taxonomy || isDeleted(taxonomy)) {
722
+ throw new Error("Taxonomy not found");
723
+ }
724
+ if (!taxonomy.allowInlineCreation) {
725
+ throw new Error("Inline term creation is not allowed for this taxonomy");
726
+ }
727
+ // Generate slug
728
+ const slug = generateSlug(name);
729
+ // Check if term already exists
730
+ const existingTerm = await ctx.db
731
+ .query("taxonomyTerms")
732
+ .withIndex("by_taxonomy_and_slug", (q) => q.eq("taxonomyId", taxonomyId).eq("slug", slug))
733
+ .first();
734
+ let termId;
735
+ if (existingTerm && !isDeleted(existingTerm)) {
736
+ // Use existing term
737
+ termId = existingTerm._id;
738
+ }
739
+ else if (existingTerm) {
740
+ // Restore soft-deleted term
741
+ await ctx.db.patch(existingTerm._id, {
742
+ deletedAt: undefined,
743
+ name,
744
+ searchText: name,
745
+ updatedBy: userId,
746
+ });
747
+ termId = existingTerm._id;
748
+ }
749
+ else {
750
+ // Create new term
751
+ termId = await ctx.db.insert("taxonomyTerms", {
752
+ taxonomyId,
753
+ slug,
754
+ name,
755
+ depth: 0,
756
+ usageCount: 0,
757
+ searchText: name,
758
+ createdBy: userId,
759
+ });
760
+ }
761
+ // Add to entry
762
+ const existingAssoc = await ctx.db
763
+ .query("contentEntryTags")
764
+ .withIndex("by_entry_and_field", (q) => q.eq("entryId", entryId).eq("fieldName", fieldName))
765
+ .collect();
766
+ if (!existingAssoc.some((a) => a.termId === termId)) {
767
+ // Get the term for usage count update
768
+ const termDoc = await ctx.db.get(termId);
769
+ if (termDoc) {
770
+ await ctx.db.patch(termId, {
771
+ usageCount: termDoc.usageCount + 1,
772
+ });
773
+ }
774
+ await ctx.db.insert("contentEntryTags", {
775
+ entryId,
776
+ termId,
777
+ taxonomyId,
778
+ fieldName,
779
+ sortOrder: existingAssoc.length,
780
+ });
781
+ }
782
+ return termId;
783
+ },
784
+ });
785
+ // =============================================================================
786
+ // Media Asset Tag Mutations
787
+ // =============================================================================
788
+ /**
789
+ * Set the terms for a media asset in a taxonomy (replaces all existing terms).
790
+ *
791
+ * @example
792
+ * ```typescript
793
+ * await ctx.runMutation(api.taxonomyMutations.setMediaTerms, {
794
+ * mediaId: imageId,
795
+ * taxonomyId: categoriesTaxonomyId,
796
+ * termIds: [landscapeTagId, summerTagId],
797
+ * });
798
+ * ```
799
+ */
800
+ export const setMediaTerms = mutation({
801
+ args: {
802
+ mediaId: v.id("mediaItems"),
803
+ taxonomyId: v.id("taxonomies"),
804
+ termIds: v.array(v.id("taxonomyTerms")),
805
+ },
806
+ returns: v.null(),
807
+ handler: async (ctx, args) => {
808
+ const { mediaId, taxonomyId, termIds } = args;
809
+ const media = await ctx.db.get(mediaId);
810
+ if (!media || isDeleted(media)) {
811
+ throw new Error("Media asset not found");
812
+ }
813
+ const taxonomy = await ctx.db.get(taxonomyId);
814
+ if (!taxonomy || isDeleted(taxonomy)) {
815
+ throw new Error("Taxonomy not found");
816
+ }
817
+ const existing = await ctx.db
818
+ .query("mediaAssetTags")
819
+ .withIndex("by_media_and_taxonomy", (q) => q.eq("mediaId", mediaId).eq("taxonomyId", taxonomyId))
820
+ .collect();
821
+ const existingTermIds = new Set(existing.map((e) => e.termId));
822
+ const newTermIds = new Set(termIds);
823
+ const toRemove = existing.filter((e) => !newTermIds.has(e.termId));
824
+ const toAdd = termIds.filter((id) => !existingTermIds.has(id));
825
+ for (const assoc of toRemove) {
826
+ const term = await ctx.db.get(assoc.termId);
827
+ if (term && term.usageCount > 0) {
828
+ await ctx.db.patch(assoc.termId, {
829
+ usageCount: term.usageCount - 1,
830
+ });
831
+ }
832
+ await ctx.db.delete(assoc._id);
833
+ }
834
+ for (let i = 0; i < toAdd.length; i++) {
835
+ const termId = toAdd[i];
836
+ const term = await ctx.db.get(termId);
837
+ if (!term || isDeleted(term)) {
838
+ continue;
839
+ }
840
+ if (term.taxonomyId !== taxonomyId) {
841
+ continue;
842
+ }
843
+ await ctx.db.patch(termId, {
844
+ usageCount: term.usageCount + 1,
845
+ });
846
+ await ctx.db.insert("mediaAssetTags", {
847
+ mediaId,
848
+ termId,
849
+ taxonomyId,
850
+ sortOrder: i,
851
+ });
852
+ }
853
+ const remainingExisting = existing.filter((e) => newTermIds.has(e.termId));
854
+ for (const assoc of remainingExisting) {
855
+ const newIndex = termIds.indexOf(assoc.termId);
856
+ if (newIndex !== assoc.sortOrder) {
857
+ await ctx.db.patch(assoc._id, { sortOrder: newIndex });
858
+ }
859
+ }
860
+ return null;
861
+ },
862
+ });
863
+ /**
864
+ * Add a single term to a media asset.
865
+ */
866
+ export const addTermToMedia = mutation({
867
+ args: {
868
+ mediaId: v.id("mediaItems"),
869
+ termId: v.id("taxonomyTerms"),
870
+ },
871
+ returns: v.null(),
872
+ handler: async (ctx, args) => {
873
+ const { mediaId, termId } = args;
874
+ const media = await ctx.db.get(mediaId);
875
+ if (!media || isDeleted(media)) {
876
+ throw new Error("Media asset not found");
877
+ }
878
+ const term = await ctx.db.get(termId);
879
+ if (!term || isDeleted(term)) {
880
+ throw new Error("Term not found");
881
+ }
882
+ const existing = await ctx.db
883
+ .query("mediaAssetTags")
884
+ .withIndex("by_media", (q) => q.eq("mediaId", mediaId))
885
+ .collect();
886
+ if (existing.some((e) => e.termId === termId)) {
887
+ return null;
888
+ }
889
+ await ctx.db.patch(termId, {
890
+ usageCount: term.usageCount + 1,
891
+ });
892
+ await ctx.db.insert("mediaAssetTags", {
893
+ mediaId,
894
+ termId,
895
+ taxonomyId: term.taxonomyId,
896
+ sortOrder: existing.length,
897
+ });
898
+ return null;
899
+ },
900
+ });
901
+ /**
902
+ * Remove a single term from a media asset.
903
+ */
904
+ export const removeTermFromMedia = mutation({
905
+ args: {
906
+ mediaId: v.id("mediaItems"),
907
+ termId: v.id("taxonomyTerms"),
908
+ },
909
+ returns: v.null(),
910
+ handler: async (ctx, args) => {
911
+ const { mediaId, termId } = args;
912
+ const associations = await ctx.db
913
+ .query("mediaAssetTags")
914
+ .withIndex("by_media", (q) => q.eq("mediaId", mediaId))
915
+ .collect();
916
+ const assoc = associations.find((a) => a.termId === termId);
917
+ if (!assoc) {
918
+ return null;
919
+ }
920
+ const term = await ctx.db.get(termId);
921
+ if (term && term.usageCount > 0) {
922
+ await ctx.db.patch(termId, {
923
+ usageCount: term.usageCount - 1,
924
+ });
925
+ }
926
+ await ctx.db.delete(assoc._id);
927
+ return null;
928
+ },
929
+ });
930
+ /**
931
+ * Create a term and add it to a media asset in one operation.
932
+ * Useful for inline tag creation in the media library.
933
+ */
934
+ export const createTermAndAddToMedia = mutation({
935
+ args: {
936
+ taxonomyId: v.id("taxonomies"),
937
+ name: v.string(),
938
+ mediaId: v.id("mediaItems"),
939
+ userId: v.optional(v.string()),
940
+ },
941
+ returns: v.id("taxonomyTerms"),
942
+ handler: async (ctx, args) => {
943
+ const { taxonomyId, name, mediaId, userId } = args;
944
+ const taxonomy = await ctx.db.get(taxonomyId);
945
+ if (!taxonomy || isDeleted(taxonomy)) {
946
+ throw new Error("Taxonomy not found");
947
+ }
948
+ if (!taxonomy.allowInlineCreation) {
949
+ throw new Error("Inline term creation is not allowed for this taxonomy");
950
+ }
951
+ const slug = generateSlug(name);
952
+ const existingTerm = await ctx.db
953
+ .query("taxonomyTerms")
954
+ .withIndex("by_taxonomy_and_slug", (q) => q.eq("taxonomyId", taxonomyId).eq("slug", slug))
955
+ .first();
956
+ let termId;
957
+ if (existingTerm && !isDeleted(existingTerm)) {
958
+ termId = existingTerm._id;
959
+ }
960
+ else if (existingTerm) {
961
+ await ctx.db.patch(existingTerm._id, {
962
+ deletedAt: undefined,
963
+ name,
964
+ searchText: name,
965
+ updatedBy: userId,
966
+ });
967
+ termId = existingTerm._id;
968
+ }
969
+ else {
970
+ termId = await ctx.db.insert("taxonomyTerms", {
971
+ taxonomyId,
972
+ slug,
973
+ name,
974
+ depth: 0,
975
+ usageCount: 0,
976
+ searchText: name,
977
+ createdBy: userId,
978
+ });
979
+ }
980
+ const existingAssoc = await ctx.db
981
+ .query("mediaAssetTags")
982
+ .withIndex("by_media", (q) => q.eq("mediaId", mediaId))
983
+ .collect();
984
+ if (!existingAssoc.some((a) => a.termId === termId)) {
985
+ const termDoc = await ctx.db.get(termId);
986
+ if (termDoc) {
987
+ await ctx.db.patch(termId, {
988
+ usageCount: termDoc.usageCount + 1,
989
+ });
990
+ }
991
+ await ctx.db.insert("mediaAssetTags", {
992
+ mediaId,
993
+ termId,
994
+ taxonomyId,
995
+ sortOrder: existingAssoc.length,
996
+ });
997
+ }
998
+ return termId;
999
+ },
1000
+ });
1001
+ //# sourceMappingURL=taxonomyMutations.js.map