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,932 @@
1
+ /**
2
+ * Media Asset Query Functions
3
+ *
4
+ * Provides query functions for retrieving media assets from the CMS.
5
+ * Media assets are file records that reference Convex's built-in file storage,
6
+ * along with metadata like dimensions, MIME type, and organization tags.
7
+ */
8
+
9
+ import { v, type Infer } from "convex/values";
10
+ import { isDeleted } from "./lib/softDelete.js";
11
+ import { paginationOptsValidator } from "convex/server";
12
+ import { stream } from "convex-helpers/server/stream";
13
+ import { query, type QueryCtx } from "./_generated/server.js";
14
+ import type { Doc, Id } from "./_generated/dataModel.js";
15
+ import {
16
+ // mediaItemDoc,
17
+ listMediaAssetsArgs,
18
+ paginationResultValidator,
19
+ mediaSortFieldValidator,
20
+ mediaSortDirectionValidator,
21
+ } from "./validators.js";
22
+ import schema, { mediaAssetItemValidator } from "./schema.js";
23
+
24
+ /**
25
+ * Return type for the get query.
26
+ * Extends the base media asset document with the resolved storage URL
27
+ * and optimization hints for efficient asset delivery.
28
+ */
29
+ const mediaAssetWithUrlDoc = v.object({
30
+ ...mediaAssetItemValidator.fields,
31
+ _id: v.id("mediaItems"),
32
+ _creationTime: v.number(),
33
+ /** The resolved public URL for accessing the asset file */
34
+ url: v.union(v.string(), v.null()),
35
+ /**
36
+ * Optimization hints for efficient asset usage.
37
+ * Includes dimensions for images, duration for audio/video,
38
+ * and recommended transformations based on asset type.
39
+ */
40
+ optimizationHints: v.object({
41
+ /** Whether the asset is an image that can be resized */
42
+ isResizable: v.boolean(),
43
+ /** Suggested aspect ratio for maintaining proportions (width / height) */
44
+ aspectRatio: v.optional(v.number()),
45
+ /** Whether the asset supports transparent backgrounds (PNG, WebP, GIF) */
46
+ hasTransparency: v.optional(v.boolean()),
47
+ /** Whether the asset is a vector format (SVG) that scales without loss */
48
+ isVector: v.optional(v.boolean()),
49
+ /** Suggested max display width based on original dimensions */
50
+ suggestedMaxWidth: v.optional(v.number()),
51
+ /** For video/audio: total duration in seconds */
52
+ durationSeconds: v.optional(v.number()),
53
+ }),
54
+ });
55
+
56
+ /**
57
+ * Arguments for retrieving a single media asset.
58
+ */
59
+ const getMediaAssetArgs = v.object({
60
+ /** The ID of the media asset to retrieve */
61
+ id: v.id("mediaItems"),
62
+ /** Whether to include soft-deleted assets (default: false) */
63
+ includeDeleted: v.optional(v.boolean()),
64
+ });
65
+
66
+ /**
67
+ * Query to retrieve a single media asset by ID.
68
+ *
69
+ * Returns the asset metadata along with a resolved storage URL and
70
+ * optimization hints for efficient frontend rendering.
71
+ *
72
+ * @param id - The media asset ID to retrieve
73
+ * @param includeDeleted - Whether to include soft-deleted assets (default: false)
74
+ * @returns The media asset document with URL and optimization hints, or null if not found
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * // Basic usage - get asset by ID
79
+ * const asset = await ctx.runQuery(api.mediaAssets.get, {
80
+ * id: assetId,
81
+ * });
82
+ *
83
+ * if (asset) {
84
+ * console.log("Asset URL:", asset.url);
85
+ * console.log("Dimensions:", asset.width, "x", asset.height);
86
+ *
87
+ * // Use optimization hints for responsive images
88
+ * if (asset.optimizationHints.isResizable) {
89
+ * console.log("Aspect ratio:", asset.optimizationHints.aspectRatio);
90
+ * console.log("Max width:", asset.optimizationHints.suggestedMaxWidth);
91
+ * }
92
+ * }
93
+ *
94
+ * // Including deleted assets (for admin recovery UI)
95
+ * const deletedAsset = await ctx.runQuery(api.mediaAssets.get, {
96
+ * id: assetId,
97
+ * includeDeleted: true,
98
+ * });
99
+ * ```
100
+ */
101
+ export const get = query({
102
+ args: getMediaAssetArgs.fields,
103
+ returns: v.union(mediaAssetWithUrlDoc, v.null()),
104
+ handler: async (ctx, args) => {
105
+ const { id, includeDeleted = false } = args;
106
+
107
+ const item = await ctx.db.get(id);
108
+
109
+ // Return null if item doesn't exist or is not an asset
110
+ if (!item || item.kind !== "asset") {
111
+ return null;
112
+ }
113
+
114
+ // Filter out soft-deleted assets unless explicitly requested
115
+ // This respects the soft delete pattern used throughout the CMS
116
+ if (!includeDeleted && isDeleted(item)) {
117
+ return null;
118
+ }
119
+
120
+ // Resolve the storage URL from Convex file storage
121
+ // This generates a public URL that can be used directly in img/video tags
122
+ const url = await ctx.storage.getUrl(item.storageId);
123
+
124
+ // Build optimization hints based on asset type and metadata
125
+ const optimizationHints = buildOptimizationHints(item);
126
+
127
+ return {
128
+ ...item,
129
+ url,
130
+ optimizationHints,
131
+ };
132
+ },
133
+ });
134
+
135
+ /**
136
+ * MIME types that support transparency.
137
+ */
138
+ const TRANSPARENT_MIME_TYPES = [
139
+ "image/png",
140
+ "image/webp",
141
+ "image/gif",
142
+ "image/avif",
143
+ ];
144
+
145
+ /**
146
+ * MIME types that are vector formats.
147
+ */
148
+ const VECTOR_MIME_TYPES = ["image/svg+xml"];
149
+
150
+ /**
151
+ * MIME types that can be resized (raster images).
152
+ */
153
+ const RESIZABLE_MIME_TYPES = [
154
+ "image/jpeg",
155
+ "image/jpg",
156
+ "image/png",
157
+ "image/webp",
158
+ "image/avif",
159
+ "image/gif",
160
+ ];
161
+
162
+ /**
163
+ * Builds optimization hints based on the asset's type and metadata.
164
+ *
165
+ * These hints help frontends make intelligent decisions about:
166
+ * - Image sizing and responsive layouts
167
+ * - Placeholder dimensions
168
+ * - Format support (transparency, vectors)
169
+ * - Media playback (duration)
170
+ */
171
+ function buildOptimizationHints(asset: {
172
+ mimeType: string;
173
+ width?: number;
174
+ height?: number;
175
+ duration?: number;
176
+ }): {
177
+ isResizable: boolean;
178
+ aspectRatio?: number;
179
+ hasTransparency?: boolean;
180
+ isVector?: boolean;
181
+ suggestedMaxWidth?: number;
182
+ durationSeconds?: number;
183
+ } {
184
+ const hints: {
185
+ isResizable: boolean;
186
+ aspectRatio?: number;
187
+ hasTransparency?: boolean;
188
+ isVector?: boolean;
189
+ suggestedMaxWidth?: number;
190
+ durationSeconds?: number;
191
+ } = {
192
+ isResizable: false,
193
+ };
194
+
195
+ // Check if the asset is a resizable raster image
196
+ if (RESIZABLE_MIME_TYPES.includes(asset.mimeType)) {
197
+ hints.isResizable = true;
198
+ }
199
+
200
+ // Check for transparency support
201
+ if (TRANSPARENT_MIME_TYPES.includes(asset.mimeType)) {
202
+ hints.hasTransparency = true;
203
+ }
204
+
205
+ // Check for vector format
206
+ if (VECTOR_MIME_TYPES.includes(asset.mimeType)) {
207
+ hints.isVector = true;
208
+ // Vectors are infinitely resizable but in a different way
209
+ hints.isResizable = false;
210
+ }
211
+
212
+ // Calculate aspect ratio if dimensions are available
213
+ if (asset.width && asset.height && asset.height > 0) {
214
+ hints.aspectRatio = Math.round((asset.width / asset.height) * 1000) / 1000;
215
+ hints.suggestedMaxWidth = asset.width;
216
+ }
217
+
218
+ // Include duration for video/audio assets
219
+ if (asset.duration !== undefined && asset.duration > 0) {
220
+ hints.durationSeconds = asset.duration;
221
+ }
222
+
223
+ return hints;
224
+ }
225
+
226
+ // =============================================================================
227
+ // List Media Assets Query
228
+ // =============================================================================
229
+
230
+ /**
231
+ * Default page size for media assets list queries.
232
+ */
233
+ const DEFAULT_NUM_ITEMS = 50;
234
+
235
+ /**
236
+ * Maximum page size for media assets list queries.
237
+ */
238
+ const MAX_NUM_ITEMS = 250;
239
+
240
+ /**
241
+ * Type for sort options.
242
+ */
243
+ type MediaSortField = Infer<typeof mediaSortFieldValidator>;
244
+ type MediaSortDirection = Infer<typeof mediaSortDirectionValidator>;
245
+
246
+ interface MediaSortOptions {
247
+ sortField: MediaSortField;
248
+ sortDirection: MediaSortDirection;
249
+ }
250
+
251
+ /**
252
+ * Paginated response type for media assets list.
253
+ * Reuses the mediaAssetWithUrlDoc defined above.
254
+ */
255
+ const paginatedMediaAssetsResponse = paginationResultValidator(
256
+ mediaAssetWithUrlDoc
257
+ );
258
+
259
+ /**
260
+ * Query to list media assets with optional folder filter and pagination.
261
+ *
262
+ * Supports filtering by:
263
+ * - Folder: Filter to assets in a specific folder or root level
264
+ * - MIME type: Exact match or prefix match (e.g., "image/")
265
+ * - Media type: Category filter (image, video, audio, document, other)
266
+ * - Tags: Assets must contain ALL specified tags
267
+ * - Search: Full-text search on filename, title, description
268
+ *
269
+ * Supports sorting by:
270
+ * - _creationTime (default, descending)
271
+ * - filename
272
+ * - size
273
+ * - type
274
+ * - mimeType
275
+ *
276
+ * @param folderId - Filter to assets in this folder (optional)
277
+ * @param includeRootLevel - Include assets without a folder (default: false with folderId, true without)
278
+ * @param type - Filter by media type category
279
+ * @param mimeType - Filter by exact MIME type
280
+ * @param mimeTypePrefix - Filter by MIME type prefix (e.g., "image/")
281
+ * @param search - Full-text search term
282
+ * @param tags - Filter by tags (must match ALL)
283
+ * @param includeDeleted - Include soft-deleted assets (default: false)
284
+ * @param sortField - Field to sort by (default: "_creationTime")
285
+ * @param sortDirection - Sort direction (default: "desc")
286
+ * @param paginationOpts - Pagination options with numItems and cursor
287
+ * @returns Paginated list of media assets with URLs and optimization hints
288
+ *
289
+ * @example
290
+ * ```typescript
291
+ * // List all images in a specific folder
292
+ * const result = await ctx.runQuery(api.mediaAssets.list, {
293
+ * folderId: folderId,
294
+ * type: "image",
295
+ * paginationOpts: { numItems: 20 }
296
+ * });
297
+ *
298
+ * // Search for assets by name
299
+ * const searchResult = await ctx.runQuery(api.mediaAssets.list, {
300
+ * search: "hero banner",
301
+ * mimeTypePrefix: "image/",
302
+ * paginationOpts: { numItems: 10 }
303
+ * });
304
+ *
305
+ * // List all PDFs sorted by size
306
+ * const pdfs = await ctx.runQuery(api.mediaAssets.list, {
307
+ * mimeType: "application/pdf",
308
+ * sortField: "size",
309
+ * sortDirection: "desc",
310
+ * paginationOpts: { numItems: 50 }
311
+ * });
312
+ *
313
+ * // Paginate through results
314
+ * const nextPage = await ctx.runQuery(api.mediaAssets.list, {
315
+ * paginationOpts: {
316
+ * numItems: 20,
317
+ * cursor: result.continueCursor
318
+ * }
319
+ * });
320
+ * ```
321
+ */
322
+ export const list = query({
323
+ args: listMediaAssetsArgs.fields,
324
+ returns: paginatedMediaAssetsResponse,
325
+ handler: async (ctx, args) => {
326
+ const {
327
+ folderId,
328
+ includeRootLevel,
329
+ type,
330
+ mimeType,
331
+ mimeTypePrefix,
332
+ search,
333
+ tags,
334
+ includeDeleted = false,
335
+ deletedOnly = false,
336
+ sortField = "_creationTime",
337
+ sortDirection = "desc",
338
+ paginationOpts,
339
+ } = args;
340
+
341
+ // Map folderId to parentId for the unified mediaItems table
342
+ const parentId = folderId as Id<"mediaItems"> | undefined;
343
+
344
+ // Clamp numItems to valid range
345
+ const numItems = Math.min(
346
+ Math.max(1, paginationOpts.numItems ?? DEFAULT_NUM_ITEMS),
347
+ MAX_NUM_ITEMS
348
+ );
349
+
350
+ const clampedPaginationOpts = {
351
+ ...paginationOpts,
352
+ numItems,
353
+ };
354
+
355
+ const sortOptions: MediaSortOptions = {
356
+ sortField,
357
+ sortDirection,
358
+ };
359
+
360
+ // Handle full-text search queries
361
+ if (search && search.trim().length > 0) {
362
+ return handleSearchQuery(ctx, {
363
+ search: search.trim(),
364
+ parentId,
365
+ includeRootLevel,
366
+ type,
367
+ mimeType,
368
+ mimeTypePrefix,
369
+ tags,
370
+ includeDeleted,
371
+ deletedOnly,
372
+ sortOptions,
373
+ paginationOpts: clampedPaginationOpts,
374
+ });
375
+ }
376
+
377
+ // Handle standard index-based queries
378
+ return handleIndexQuery(ctx, {
379
+ parentId,
380
+ includeRootLevel,
381
+ type,
382
+ mimeType,
383
+ mimeTypePrefix,
384
+ tags,
385
+ includeDeleted,
386
+ deletedOnly,
387
+ sortOptions,
388
+ paginationOpts: clampedPaginationOpts,
389
+ });
390
+ },
391
+ });
392
+
393
+ // Type for pagination options (using proper Convex pagination type)
394
+ type PaginationOpts = Infer<typeof paginationOptsValidator>;
395
+
396
+ // Type for pagination result
397
+ interface MediaAssetPaginationResult {
398
+ page: any[];
399
+ continueCursor: string | null;
400
+ isDone: boolean;
401
+ }
402
+
403
+ /**
404
+ * Get a sortable value from an asset based on the sort field.
405
+ */
406
+ function getSortValue(asset: any, sortField: MediaSortField): unknown {
407
+ return asset[sortField];
408
+ }
409
+
410
+ /**
411
+ * Compare two values for sorting.
412
+ * Handles null/undefined by pushing them to the end.
413
+ */
414
+ function compareValues(a: unknown, b: unknown, direction: MediaSortDirection): number {
415
+ // Handle null/undefined - push them to the end
416
+ if (a === null || a === undefined) {
417
+ return direction === "asc" ? 1 : -1;
418
+ }
419
+ if (b === null || b === undefined) {
420
+ return direction === "asc" ? -1 : 1;
421
+ }
422
+
423
+ // Compare numbers
424
+ if (typeof a === "number" && typeof b === "number") {
425
+ return direction === "asc" ? a - b : b - a;
426
+ }
427
+
428
+ // Compare strings (case-insensitive)
429
+ if (typeof a === "string" && typeof b === "string") {
430
+ const comparison = a.toLowerCase().localeCompare(b.toLowerCase());
431
+ return direction === "asc" ? comparison : -comparison;
432
+ }
433
+
434
+ // Fallback: convert to string and compare
435
+ const aStr = String(a);
436
+ const bStr = String(b);
437
+ const comparison = aStr.localeCompare(bStr);
438
+ return direction === "asc" ? comparison : -comparison;
439
+ }
440
+
441
+ /**
442
+ * Sort an array of assets by the specified sort options.
443
+ */
444
+ function sortAssets(assets: any[], sortOptions: MediaSortOptions): any[] {
445
+ return [...assets].sort((a, b) => {
446
+ const aValue = getSortValue(a, sortOptions.sortField);
447
+ const bValue = getSortValue(b, sortOptions.sortField);
448
+ return compareValues(aValue, bValue, sortOptions.sortDirection);
449
+ });
450
+ }
451
+
452
+ /**
453
+ * Check if an asset matches the MIME type filter criteria.
454
+ */
455
+ function matchesMimeType(
456
+ asset: { mimeType: string },
457
+ mimeType?: string,
458
+ mimeTypePrefix?: string
459
+ ): boolean {
460
+ if (mimeType && asset.mimeType !== mimeType) {
461
+ return false;
462
+ }
463
+ if (mimeTypePrefix && !asset.mimeType.startsWith(mimeTypePrefix)) {
464
+ return false;
465
+ }
466
+ return true;
467
+ }
468
+
469
+ /**
470
+ * Check if an asset has all the specified tags.
471
+ */
472
+ function matchesTags(asset: { tags?: string[] }, requiredTags?: string[]): boolean {
473
+ if (!requiredTags || requiredTags.length === 0) {
474
+ return true;
475
+ }
476
+ if (!asset.tags || asset.tags.length === 0) {
477
+ return false;
478
+ }
479
+ return requiredTags.every((tag) => asset.tags!.includes(tag));
480
+ }
481
+
482
+ /**
483
+ * Enrich an asset with URL and optimization hints.
484
+ */
485
+ async function enrichAsset(
486
+ ctx: QueryCtx,
487
+ asset: Doc<"mediaItems"> & { kind: "asset" }
488
+ ): Promise<Doc<"mediaItems"> & { kind: "asset"; url: string | null; optimizationHints: ReturnType<typeof buildOptimizationHints> }> {
489
+ const url = await ctx.storage.getUrl(asset.storageId);
490
+ const optimizationHints = buildOptimizationHints(asset);
491
+ return {
492
+ ...asset,
493
+ url,
494
+ optimizationHints,
495
+ };
496
+ }
497
+
498
+ /**
499
+ * Internal helper to handle full-text search queries.
500
+ * Uses the search_assets search index for efficient text matching.
501
+ */
502
+ async function handleSearchQuery(
503
+ ctx: QueryCtx,
504
+ args: {
505
+ search: string;
506
+ parentId?: Id<"mediaItems">;
507
+ includeRootLevel?: boolean;
508
+ type?: string;
509
+ mimeType?: string;
510
+ mimeTypePrefix?: string;
511
+ tags?: string[];
512
+ includeDeleted: boolean;
513
+ deletedOnly: boolean;
514
+ sortOptions: MediaSortOptions;
515
+ paginationOpts: PaginationOpts;
516
+ }
517
+ ): Promise<MediaAssetPaginationResult> {
518
+ const {
519
+ search,
520
+ parentId,
521
+ includeRootLevel,
522
+ type,
523
+ mimeType,
524
+ mimeTypePrefix,
525
+ tags,
526
+ includeDeleted,
527
+ deletedOnly,
528
+ sortOptions,
529
+ paginationOpts,
530
+ } = args;
531
+ const { numItems, cursor } = paginationOpts;
532
+
533
+ // Build search query with filter fields available in the index
534
+ // The search_media index supports filtering by kind, type, and parentId
535
+ const searchQuery = ctx.db
536
+ .query("mediaItems")
537
+ .withSearchIndex("search_media", (q: any) => {
538
+ let query = q.search("searchText", search);
539
+
540
+ // Always filter for assets only
541
+ query = query.eq("kind", "asset");
542
+
543
+ // Apply filter fields available in the search index
544
+ if (type) {
545
+ query = query.eq("type", type);
546
+ }
547
+ if (parentId) {
548
+ query = query.eq("parentId", parentId);
549
+ }
550
+
551
+ return query;
552
+ });
553
+
554
+ // Fetch more results for post-filtering
555
+ const hasMimeFilter = mimeType || mimeTypePrefix;
556
+ const hasTagFilter = tags && tags.length > 0;
557
+ const fetchMultiplier = !includeDeleted || hasMimeFilter || hasTagFilter ? 4 : 1;
558
+ const results = await searchQuery.take((numItems + 1) * fetchMultiplier);
559
+
560
+ // Apply post-processing filters
561
+ let filteredResults = results;
562
+
563
+ // Filter by soft-delete status
564
+ if (deletedOnly) {
565
+ // Only show deleted items
566
+ filteredResults = filteredResults.filter((asset: any) => isDeleted(asset));
567
+ } else if (!includeDeleted) {
568
+ // Exclude deleted items
569
+ filteredResults = filteredResults.filter(
570
+ (asset: any) => !isDeleted(asset)
571
+ );
572
+ }
573
+
574
+ // Filter by folder (for root level handling)
575
+ if (parentId && includeRootLevel) {
576
+ // Show both folder assets and root-level assets
577
+ filteredResults = filteredResults.filter(
578
+ (asset: any) => asset.parentId === parentId || asset.parentId === undefined
579
+ );
580
+ } else if (!parentId && includeRootLevel === false) {
581
+ // Explicitly exclude root-level assets when not filtering by folder
582
+ filteredResults = filteredResults.filter(
583
+ (asset: any) => asset.parentId !== undefined
584
+ );
585
+ }
586
+ // Default (no parentId, includeRootLevel undefined/true): show all assets
587
+
588
+ // Filter by MIME type
589
+ if (hasMimeFilter) {
590
+ filteredResults = filteredResults.filter((asset: any) =>
591
+ matchesMimeType(asset, mimeType, mimeTypePrefix)
592
+ );
593
+ }
594
+
595
+ // Filter by tags
596
+ if (hasTagFilter) {
597
+ filteredResults = filteredResults.filter((asset: any) =>
598
+ matchesTags(asset, tags)
599
+ );
600
+ }
601
+
602
+ // Apply sorting
603
+ const sortedResults = sortAssets(filteredResults, sortOptions);
604
+
605
+ // Handle cursor-based pagination
606
+ let startIndex = 0;
607
+ if (cursor) {
608
+ const cursorIndex = sortedResults.findIndex(
609
+ (asset: any) => asset._id === cursor
610
+ );
611
+ if (cursorIndex !== -1) {
612
+ startIndex = cursorIndex + 1;
613
+ }
614
+ }
615
+
616
+ // Get the page of results
617
+ const pageResults = sortedResults.slice(startIndex, startIndex + numItems + 1);
618
+ const isDone = pageResults.length <= numItems;
619
+ const page = isDone ? pageResults : pageResults.slice(0, numItems);
620
+
621
+ // Enrich assets with URLs and optimization hints
622
+ const enrichedPage = await Promise.all(
623
+ page.map((asset: any) => enrichAsset(ctx, asset))
624
+ );
625
+
626
+ // Get continuation cursor
627
+ const continueCursor =
628
+ !isDone && page.length > 0 ? page[page.length - 1]._id : null;
629
+
630
+ return {
631
+ page: enrichedPage,
632
+ continueCursor,
633
+ isDone,
634
+ };
635
+ }
636
+
637
+ /**
638
+ * Internal helper to handle index-based queries using convex-helpers stream.
639
+ * Selects the optimal index based on provided filters.
640
+ */
641
+ // =============================================================================
642
+ // Count Media Assets Query
643
+ // =============================================================================
644
+
645
+ /**
646
+ * Query to count media assets.
647
+ * Efficiently counts all assets without pagination limits.
648
+ *
649
+ * @param folderId - Filter to assets in this folder (optional)
650
+ * @param mimeType - Filter by exact MIME type (optional)
651
+ * @param mimeTypePrefix - Filter by MIME type prefix e.g. "image/" (optional)
652
+ * @param includeDeleted - Include soft-deleted assets (default: false)
653
+ * @param deletedOnly - Count only deleted assets (default: false)
654
+ * @returns Object with count property
655
+ *
656
+ * @example
657
+ * ```typescript
658
+ * // Count all active media assets
659
+ * const result = await ctx.runQuery(api.mediaAssets.count, {});
660
+ *
661
+ * // Count images only
662
+ * const imageCount = await ctx.runQuery(api.mediaAssets.count, {
663
+ * mimeTypePrefix: "image/",
664
+ * });
665
+ * ```
666
+ */
667
+ export const count = query({
668
+ args: {
669
+ folderId: v.optional(v.id("mediaItems")),
670
+ mimeType: v.optional(v.string()),
671
+ mimeTypePrefix: v.optional(v.string()),
672
+ includeDeleted: v.optional(v.boolean()),
673
+ deletedOnly: v.optional(v.boolean()),
674
+ },
675
+ returns: v.object({
676
+ count: v.number(),
677
+ }),
678
+ handler: async (ctx, args) => {
679
+ const {
680
+ folderId,
681
+ mimeType,
682
+ mimeTypePrefix,
683
+ includeDeleted = false,
684
+ deletedOnly = false,
685
+ } = args;
686
+
687
+ const parentId = folderId as Id<"mediaItems"> | undefined;
688
+
689
+ // Select optimal index based on filters
690
+ let queryBuilder;
691
+ if (deletedOnly) {
692
+ queryBuilder = ctx.db
693
+ .query("mediaItems")
694
+ .withIndex("by_deleted", (q) => q.gte("deletedAt", null as unknown as number));
695
+ } else if (mimeType) {
696
+ queryBuilder = ctx.db
697
+ .query("mediaItems")
698
+ .withIndex("by_mime_type", (q) => q.eq("mimeType", mimeType));
699
+ } else if (parentId) {
700
+ queryBuilder = ctx.db
701
+ .query("mediaItems")
702
+ .withIndex("by_kind_and_parent", (q) =>
703
+ q.eq("kind", "asset").eq("parentId", parentId)
704
+ );
705
+ } else {
706
+ queryBuilder = ctx.db
707
+ .query("mediaItems")
708
+ .withIndex("by_kind", (q) => q.eq("kind", "asset"));
709
+ }
710
+
711
+ let count = 0;
712
+ for await (const item of queryBuilder) {
713
+ if (item.kind !== "asset") continue;
714
+
715
+ // Exclude soft-deleted items by default
716
+ if (!deletedOnly && !includeDeleted && isDeleted(item)) {
717
+ continue;
718
+ }
719
+
720
+ if (parentId && item.parentId !== parentId) continue;
721
+ if (mimeTypePrefix && !item.mimeType.startsWith(mimeTypePrefix)) continue;
722
+ count++;
723
+ }
724
+
725
+ return { count };
726
+ },
727
+ });
728
+
729
+ // =============================================================================
730
+ // List Media Assets Helper Functions
731
+ // =============================================================================
732
+
733
+ async function handleIndexQuery(
734
+ ctx: QueryCtx,
735
+ args: {
736
+ parentId?: Id<"mediaItems">;
737
+ includeRootLevel?: boolean;
738
+ type?: string;
739
+ mimeType?: string;
740
+ mimeTypePrefix?: string;
741
+ tags?: string[];
742
+ includeDeleted: boolean;
743
+ deletedOnly: boolean;
744
+ sortOptions: MediaSortOptions;
745
+ paginationOpts: PaginationOpts;
746
+ }
747
+ ): Promise<MediaAssetPaginationResult> {
748
+ const {
749
+ parentId,
750
+ includeRootLevel,
751
+ type,
752
+ mimeType,
753
+ mimeTypePrefix,
754
+ tags,
755
+ includeDeleted,
756
+ deletedOnly,
757
+ sortOptions,
758
+ paginationOpts,
759
+ } = args;
760
+ const { numItems, cursor } = paginationOpts;
761
+
762
+ // Determine if we need in-memory sorting
763
+ const needsCustomSort = sortOptions.sortField !== "_creationTime";
764
+
765
+ // Check for post-processing filters
766
+ const hasMimeFilter = mimeType || mimeTypePrefix;
767
+ const hasTagFilter = tags && tags.length > 0;
768
+ const hasFolderLogic = parentId || includeRootLevel !== undefined;
769
+ const hasTypeFilter = !!type;
770
+ const needsPostFiltering = !includeDeleted || hasMimeFilter || hasTagFilter || hasTypeFilter;
771
+
772
+ // Create the stream-based query
773
+ const streamDb = stream(ctx.db, schema);
774
+
775
+ // Select the best index based on filters
776
+ // Always need to filter for kind: "asset"
777
+ // Note: `type` filtering is now done post-query since type is derived from mimeType
778
+ let baseQuery;
779
+ if (deletedOnly) {
780
+ // Use by_deleted index to efficiently query only deleted items
781
+ // q.gte("deletedAt", null) matches all items where deletedAt has a timestamp value
782
+ // In Convex type ordering: undefined < null < numbers, so gte(null) gets all timestamps
783
+ baseQuery = streamDb
784
+ .query("mediaItems")
785
+ .withIndex("by_deleted", (q: any) => q.gte("deletedAt", null as any));
786
+ } else if (mimeType) {
787
+ // Use by_mime_type index for exact MIME type match (assets only)
788
+ baseQuery = streamDb
789
+ .query("mediaItems")
790
+ .withIndex("by_mime_type", (q: any) => q.eq("mimeType", mimeType));
791
+ } else if (parentId) {
792
+ // Use by_kind_and_parent index when filtering by folder
793
+ baseQuery = streamDb
794
+ .query("mediaItems")
795
+ .withIndex("by_kind_and_parent", (q: any) => q.eq("kind", "asset").eq("parentId", parentId));
796
+ } else {
797
+ // Default: use by_kind index to get only assets
798
+ baseQuery = streamDb
799
+ .query("mediaItems")
800
+ .withIndex("by_kind", (q: any) => q.eq("kind", "asset"));
801
+ }
802
+
803
+ // Apply order for index-based sorting
804
+ const order = sortOptions.sortDirection;
805
+ const orderedQuery = baseQuery.order(order);
806
+
807
+ // Apply filterWith for post-processing filters
808
+ const filteredQuery = orderedQuery.filterWith(async (item: any) => {
809
+ // Ensure this is an asset (required since by_deleted index doesn't filter by kind)
810
+ if (item.kind !== "asset") {
811
+ return false;
812
+ }
813
+
814
+ // Filter by soft-delete status
815
+ // When deletedOnly is true, we're using the by_deleted index which already
816
+ // filters to only items with deletedAt defined - no additional check needed
817
+ if (!deletedOnly && !includeDeleted && isDeleted(item)) {
818
+ // Exclude deleted items unless includeDeleted is true or deletedOnly is true
819
+ return false;
820
+ }
821
+
822
+ // Filter by folder logic (when not already using folder index)
823
+ if (!parentId && hasFolderLogic) {
824
+ if (includeRootLevel === true && item.parentId !== undefined) {
825
+ return false; // Only show root-level assets
826
+ }
827
+ if (includeRootLevel === false && item.parentId === undefined) {
828
+ return false; // Exclude root-level assets
829
+ }
830
+ }
831
+
832
+ // Filter by MIME type (when not already using mimeType index)
833
+ if (hasMimeFilter && !mimeType) {
834
+ if (!matchesMimeType(item, undefined, mimeTypePrefix)) {
835
+ return false;
836
+ }
837
+ }
838
+
839
+ // Filter by tags
840
+ if (hasTagFilter && !matchesTags(item, tags)) {
841
+ return false;
842
+ }
843
+
844
+ // Filter by media type category (type is derived from mimeType)
845
+ if (type) {
846
+ // Map type to mimeType prefix for filtering
847
+ const mimePrefix = type === "other" ? null : `${type}/`;
848
+ if (mimePrefix && !item.mimeType.startsWith(mimePrefix)) {
849
+ return false;
850
+ }
851
+ // For "other" type, exclude known media types
852
+ if (type === "other" && (
853
+ item.mimeType.startsWith("image/") ||
854
+ item.mimeType.startsWith("video/") ||
855
+ item.mimeType.startsWith("audio/") ||
856
+ item.mimeType.startsWith("application/pdf") ||
857
+ item.mimeType.includes("document") ||
858
+ item.mimeType.includes("sheet") ||
859
+ item.mimeType.includes("presentation")
860
+ )) {
861
+ return false;
862
+ }
863
+ }
864
+
865
+ return true;
866
+ });
867
+
868
+ // For custom sorting, we need to fetch all and sort in memory
869
+ if (needsCustomSort) {
870
+ // Fetch more results for in-memory sorting and filtering
871
+ const fetchMultiplier = needsPostFiltering ? 10 : 5;
872
+ const results: any[] = [];
873
+ let fetchedCount = 0;
874
+ const maxFetch = (numItems + 1) * fetchMultiplier;
875
+
876
+ // Manual iteration with filterWith
877
+ for await (const asset of filteredQuery) {
878
+ results.push(asset);
879
+ fetchedCount++;
880
+ if (fetchedCount >= maxFetch) {
881
+ break;
882
+ }
883
+ }
884
+
885
+ // Apply custom sorting
886
+ const sortedResults = sortAssets(results, sortOptions);
887
+
888
+ // Handle cursor-based pagination
889
+ let startIndex = 0;
890
+ if (cursor) {
891
+ const cursorIndex = sortedResults.findIndex(
892
+ (asset: any) => asset._id === cursor
893
+ );
894
+ if (cursorIndex !== -1) {
895
+ startIndex = cursorIndex + 1;
896
+ }
897
+ }
898
+
899
+ // Get the page of results
900
+ const pageResults = sortedResults.slice(startIndex, startIndex + numItems + 1);
901
+ const isDone = pageResults.length <= numItems;
902
+ const page = isDone ? pageResults : pageResults.slice(0, numItems);
903
+
904
+ // Enrich assets with URLs and optimization hints
905
+ const enrichedPage = await Promise.all(
906
+ page.map((asset: any) => enrichAsset(ctx, asset))
907
+ );
908
+
909
+ const continueCursor =
910
+ !isDone && page.length > 0 ? page[page.length - 1]._id : null;
911
+
912
+ return {
913
+ page: enrichedPage,
914
+ continueCursor,
915
+ isDone,
916
+ };
917
+ }
918
+
919
+ // For default sorting (_creationTime), use paginator
920
+ const result = await filteredQuery.paginate(paginationOpts);
921
+
922
+ // Enrich assets with URLs and optimization hints
923
+ const enrichedPage = await Promise.all(
924
+ result.page.map((asset: any) => enrichAsset(ctx, asset))
925
+ );
926
+
927
+ return {
928
+ page: enrichedPage,
929
+ continueCursor: result.continueCursor,
930
+ isDone: result.isDone,
931
+ };
932
+ }