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,849 @@
1
+ /**
2
+ * Media Folder Mutation Functions
3
+ *
4
+ * Provides mutation functions for creating, updating, moving, and deleting media folders.
5
+ * Media folders organize media assets into a hierarchical structure with path validation.
6
+ *
7
+ * Folder Hierarchy:
8
+ * - Root folders have no parentId
9
+ * - Nested folders reference their parent folder
10
+ * - Path is automatically computed from the folder hierarchy (e.g., "/images/blog/2026")
11
+ * - Moving folders updates paths for the folder and all descendants
12
+ */
13
+ import { mutation, query } from "./_generated/server.js";
14
+ import { v } from "convex/values";
15
+ import { createMediaFolderArgs, updateMediaFolderArgs, moveFolderArgs, mediaItemDoc, mutationAuthContext, } from "./validators.js";
16
+ import { mediaFolderNotFound, mediaFolderDeleted, mediaFolderNotDeleted, mediaFolderNameInvalid, mediaFolderNameDuplicate, mediaFolderDepthExceeded, mediaFolderPathTooLong, mediaFolderHasContents, mediaFolderCircularMove, mediaFolderParentDeleted, mediaFolderCreateFailed, internalError, } from "./lib/errors.js";
17
+ import { requireMutationAuth } from "./lib/mutationAuth.js";
18
+ import { isDeleted } from "./lib/softDelete.js";
19
+ // =============================================================================
20
+ // Constants
21
+ // =============================================================================
22
+ /**
23
+ * Maximum depth of folder nesting.
24
+ * Prevents excessively deep hierarchies that could impact performance.
25
+ */
26
+ const MAX_FOLDER_DEPTH = 10;
27
+ /**
28
+ * Maximum length of a folder path.
29
+ * Prevents extremely long paths that could cause issues.
30
+ */
31
+ const MAX_PATH_LENGTH = 500;
32
+ /**
33
+ * Invalid characters in folder names.
34
+ * These characters are not allowed because they could break path parsing.
35
+ */
36
+ const INVALID_NAME_CHARS = /[/\\:*?"<>|]/;
37
+ // =============================================================================
38
+ // Helper Functions
39
+ // =============================================================================
40
+ /**
41
+ * Validates a folder name.
42
+ *
43
+ * @param name - The folder name to validate
44
+ * @throws Error if the name is invalid
45
+ */
46
+ function validateFolderName(name) {
47
+ // Check for empty or whitespace-only names
48
+ const trimmed = name.trim();
49
+ if (trimmed.length === 0) {
50
+ throw mediaFolderNameInvalid(name, "Name cannot be empty");
51
+ }
52
+ // Check for invalid characters
53
+ if (INVALID_NAME_CHARS.test(trimmed)) {
54
+ throw mediaFolderNameInvalid(name, `Contains invalid characters. The following are not allowed: / \\ : * ? " < > |`);
55
+ }
56
+ // Check length
57
+ if (trimmed.length > 255) {
58
+ throw mediaFolderNameInvalid(name, "Name cannot exceed 255 characters");
59
+ }
60
+ }
61
+ /**
62
+ * Builds the full path for a folder based on its parent.
63
+ *
64
+ * @param name - The folder name
65
+ * @param parentPath - The parent folder's path (empty string for root)
66
+ * @returns The full path for the folder
67
+ */
68
+ function buildFolderPath(name, parentPath) {
69
+ const trimmedName = name.trim();
70
+ if (!parentPath || parentPath === "/") {
71
+ return `/${trimmedName}`;
72
+ }
73
+ return `${parentPath}/${trimmedName}`;
74
+ }
75
+ /**
76
+ * Calculates the depth of a path.
77
+ *
78
+ * @param path - The folder path
79
+ * @returns The depth (number of segments)
80
+ */
81
+ function getPathDepth(path) {
82
+ if (!path || path === "/")
83
+ return 0;
84
+ // Split by "/" and filter out empty strings
85
+ return path.split("/").filter((segment) => segment.length > 0).length;
86
+ }
87
+ // =============================================================================
88
+ // Create Media Folder Mutation
89
+ // =============================================================================
90
+ /**
91
+ * Mutation to create a new media folder.
92
+ *
93
+ * Creates a folder in the media library hierarchy. Folders can be nested
94
+ * within other folders up to MAX_FOLDER_DEPTH levels deep. The full path
95
+ * is automatically computed based on the parent folder hierarchy.
96
+ *
97
+ * Validation Rules:
98
+ * - Folder name must not be empty or whitespace-only
99
+ * - Folder name must not contain: / \ : * ? " < > |
100
+ * - Folder name must not exceed 255 characters
101
+ * - Full path must not exceed MAX_PATH_LENGTH characters
102
+ * - Folder depth must not exceed MAX_FOLDER_DEPTH levels
103
+ * - Parent folder must exist and not be soft-deleted (if provided)
104
+ * - Folder name must be unique within the parent folder
105
+ *
106
+ * @param name - The folder name (required)
107
+ * @param parentId - Optional parent folder ID for nesting
108
+ * @param description - Optional description of the folder
109
+ * @param sortOrder - Optional custom sort order
110
+ * @param createdBy - Optional user ID for audit trail
111
+ *
112
+ * @returns The created media folder document
113
+ *
114
+ * @throws Error if the folder name is invalid
115
+ * @throws Error if the parent folder does not exist
116
+ * @throws Error if the parent folder has been deleted
117
+ * @throws Error if the folder depth would exceed the maximum
118
+ * @throws Error if the path length would exceed the maximum
119
+ * @throws Error if a folder with the same name already exists in the parent
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * // Create a root folder
124
+ * const imagesFolder = await ctx.runMutation(api.mediaFolderMutations.createMediaFolder, {
125
+ * name: "Images",
126
+ * description: "All image assets",
127
+ * createdBy: currentUserId,
128
+ * });
129
+ *
130
+ * // Create a nested folder
131
+ * const blogFolder = await ctx.runMutation(api.mediaFolderMutations.createMediaFolder, {
132
+ * name: "Blog",
133
+ * parentId: imagesFolder._id,
134
+ * description: "Blog post images",
135
+ * createdBy: currentUserId,
136
+ * });
137
+ *
138
+ * // Result: blogFolder.path === "/Images/Blog"
139
+ * ```
140
+ */
141
+ export const createMediaFolder = mutation({
142
+ args: {
143
+ ...createMediaFolderArgs.fields,
144
+ /** Optional auth context for mutation-level authorization */
145
+ _auth: v.optional(mutationAuthContext),
146
+ },
147
+ returns: mediaItemDoc,
148
+ handler: async (ctx, args) => {
149
+ const { name, parentId, description, sortOrder, createdBy, _auth } = args;
150
+ // Authorization check - mediaFolders.create permission
151
+ requireMutationAuth(_auth, "mediaItems", "create");
152
+ // Validate folder name
153
+ validateFolderName(name);
154
+ // Determine parent path and validate parent folder
155
+ let parentPath = "";
156
+ if (parentId !== undefined) {
157
+ // Fetch parent folder
158
+ const parentFolder = await ctx.db.get(parentId);
159
+ if (!parentFolder) {
160
+ throw mediaFolderNotFound(parentId);
161
+ }
162
+ if (isDeleted(parentFolder)) {
163
+ throw mediaFolderDeleted(parentId);
164
+ }
165
+ parentPath = parentFolder.path;
166
+ // Check folder depth limit
167
+ const parentDepth = getPathDepth(parentPath);
168
+ if (parentDepth >= MAX_FOLDER_DEPTH) {
169
+ throw mediaFolderDepthExceeded(MAX_FOLDER_DEPTH, parentDepth + 1);
170
+ }
171
+ }
172
+ // Build the full path
173
+ const path = buildFolderPath(name, parentPath);
174
+ // Check path length limit
175
+ if (path.length > MAX_PATH_LENGTH) {
176
+ throw mediaFolderPathTooLong(MAX_PATH_LENGTH, path.length);
177
+ }
178
+ // Check for duplicate folder name in the same parent
179
+ const existingFolder = await ctx.db
180
+ .query("mediaItems")
181
+ .withIndex("by_path", (q) => q.eq("path", path))
182
+ .filter((q) => q.and(q.eq(q.field("kind"), "folder"), q.eq(q.field("deletedAt"), undefined)))
183
+ .first();
184
+ if (existingFolder) {
185
+ throw mediaFolderNameDuplicate(name.trim(), parentPath || undefined);
186
+ }
187
+ // Create the folder
188
+ const folderId = await ctx.db.insert("mediaItems", {
189
+ kind: "folder",
190
+ name: name.trim(),
191
+ parentId,
192
+ path,
193
+ description,
194
+ sortOrder,
195
+ createdBy,
196
+ });
197
+ // Retrieve and return the created folder
198
+ const folder = await ctx.db.get(folderId);
199
+ if (!folder) {
200
+ throw mediaFolderCreateFailed();
201
+ }
202
+ return folder;
203
+ },
204
+ });
205
+ // =============================================================================
206
+ // Update Media Folder Mutation
207
+ // =============================================================================
208
+ /**
209
+ * Mutation to update a media folder's metadata.
210
+ *
211
+ * Updates the folder name, description, or sort order. If the name is changed,
212
+ * the path is automatically updated for this folder and all its descendants.
213
+ *
214
+ * @param id - The folder ID to update
215
+ * @param name - Optional new folder name
216
+ * @param description - Optional new description
217
+ * @param sortOrder - Optional new sort order
218
+ *
219
+ * @returns The updated media folder document
220
+ *
221
+ * @throws Error if the folder does not exist
222
+ * @throws Error if the folder has been deleted
223
+ * @throws Error if the new name is invalid
224
+ * @throws Error if a folder with the new name already exists in the same parent
225
+ *
226
+ * @example
227
+ * ```typescript
228
+ * const updated = await ctx.runMutation(api.mediaFolderMutations.updateMediaFolder, {
229
+ * id: folderId,
230
+ * name: "Blog Images",
231
+ * description: "Updated description",
232
+ * });
233
+ * ```
234
+ */
235
+ export const updateMediaFolder = mutation({
236
+ args: {
237
+ ...updateMediaFolderArgs.fields,
238
+ /** Optional auth context for mutation-level authorization */
239
+ _auth: v.optional(mutationAuthContext),
240
+ },
241
+ returns: mediaItemDoc,
242
+ handler: async (ctx, args) => {
243
+ const { id, name, description, sortOrder, _auth } = args;
244
+ // Authorization check - mediaFolders.update permission
245
+ requireMutationAuth(_auth, "mediaItems", "update");
246
+ const folder = await ctx.db.get(id);
247
+ if (!folder) {
248
+ throw mediaFolderNotFound(id);
249
+ }
250
+ if (isDeleted(folder)) {
251
+ throw mediaFolderDeleted(id);
252
+ }
253
+ // Build updates object
254
+ const updates = {};
255
+ // Handle name change (requires path update)
256
+ if (name !== undefined && name.trim() !== folder.name) {
257
+ validateFolderName(name);
258
+ // Get parent path
259
+ let parentPath = "";
260
+ if (folder.parentId) {
261
+ const parentFolder = await ctx.db.get(folder.parentId);
262
+ if (parentFolder) {
263
+ parentPath = parentFolder.path;
264
+ }
265
+ }
266
+ // Build new path
267
+ const newPath = buildFolderPath(name, parentPath);
268
+ // Check path length
269
+ if (newPath.length > MAX_PATH_LENGTH) {
270
+ throw mediaFolderPathTooLong(MAX_PATH_LENGTH, newPath.length);
271
+ }
272
+ // Check for duplicate name in same parent
273
+ const existingFolder = await ctx.db
274
+ .query("mediaItems")
275
+ .withIndex("by_path", (q) => q.eq("path", newPath))
276
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
277
+ .first();
278
+ if (existingFolder && existingFolder._id !== id) {
279
+ throw mediaFolderNameDuplicate(name.trim(), parentPath || undefined);
280
+ }
281
+ updates.name = name.trim();
282
+ const oldPath = folder.path;
283
+ updates.path = newPath;
284
+ // Update all descendant folder paths
285
+ await updateDescendantPaths(ctx, oldPath, newPath);
286
+ }
287
+ if (description !== undefined) {
288
+ updates.description = description;
289
+ }
290
+ if (sortOrder !== undefined) {
291
+ updates.sortOrder = sortOrder;
292
+ }
293
+ // Apply updates if any
294
+ if (Object.keys(updates).length > 0) {
295
+ await ctx.db.patch(id, updates);
296
+ }
297
+ // Retrieve and return the updated folder
298
+ const updatedFolder = await ctx.db.get(id);
299
+ if (!updatedFolder) {
300
+ throw new Error("Failed to retrieve updated media folder");
301
+ }
302
+ return updatedFolder;
303
+ },
304
+ });
305
+ /**
306
+ * Updates paths for all descendant folders when a parent folder is renamed.
307
+ */
308
+ async function updateDescendantPaths(ctx, oldParentPath, newParentPath) {
309
+ // Find all folders whose path starts with the old path
310
+ const descendants = await ctx.db
311
+ .query("mediaItems")
312
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
313
+ .collect();
314
+ for (const descendant of descendants) {
315
+ if (descendant.path.startsWith(oldParentPath + "/") &&
316
+ descendant.path !== oldParentPath) {
317
+ const newDescendantPath = descendant.path.replace(oldParentPath, newParentPath);
318
+ await ctx.db.patch(descendant._id, { path: newDescendantPath });
319
+ }
320
+ }
321
+ }
322
+ // =============================================================================
323
+ // Move Media Folder Mutation
324
+ // =============================================================================
325
+ /**
326
+ * Mutation to move a folder to a different parent.
327
+ *
328
+ * Moves a folder and all its contents (assets and subfolders) to a new
329
+ * location in the hierarchy. Updates paths for the folder and all descendants.
330
+ *
331
+ * @param id - The folder ID to move
332
+ * @param newParentId - The new parent folder ID (undefined for root level)
333
+ *
334
+ * @returns The moved media folder document
335
+ *
336
+ * @throws Error if the folder does not exist
337
+ * @throws Error if the folder has been deleted
338
+ * @throws Error if the new parent does not exist
339
+ * @throws Error if the new parent has been deleted
340
+ * @throws Error if moving would create a circular reference
341
+ * @throws Error if moving would exceed the maximum depth
342
+ *
343
+ * @example
344
+ * ```typescript
345
+ * // Move folder to a different parent
346
+ * const moved = await ctx.runMutation(api.mediaFolderMutations.moveMediaFolder, {
347
+ * id: folderId,
348
+ * newParentId: newParentFolderId,
349
+ * });
350
+ *
351
+ * // Move folder to root level
352
+ * const movedToRoot = await ctx.runMutation(api.mediaFolderMutations.moveMediaFolder, {
353
+ * id: folderId,
354
+ * newParentId: undefined,
355
+ * });
356
+ * ```
357
+ */
358
+ export const moveMediaFolder = mutation({
359
+ args: {
360
+ ...moveFolderArgs.fields,
361
+ /** Optional auth context for mutation-level authorization */
362
+ _auth: v.optional(mutationAuthContext),
363
+ },
364
+ returns: mediaItemDoc,
365
+ handler: async (ctx, args) => {
366
+ const { id, newParentId, _auth } = args;
367
+ // Authorization check - mediaFolders.update permission (move is a form of update)
368
+ requireMutationAuth(_auth, "mediaItems", "update");
369
+ const folder = await ctx.db.get(id);
370
+ if (!folder) {
371
+ throw mediaFolderNotFound(id);
372
+ }
373
+ if (isDeleted(folder)) {
374
+ throw mediaFolderDeleted(id);
375
+ }
376
+ // No change needed if parent is the same
377
+ if (folder.parentId === newParentId) {
378
+ return folder;
379
+ }
380
+ // Determine new parent path
381
+ let newParentPath = "";
382
+ if (newParentId !== undefined) {
383
+ // Fetch new parent folder
384
+ const newParentFolder = await ctx.db.get(newParentId);
385
+ if (!newParentFolder) {
386
+ throw mediaFolderNotFound(newParentId);
387
+ }
388
+ if (isDeleted(newParentFolder)) {
389
+ throw mediaFolderDeleted(newParentId);
390
+ }
391
+ // Check for circular reference
392
+ // Cannot move a folder into itself or one of its descendants
393
+ if (newParentFolder.path.startsWith(folder.path + "/") ||
394
+ newParentFolder._id === id) {
395
+ throw mediaFolderCircularMove(id);
396
+ }
397
+ newParentPath = newParentFolder.path;
398
+ // Check depth limit
399
+ const newParentDepth = getPathDepth(newParentPath);
400
+ const folderSubtreeDepth = await getMaxSubtreeDepth(ctx, folder.path);
401
+ const totalDepth = newParentDepth + 1 + folderSubtreeDepth;
402
+ if (totalDepth > MAX_FOLDER_DEPTH) {
403
+ throw mediaFolderDepthExceeded(MAX_FOLDER_DEPTH, totalDepth);
404
+ }
405
+ }
406
+ // Build new path
407
+ const oldPath = folder.path;
408
+ const newPath = buildFolderPath(folder.name, newParentPath);
409
+ // Check path length
410
+ if (newPath.length > MAX_PATH_LENGTH) {
411
+ throw mediaFolderPathTooLong(MAX_PATH_LENGTH, newPath.length);
412
+ }
413
+ // Check for duplicate name in new parent
414
+ const existingFolder = await ctx.db
415
+ .query("mediaItems")
416
+ .withIndex("by_path", (q) => q.eq("path", newPath))
417
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
418
+ .first();
419
+ if (existingFolder && existingFolder._id !== id) {
420
+ throw mediaFolderNameDuplicate(folder.name, newParentPath || undefined);
421
+ }
422
+ // Update the folder
423
+ await ctx.db.patch(id, {
424
+ parentId: newParentId,
425
+ path: newPath,
426
+ });
427
+ // Update all descendant folder paths
428
+ await updateDescendantPaths(ctx, oldPath, newPath);
429
+ // Retrieve and return the moved folder
430
+ const movedFolder = await ctx.db.get(id);
431
+ if (!movedFolder) {
432
+ throw internalError("Failed to retrieve moved media folder");
433
+ }
434
+ return movedFolder;
435
+ },
436
+ });
437
+ /**
438
+ * Gets the maximum depth of descendants under a folder path.
439
+ */
440
+ async function getMaxSubtreeDepth(ctx, folderPath) {
441
+ const descendants = await ctx.db
442
+ .query("mediaItems")
443
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
444
+ .collect();
445
+ let maxDepth = 0;
446
+ const baseDepth = getPathDepth(folderPath);
447
+ for (const descendant of descendants) {
448
+ if (descendant.path.startsWith(folderPath + "/")) {
449
+ const descendantDepth = getPathDepth(descendant.path);
450
+ const relativeDepth = descendantDepth - baseDepth;
451
+ if (relativeDepth > maxDepth) {
452
+ maxDepth = relativeDepth;
453
+ }
454
+ }
455
+ }
456
+ return maxDepth;
457
+ }
458
+ // =============================================================================
459
+ // Delete Media Folder Mutation
460
+ // =============================================================================
461
+ /**
462
+ * Validator for delete folder arguments.
463
+ */
464
+ export const deleteMediaFolderArgs = {
465
+ id: v.id("mediaItems"),
466
+ deletedBy: v.optional(v.string()),
467
+ hardDelete: v.optional(v.boolean()),
468
+ recursive: v.optional(v.boolean()),
469
+ };
470
+ /**
471
+ * Mutation to delete a media folder.
472
+ *
473
+ * Supports two modes:
474
+ * - Soft delete (default): Sets deletedAt timestamp, folder can be restored
475
+ * - Hard delete: Permanently removes the folder from the database
476
+ *
477
+ * By default, deletion fails if the folder contains assets or subfolders.
478
+ * Use recursive: true to delete the folder and all its contents.
479
+ *
480
+ * @param id - The folder ID to delete
481
+ * @param deletedBy - Optional user ID for audit trail
482
+ * @param hardDelete - If true, permanently deletes the folder
483
+ * @param recursive - If true, deletes folder and all contents
484
+ *
485
+ * @returns The deleted media folder document
486
+ *
487
+ * @throws Error if the folder does not exist
488
+ * @throws Error if the folder is already deleted (for soft delete)
489
+ * @throws Error if the folder has contents and recursive is not true
490
+ *
491
+ * @example
492
+ * ```typescript
493
+ * // Soft delete an empty folder
494
+ * const deleted = await ctx.runMutation(api.mediaFolderMutations.deleteMediaFolder, {
495
+ * id: folderId,
496
+ * deletedBy: currentUserId,
497
+ * });
498
+ *
499
+ * // Recursively delete folder and all contents
500
+ * const deleted = await ctx.runMutation(api.mediaFolderMutations.deleteMediaFolder, {
501
+ * id: folderId,
502
+ * deletedBy: currentUserId,
503
+ * recursive: true,
504
+ * });
505
+ * ```
506
+ */
507
+ export const deleteMediaFolder = mutation({
508
+ args: {
509
+ ...deleteMediaFolderArgs,
510
+ /** Optional auth context for mutation-level authorization */
511
+ _auth: v.optional(mutationAuthContext),
512
+ },
513
+ returns: mediaItemDoc,
514
+ handler: async (ctx, args) => {
515
+ const { id,
516
+ // deletedBy,
517
+ hardDelete = false, recursive = false, _auth, } = args;
518
+ // Authorization check - mediaFolders.delete permission
519
+ requireMutationAuth(_auth, "mediaItems", "delete");
520
+ const folder = await ctx.db.get(id);
521
+ if (!folder) {
522
+ throw mediaFolderNotFound(id);
523
+ }
524
+ // For soft delete, check if already deleted
525
+ if (!hardDelete && isDeleted(folder)) {
526
+ throw mediaFolderDeleted(id);
527
+ }
528
+ // Check for contents (subfolders and assets)
529
+ const subfolders = await ctx.db
530
+ .query("mediaItems")
531
+ .withIndex("by_parent", (q) => q.eq("parentId", id))
532
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
533
+ .take(1);
534
+ const assets = await ctx.db
535
+ .query("mediaItems")
536
+ .withIndex("by_parent", (q) => q.eq("parentId", id))
537
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
538
+ .take(1);
539
+ const hasContents = subfolders.length > 0 || assets.length > 0;
540
+ if (hasContents && !recursive) {
541
+ throw mediaFolderHasContents(id, subfolders.length, assets.length);
542
+ }
543
+ // If recursive, delete all contents first
544
+ if (recursive && hasContents) {
545
+ await deleteContentsRecursively(ctx, id, hardDelete);
546
+ }
547
+ if (hardDelete) {
548
+ // Permanently delete the folder
549
+ await ctx.db.delete(id);
550
+ return {
551
+ ...folder,
552
+ deletedAt: Date.now(),
553
+ };
554
+ }
555
+ else {
556
+ // Soft delete
557
+ const now = Date.now();
558
+ await ctx.db.patch(id, {
559
+ deletedAt: now,
560
+ });
561
+ return {
562
+ ...folder,
563
+ deletedAt: now,
564
+ };
565
+ }
566
+ },
567
+ });
568
+ /**
569
+ * Recursively deletes all contents of a folder.
570
+ */
571
+ async function deleteContentsRecursively(ctx, folderId, hardDelete) {
572
+ // Get all subfolders
573
+ const subfolders = await ctx.db
574
+ .query("mediaItems")
575
+ .withIndex("by_kind_and_parent", (q) => q.eq("kind", "folder").eq("parentId", folderId))
576
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
577
+ .collect();
578
+ // Recursively delete subfolders first
579
+ for (const subfolder of subfolders) {
580
+ await deleteContentsRecursively(ctx, subfolder._id, hardDelete);
581
+ if (hardDelete) {
582
+ await ctx.db.delete(subfolder._id);
583
+ }
584
+ else {
585
+ await ctx.db.patch(subfolder._id, { deletedAt: Date.now() });
586
+ }
587
+ }
588
+ // Delete/soft-delete all assets in this folder
589
+ const assets = await ctx.db
590
+ .query("mediaItems")
591
+ .withIndex("by_kind_and_parent", (q) => q.eq("kind", "asset").eq("parentId", folderId))
592
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
593
+ .collect();
594
+ for (const item of assets) {
595
+ // Type guard for asset (we already filtered by kind="asset" in the query)
596
+ if (item.kind !== "asset")
597
+ continue;
598
+ const asset = item;
599
+ if (hardDelete) {
600
+ // For hard delete, also delete the storage file
601
+ try {
602
+ await ctx.storage.delete(asset.storageId);
603
+ }
604
+ catch (error) {
605
+ console.warn(`Could not delete storage file for asset ${asset._id}:`, error instanceof Error ? error.message : error);
606
+ }
607
+ await ctx.db.delete(asset._id);
608
+ }
609
+ else {
610
+ await ctx.db.patch(asset._id, { deletedAt: Date.now() });
611
+ }
612
+ }
613
+ }
614
+ // =============================================================================
615
+ // Restore Media Folder Mutation
616
+ // =============================================================================
617
+ /**
618
+ * Validator for restore folder arguments.
619
+ */
620
+ export const restoreMediaFolderArgs = {
621
+ id: v.id("mediaItems"),
622
+ restoredBy: v.optional(v.string()),
623
+ recursive: v.optional(v.boolean()),
624
+ };
625
+ /**
626
+ * Mutation to restore a soft-deleted media folder.
627
+ *
628
+ * Removes the deletedAt timestamp from the folder.
629
+ * Optionally restores all contents recursively.
630
+ *
631
+ * @param id - The folder ID to restore
632
+ * @param restoredBy - Optional user ID for audit trail
633
+ * @param recursive - If true, restores folder and all contents
634
+ *
635
+ * @returns The restored media folder document
636
+ *
637
+ * @throws Error if the folder does not exist
638
+ * @throws Error if the folder is not deleted
639
+ * @throws Error if the parent folder is still deleted
640
+ *
641
+ * @example
642
+ * ```typescript
643
+ * const restored = await ctx.runMutation(api.mediaFolderMutations.restoreMediaFolder, {
644
+ * id: folderId,
645
+ * restoredBy: currentUserId,
646
+ * recursive: true,
647
+ * });
648
+ * ```
649
+ */
650
+ export const restoreMediaFolder = mutation({
651
+ args: {
652
+ ...restoreMediaFolderArgs,
653
+ /** Optional auth context for mutation-level authorization */
654
+ _auth: v.optional(mutationAuthContext),
655
+ },
656
+ returns: mediaItemDoc,
657
+ handler: async (ctx, args) => {
658
+ const { id,
659
+ // restoredBy,
660
+ recursive = false, _auth } = args;
661
+ // Authorization check - use update permission for restore
662
+ requireMutationAuth(_auth, "mediaItems", "update");
663
+ const folder = await ctx.db.get(id);
664
+ if (!folder) {
665
+ throw mediaFolderNotFound(id);
666
+ }
667
+ if (!isDeleted(folder)) {
668
+ throw mediaFolderNotDeleted(id);
669
+ }
670
+ // Check that parent folder is not deleted (if it exists)
671
+ if (folder.parentId) {
672
+ const parentFolder = await ctx.db.get(folder.parentId);
673
+ if (parentFolder && isDeleted(parentFolder)) {
674
+ throw mediaFolderParentDeleted(id, folder.parentId);
675
+ }
676
+ }
677
+ // Restore the folder
678
+ await ctx.db.patch(id, {
679
+ deletedAt: undefined,
680
+ });
681
+ // If recursive, restore all contents
682
+ if (recursive) {
683
+ await restoreContentsRecursively(ctx, id);
684
+ }
685
+ // Retrieve and return the restored folder
686
+ const restoredFolder = await ctx.db.get(id);
687
+ if (!restoredFolder) {
688
+ throw internalError("Failed to restore media folder");
689
+ }
690
+ return restoredFolder;
691
+ },
692
+ });
693
+ /**
694
+ * Recursively restores all contents of a folder.
695
+ */
696
+ async function restoreContentsRecursively(ctx, folderId) {
697
+ // Get all soft-deleted subfolders
698
+ const subfolders = await ctx.db
699
+ .query("mediaItems")
700
+ .withIndex("by_kind_and_parent", (q) => q.eq("kind", "folder").eq("parentId", folderId))
701
+ .filter((q) => q.neq(q.field("deletedAt"), undefined))
702
+ .collect();
703
+ for (const subfolder of subfolders) {
704
+ await ctx.db.patch(subfolder._id, { deletedAt: undefined });
705
+ await restoreContentsRecursively(ctx, subfolder._id);
706
+ }
707
+ // Restore all soft-deleted assets in this folder
708
+ const assets = await ctx.db
709
+ .query("mediaItems")
710
+ .withIndex("by_kind_and_parent", (q) => q.eq("kind", "asset").eq("parentId", folderId))
711
+ .filter((q) => q.neq(q.field("deletedAt"), undefined))
712
+ .collect();
713
+ for (const asset of assets) {
714
+ await ctx.db.patch(asset._id, { deletedAt: undefined });
715
+ }
716
+ }
717
+ // =============================================================================
718
+ // Query Functions
719
+ // =============================================================================
720
+ /**
721
+ * Query to get a media folder by ID.
722
+ *
723
+ * @param id - The folder ID
724
+ * @param includeDeleted - If true, returns soft-deleted folders
725
+ *
726
+ * @returns The folder document or null if not found
727
+ */
728
+ export const getMediaFolder = query({
729
+ args: {
730
+ id: v.id("mediaItems"),
731
+ includeDeleted: v.optional(v.boolean()),
732
+ },
733
+ returns: v.union(mediaItemDoc, v.null()),
734
+ handler: async (ctx, args) => {
735
+ const { id, includeDeleted = false } = args;
736
+ const item = await ctx.db.get(id);
737
+ // Must be a folder
738
+ if (!item || item.kind !== "folder") {
739
+ return null;
740
+ }
741
+ if (!includeDeleted && isDeleted(item)) {
742
+ return null;
743
+ }
744
+ return item;
745
+ },
746
+ });
747
+ /**
748
+ * Query to list folders in a parent folder.
749
+ *
750
+ * @param parentId - The parent folder ID (undefined for root folders)
751
+ * @param includeDeleted - If true, includes soft-deleted folders
752
+ * @param deletedOnly - If true, shows only soft-deleted folders (ignores parentId)
753
+ *
754
+ * @returns Array of folder documents sorted by sortOrder, then name
755
+ */
756
+ export const listMediaFolders = query({
757
+ args: {
758
+ parentId: v.optional(v.id("mediaItems")),
759
+ includeDeleted: v.optional(v.boolean()),
760
+ deletedOnly: v.optional(v.boolean()),
761
+ },
762
+ returns: v.array(mediaItemDoc),
763
+ handler: async (ctx, args) => {
764
+ const { parentId, includeDeleted = false, deletedOnly = false } = args;
765
+ // When viewing trash (deletedOnly), show all deleted folders regardless of parent
766
+ let query = deletedOnly
767
+ ? ctx.db
768
+ .query("mediaItems")
769
+ .withIndex("by_kind", (q) => q.eq("kind", "folder"))
770
+ : ctx.db
771
+ .query("mediaItems")
772
+ .withIndex("by_kind_and_parent", (q) => q.eq("kind", "folder").eq("parentId", parentId));
773
+ if (deletedOnly) {
774
+ query = query.filter((q) => q.neq(q.field("deletedAt"), undefined));
775
+ }
776
+ else if (!includeDeleted) {
777
+ query = query.filter((q) => q.eq(q.field("deletedAt"), undefined));
778
+ }
779
+ const folders = await query.collect();
780
+ // Sort by sortOrder (nulls last), then by name
781
+ // Note: sortOrder is only defined on folders, so these are safe after kind filter
782
+ folders.sort((a, b) => {
783
+ const aOrder = a.kind === "folder" ? a.sortOrder : undefined;
784
+ const bOrder = b.kind === "folder" ? b.sortOrder : undefined;
785
+ if (aOrder !== undefined && bOrder !== undefined) {
786
+ return aOrder - bOrder;
787
+ }
788
+ if (aOrder !== undefined)
789
+ return -1;
790
+ if (bOrder !== undefined)
791
+ return 1;
792
+ return a.name.localeCompare(b.name);
793
+ });
794
+ return folders;
795
+ },
796
+ });
797
+ /**
798
+ * Query to get a folder by its path.
799
+ *
800
+ * @param path - The full folder path (e.g., "/Images/Blog")
801
+ * @param includeDeleted - If true, returns soft-deleted folders
802
+ *
803
+ * @returns The folder document or null if not found
804
+ */
805
+ export const getMediaFolderByPath = query({
806
+ args: {
807
+ path: v.string(),
808
+ includeDeleted: v.optional(v.boolean()),
809
+ },
810
+ returns: v.union(mediaItemDoc, v.null()),
811
+ handler: async (ctx, args) => {
812
+ const { path, includeDeleted = false } = args;
813
+ let query = ctx.db
814
+ .query("mediaItems")
815
+ .withIndex("by_path", (q) => q.eq("path", path))
816
+ .filter((q) => q.eq(q.field("kind"), "folder"));
817
+ if (!includeDeleted) {
818
+ query = query.filter((q) => q.eq(q.field("deletedAt"), undefined));
819
+ }
820
+ return await query.first();
821
+ },
822
+ });
823
+ /**
824
+ * Query to get the folder tree (all folders as a flat list with hierarchy info).
825
+ *
826
+ * @param includeDeleted - If true, includes soft-deleted folders
827
+ *
828
+ * @returns Array of all folders
829
+ */
830
+ export const getFolderTree = query({
831
+ args: {
832
+ includeDeleted: v.optional(v.boolean()),
833
+ },
834
+ returns: v.array(mediaItemDoc),
835
+ handler: async (ctx, args) => {
836
+ const { includeDeleted = false } = args;
837
+ let query = ctx.db
838
+ .query("mediaItems")
839
+ .withIndex("by_kind", (q) => q.eq("kind", "folder"));
840
+ if (!includeDeleted) {
841
+ query = query.filter((q) => q.eq(q.field("deletedAt"), undefined));
842
+ }
843
+ const folders = await query.collect();
844
+ // Sort by path for hierarchical display
845
+ folders.sort((a, b) => a.path.localeCompare(b.path));
846
+ return folders;
847
+ },
848
+ });
849
+ //# sourceMappingURL=mediaFolderMutations.js.map