convex-cms 0.0.2 → 0.0.3

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 (265) hide show
  1. package/admin-dist/nitro.json +15 -0
  2. package/admin-dist/public/assets/CmsEmptyState-CRswfTzk.js +5 -0
  3. package/admin-dist/public/assets/CmsPageHeader-CirpXndm.js +1 -0
  4. package/admin-dist/public/assets/CmsStatusBadge-CbEUpQu-.js +1 -0
  5. package/admin-dist/public/assets/CmsToolbar-BI2nZOXp.js +1 -0
  6. package/admin-dist/public/assets/ContentEntryEditor-CBeCyK_m.js +4 -0
  7. package/admin-dist/public/assets/ErrorState-BIVaWmom.js +1 -0
  8. package/admin-dist/public/assets/TaxonomyFilter-ChaY6Y_x.js +1 -0
  9. package/admin-dist/public/assets/_contentTypeId-DQ8k_Rvw.js +1 -0
  10. package/admin-dist/public/assets/_entryId-CKU_glsK.js +1 -0
  11. package/admin-dist/public/assets/alert-BXjTqrwQ.js +1 -0
  12. package/admin-dist/public/assets/badge-hvUOzpVZ.js +1 -0
  13. package/admin-dist/public/assets/circle-check-big-CF_pR17r.js +1 -0
  14. package/admin-dist/public/assets/command-DU82cJlt.js +1 -0
  15. package/admin-dist/public/assets/content-_LXl3pp7.js +1 -0
  16. package/admin-dist/public/assets/content-types-KjxaXGxY.js +2 -0
  17. package/admin-dist/public/assets/globals-CS6BZ0zp.css +1 -0
  18. package/admin-dist/public/assets/index-DNGIZHL-.js +1 -0
  19. package/admin-dist/public/assets/label-KNtpL71g.js +1 -0
  20. package/admin-dist/public/assets/link-2-Bw2aI4V4.js +1 -0
  21. package/admin-dist/public/assets/list-sYepHjt_.js +1 -0
  22. package/admin-dist/public/assets/main-CKj5yfEi.js +97 -0
  23. package/admin-dist/public/assets/media-Bkrkffm7.js +1 -0
  24. package/admin-dist/public/assets/new._contentTypeId-C3LstjNs.js +1 -0
  25. package/admin-dist/public/assets/plus-DUn8v_Xf.js +1 -0
  26. package/admin-dist/public/assets/rotate-ccw-DJEoHcRI.js +1 -0
  27. package/admin-dist/public/assets/scroll-area-DfIlT0in.js +1 -0
  28. package/admin-dist/public/assets/search-MuAUDJKR.js +1 -0
  29. package/admin-dist/public/assets/select-BD29IXCI.js +1 -0
  30. package/admin-dist/public/assets/settings-DmMyn_6A.js +1 -0
  31. package/admin-dist/public/assets/switch-h3Rrnl5i.js +1 -0
  32. package/admin-dist/public/assets/tabs-imc8h-Dp.js +1 -0
  33. package/admin-dist/public/assets/taxonomies-dAsrT65H.js +1 -0
  34. package/admin-dist/public/assets/textarea-BTy7nwzR.js +1 -0
  35. package/admin-dist/public/assets/trash-SAWKZZHv.js +1 -0
  36. package/admin-dist/public/assets/triangle-alert-E52Vfeuh.js +1 -0
  37. package/admin-dist/public/assets/useBreadcrumbLabel-BECBMCzM.js +1 -0
  38. package/admin-dist/public/assets/usePermissions-Basjs9BT.js +1 -0
  39. package/admin-dist/public/favicon.ico +0 -0
  40. package/admin-dist/server/_chunks/_libs/@date-fns/tz.mjs +217 -0
  41. package/admin-dist/server/_chunks/_libs/@floating-ui/core.mjs +719 -0
  42. package/admin-dist/server/_chunks/_libs/@floating-ui/dom.mjs +622 -0
  43. package/admin-dist/server/_chunks/_libs/@floating-ui/react-dom.mjs +292 -0
  44. package/admin-dist/server/_chunks/_libs/@floating-ui/utils.mjs +320 -0
  45. package/admin-dist/server/_chunks/_libs/@radix-ui/number.mjs +6 -0
  46. package/admin-dist/server/_chunks/_libs/@radix-ui/primitive.mjs +11 -0
  47. package/admin-dist/server/_chunks/_libs/@radix-ui/react-arrow.mjs +23 -0
  48. package/admin-dist/server/_chunks/_libs/@radix-ui/react-avatar.mjs +119 -0
  49. package/admin-dist/server/_chunks/_libs/@radix-ui/react-checkbox.mjs +270 -0
  50. package/admin-dist/server/_chunks/_libs/@radix-ui/react-collection.mjs +69 -0
  51. package/admin-dist/server/_chunks/_libs/@radix-ui/react-compose-refs.mjs +39 -0
  52. package/admin-dist/server/_chunks/_libs/@radix-ui/react-context.mjs +137 -0
  53. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dialog.mjs +325 -0
  54. package/admin-dist/server/_chunks/_libs/@radix-ui/react-direction.mjs +9 -0
  55. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dismissable-layer.mjs +210 -0
  56. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dropdown-menu.mjs +253 -0
  57. package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-guards.mjs +29 -0
  58. package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-scope.mjs +206 -0
  59. package/admin-dist/server/_chunks/_libs/@radix-ui/react-id.mjs +14 -0
  60. package/admin-dist/server/_chunks/_libs/@radix-ui/react-label.mjs +23 -0
  61. package/admin-dist/server/_chunks/_libs/@radix-ui/react-menu.mjs +812 -0
  62. package/admin-dist/server/_chunks/_libs/@radix-ui/react-popover.mjs +300 -0
  63. package/admin-dist/server/_chunks/_libs/@radix-ui/react-popper.mjs +286 -0
  64. package/admin-dist/server/_chunks/_libs/@radix-ui/react-portal.mjs +16 -0
  65. package/admin-dist/server/_chunks/_libs/@radix-ui/react-presence.mjs +128 -0
  66. package/admin-dist/server/_chunks/_libs/@radix-ui/react-primitive.mjs +141 -0
  67. package/admin-dist/server/_chunks/_libs/@radix-ui/react-roving-focus.mjs +224 -0
  68. package/admin-dist/server/_chunks/_libs/@radix-ui/react-scroll-area.mjs +721 -0
  69. package/admin-dist/server/_chunks/_libs/@radix-ui/react-select.mjs +1163 -0
  70. package/admin-dist/server/_chunks/_libs/@radix-ui/react-separator.mjs +28 -0
  71. package/admin-dist/server/_chunks/_libs/@radix-ui/react-slot.mjs +601 -0
  72. package/admin-dist/server/_chunks/_libs/@radix-ui/react-switch.mjs +152 -0
  73. package/admin-dist/server/_chunks/_libs/@radix-ui/react-tabs.mjs +189 -0
  74. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-callback-ref.mjs +11 -0
  75. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-controllable-state.mjs +69 -0
  76. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-effect-event.mjs +1 -0
  77. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-escape-keydown.mjs +17 -0
  78. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-is-hydrated.mjs +15 -0
  79. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-layout-effect.mjs +6 -0
  80. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-previous.mjs +14 -0
  81. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-size.mjs +39 -0
  82. package/admin-dist/server/_chunks/_libs/@radix-ui/react-visually-hidden.mjs +33 -0
  83. package/admin-dist/server/_chunks/_libs/@tanstack/history.mjs +409 -0
  84. package/admin-dist/server/_chunks/_libs/@tanstack/react-router.mjs +1711 -0
  85. package/admin-dist/server/_chunks/_libs/@tanstack/react-store.mjs +56 -0
  86. package/admin-dist/server/_chunks/_libs/@tanstack/router-core.mjs +4829 -0
  87. package/admin-dist/server/_chunks/_libs/@tanstack/store.mjs +134 -0
  88. package/admin-dist/server/_chunks/_libs/react-dom.mjs +10781 -0
  89. package/admin-dist/server/_chunks/_libs/react.mjs +513 -0
  90. package/admin-dist/server/_libs/aria-hidden.mjs +122 -0
  91. package/admin-dist/server/_libs/class-variance-authority.mjs +44 -0
  92. package/admin-dist/server/_libs/clsx.mjs +16 -0
  93. package/admin-dist/server/_libs/cmdk.mjs +315 -0
  94. package/admin-dist/server/_libs/convex.mjs +4841 -0
  95. package/admin-dist/server/_libs/cookie-es.mjs +58 -0
  96. package/admin-dist/server/_libs/croner.mjs +1 -0
  97. package/admin-dist/server/_libs/crossws.mjs +1 -0
  98. package/admin-dist/server/_libs/date-fns.mjs +1716 -0
  99. package/admin-dist/server/_libs/detect-node-es.mjs +1 -0
  100. package/admin-dist/server/_libs/get-nonce.mjs +9 -0
  101. package/admin-dist/server/_libs/h3-v2.mjs +277 -0
  102. package/admin-dist/server/_libs/h3.mjs +401 -0
  103. package/admin-dist/server/_libs/hookable.mjs +1 -0
  104. package/admin-dist/server/_libs/isbot.mjs +20 -0
  105. package/admin-dist/server/_libs/lucide-react.mjs +850 -0
  106. package/admin-dist/server/_libs/ohash.mjs +1 -0
  107. package/admin-dist/server/_libs/react-day-picker.mjs +2201 -0
  108. package/admin-dist/server/_libs/react-remove-scroll-bar.mjs +82 -0
  109. package/admin-dist/server/_libs/react-remove-scroll.mjs +328 -0
  110. package/admin-dist/server/_libs/react-style-singleton.mjs +69 -0
  111. package/admin-dist/server/_libs/rou3.mjs +8 -0
  112. package/admin-dist/server/_libs/seroval-plugins.mjs +58 -0
  113. package/admin-dist/server/_libs/seroval.mjs +1765 -0
  114. package/admin-dist/server/_libs/srvx.mjs +719 -0
  115. package/admin-dist/server/_libs/tailwind-merge.mjs +3010 -0
  116. package/admin-dist/server/_libs/tiny-invariant.mjs +12 -0
  117. package/admin-dist/server/_libs/tiny-warning.mjs +5 -0
  118. package/admin-dist/server/_libs/tslib.mjs +39 -0
  119. package/admin-dist/server/_libs/ufo.mjs +54 -0
  120. package/admin-dist/server/_libs/unctx.mjs +1 -0
  121. package/admin-dist/server/_libs/unstorage.mjs +1 -0
  122. package/admin-dist/server/_libs/use-callback-ref.mjs +66 -0
  123. package/admin-dist/server/_libs/use-sidecar.mjs +106 -0
  124. package/admin-dist/server/_libs/use-sync-external-store.mjs +139 -0
  125. package/admin-dist/server/_libs/zod.mjs +4223 -0
  126. package/admin-dist/server/_ssr/CmsEmptyState-DU7-7-mV.mjs +290 -0
  127. package/admin-dist/server/_ssr/CmsPageHeader-CseW0AHm.mjs +24 -0
  128. package/admin-dist/server/_ssr/CmsStatusBadge-B_pi4KCp.mjs +127 -0
  129. package/admin-dist/server/_ssr/CmsToolbar-X75ex6ek.mjs +49 -0
  130. package/admin-dist/server/_ssr/ContentEntryEditor-CepusRsA.mjs +3720 -0
  131. package/admin-dist/server/_ssr/ErrorState-cI-bKLez.mjs +89 -0
  132. package/admin-dist/server/_ssr/TaxonomyFilter-Bwrq0-cz.mjs +188 -0
  133. package/admin-dist/server/_ssr/_contentTypeId-BqYKEcLr.mjs +379 -0
  134. package/admin-dist/server/_ssr/_entryId-CRfnqeDf.mjs +161 -0
  135. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-BwDlABVk.mjs +4 -0
  136. package/admin-dist/server/_ssr/alert-CVt45UUP.mjs +92 -0
  137. package/admin-dist/server/_ssr/badge-6BsP37vG.mjs +125 -0
  138. package/admin-dist/server/_ssr/command-fy8epIKf.mjs +128 -0
  139. package/admin-dist/server/_ssr/config.server-D7JHDcDv.mjs +117 -0
  140. package/admin-dist/server/_ssr/content-B5RhL7uW.mjs +532 -0
  141. package/admin-dist/server/_ssr/content-types-BIOqCQYN.mjs +1166 -0
  142. package/admin-dist/server/_ssr/index-DHSHDPt1.mjs +193 -0
  143. package/admin-dist/server/_ssr/index.mjs +1275 -0
  144. package/admin-dist/server/_ssr/label-C8Dko1j7.mjs +22 -0
  145. package/admin-dist/server/_ssr/media-CSx3XttC.mjs +1832 -0
  146. package/admin-dist/server/_ssr/new._contentTypeId-DzanEZQM.mjs +144 -0
  147. package/admin-dist/server/_ssr/router-DDWcF-kt.mjs +1556 -0
  148. package/admin-dist/server/_ssr/scroll-area-bjPYwhXN.mjs +59 -0
  149. package/admin-dist/server/_ssr/select-BUhDDf4T.mjs +142 -0
  150. package/admin-dist/server/_ssr/settings-DAsxnw2q.mjs +348 -0
  151. package/admin-dist/server/_ssr/start-HYkvq4Ni.mjs +4 -0
  152. package/admin-dist/server/_ssr/switch-BgyRtQ1Z.mjs +31 -0
  153. package/admin-dist/server/_ssr/tabs-DzMdRB1A.mjs +628 -0
  154. package/admin-dist/server/_ssr/taxonomies-C8j8g5Q5.mjs +915 -0
  155. package/admin-dist/server/_ssr/textarea-9jNeYJSc.mjs +18 -0
  156. package/admin-dist/server/_ssr/trash-DYMxwhZB.mjs +291 -0
  157. package/admin-dist/server/_ssr/useBreadcrumbLabel-FNSAr2Ha.mjs +16 -0
  158. package/admin-dist/server/_ssr/usePermissions-BJGGahrJ.mjs +68 -0
  159. package/admin-dist/server/favicon.ico +0 -0
  160. package/admin-dist/server/index.mjs +627 -0
  161. package/dist/cli/index.js +0 -0
  162. package/dist/client/admin-config.d.ts +0 -1
  163. package/dist/client/admin-config.d.ts.map +1 -1
  164. package/dist/client/admin-config.js +0 -1
  165. package/dist/client/admin-config.js.map +1 -1
  166. package/dist/client/adminApi.d.ts.map +1 -1
  167. package/dist/client/agentTools.d.ts +1237 -135
  168. package/dist/client/agentTools.d.ts.map +1 -1
  169. package/dist/client/agentTools.js +33 -9
  170. package/dist/client/agentTools.js.map +1 -1
  171. package/dist/client/index.d.ts +1 -1
  172. package/dist/client/index.d.ts.map +1 -1
  173. package/dist/client/index.js.map +1 -1
  174. package/dist/component/_generated/component.d.ts +9 -0
  175. package/dist/component/_generated/component.d.ts.map +1 -1
  176. package/dist/component/mediaAssets.d.ts +35 -0
  177. package/dist/component/mediaAssets.d.ts.map +1 -1
  178. package/dist/component/mediaAssets.js +81 -0
  179. package/dist/component/mediaAssets.js.map +1 -1
  180. package/dist/test.d.ts.map +1 -1
  181. package/dist/test.js +2 -1
  182. package/dist/test.js.map +1 -1
  183. package/package.json +9 -5
  184. package/dist/component/auditLog.d.ts +0 -410
  185. package/dist/component/auditLog.d.ts.map +0 -1
  186. package/dist/component/auditLog.js +0 -607
  187. package/dist/component/auditLog.js.map +0 -1
  188. package/dist/component/types.d.ts +0 -4
  189. package/dist/component/types.d.ts.map +0 -1
  190. package/dist/component/types.js +0 -2
  191. package/dist/component/types.js.map +0 -1
  192. package/src/cli/commands/admin.ts +0 -104
  193. package/src/cli/index.ts +0 -21
  194. package/src/cli/utils/detectConvexUrl.ts +0 -54
  195. package/src/cli/utils/openBrowser.ts +0 -16
  196. package/src/client/admin-config.ts +0 -138
  197. package/src/client/adminApi.ts +0 -942
  198. package/src/client/agentTools.ts +0 -1311
  199. package/src/client/argTypes.ts +0 -316
  200. package/src/client/field-types.ts +0 -187
  201. package/src/client/index.ts +0 -1301
  202. package/src/client/queryBuilder.ts +0 -1100
  203. package/src/client/schema/codegen.ts +0 -500
  204. package/src/client/schema/defineContentType.ts +0 -501
  205. package/src/client/schema/index.ts +0 -169
  206. package/src/client/schema/schemaDrift.ts +0 -574
  207. package/src/client/schema/typedClient.ts +0 -688
  208. package/src/client/schema/types.ts +0 -666
  209. package/src/client/types.ts +0 -723
  210. package/src/client/workflows.ts +0 -141
  211. package/src/client/wrapper.ts +0 -4304
  212. package/src/component/_generated/api.ts +0 -140
  213. package/src/component/_generated/component.ts +0 -5029
  214. package/src/component/_generated/dataModel.ts +0 -60
  215. package/src/component/_generated/server.ts +0 -156
  216. package/src/component/authorization.ts +0 -647
  217. package/src/component/authorizationHooks.ts +0 -668
  218. package/src/component/bulkOperations.ts +0 -687
  219. package/src/component/contentEntries.ts +0 -1976
  220. package/src/component/contentEntryMutations.ts +0 -1223
  221. package/src/component/contentEntryValidation.ts +0 -707
  222. package/src/component/contentLock.ts +0 -550
  223. package/src/component/contentTypeMigration.ts +0 -1064
  224. package/src/component/contentTypeMutations.ts +0 -969
  225. package/src/component/contentTypes.ts +0 -346
  226. package/src/component/convex.config.ts +0 -44
  227. package/src/component/documentTypes.ts +0 -240
  228. package/src/component/eventEmitter.ts +0 -485
  229. package/src/component/exportImport.ts +0 -1169
  230. package/src/component/index.ts +0 -491
  231. package/src/component/lib/deepReferenceResolver.ts +0 -999
  232. package/src/component/lib/errors.ts +0 -816
  233. package/src/component/lib/index.ts +0 -145
  234. package/src/component/lib/mediaReferenceResolver.ts +0 -495
  235. package/src/component/lib/metadataExtractor.ts +0 -792
  236. package/src/component/lib/mutationAuth.ts +0 -199
  237. package/src/component/lib/queries.ts +0 -79
  238. package/src/component/lib/ragContentChunker.ts +0 -1371
  239. package/src/component/lib/referenceResolver.ts +0 -430
  240. package/src/component/lib/slugGenerator.ts +0 -262
  241. package/src/component/lib/slugUniqueness.ts +0 -333
  242. package/src/component/lib/softDelete.ts +0 -44
  243. package/src/component/localeFallbackChain.ts +0 -673
  244. package/src/component/localeFields.ts +0 -896
  245. package/src/component/mediaAssetMutations.ts +0 -725
  246. package/src/component/mediaAssets.ts +0 -932
  247. package/src/component/mediaFolderMutations.ts +0 -1046
  248. package/src/component/mediaUploadMutations.ts +0 -224
  249. package/src/component/mediaVariantMutations.ts +0 -900
  250. package/src/component/mediaVariants.ts +0 -793
  251. package/src/component/ragContentIndexer.ts +0 -1067
  252. package/src/component/rateLimitHooks.ts +0 -572
  253. package/src/component/roles.ts +0 -1360
  254. package/src/component/scheduledPublish.ts +0 -358
  255. package/src/component/schema.ts +0 -617
  256. package/src/component/taxonomies.ts +0 -949
  257. package/src/component/taxonomyMutations.ts +0 -1210
  258. package/src/component/trash.ts +0 -724
  259. package/src/component/userContext.ts +0 -898
  260. package/src/component/validation.ts +0 -1388
  261. package/src/component/validators.ts +0 -949
  262. package/src/component/versionMutations.ts +0 -392
  263. package/src/component/webhookTrigger.ts +0 -1922
  264. package/src/react/index.ts +0 -898
  265. package/src/test.ts +0 -1580
@@ -1,949 +0,0 @@
1
- /**
2
- * Taxonomy Query Functions
3
- *
4
- * Provides query functions for retrieving taxonomy definitions and terms.
5
- * Taxonomies are classification systems (like tags, categories, topics) that
6
- * can be applied to content entries for organization and filtering.
7
- *
8
- * Available queries:
9
- * - `get`: Retrieve a single taxonomy by ID or name
10
- * - `list`: List all taxonomies with optional filtering
11
- * - `getTerm`: Retrieve a single term by ID or slug
12
- * - `listTerms`: List terms within a taxonomy with filtering and search
13
- * - `getTermsByEntry`: Get all terms associated with a content entry
14
- * - `getEntriesByTerm`: Get content entries associated with a term
15
- * - `suggestTerms`: Get term suggestions based on partial input
16
- */
17
-
18
- import { v } from "convex/values";
19
- import { isDeleted } from "./lib/softDelete.js";
20
- import { paginationOptsValidator } from "convex/server";
21
- import { query } from "./_generated/server.js";
22
- import { taxonomyDoc, taxonomyTermDoc } from "./validators.js";
23
-
24
- // =============================================================================
25
- // Constants
26
- // =============================================================================
27
-
28
- const DEFAULT_NUM_ITEMS = 50;
29
- const MAX_NUM_ITEMS = 250;
30
-
31
- // =============================================================================
32
- // Extended Validators
33
- // =============================================================================
34
-
35
- /**
36
- * Term with children for hierarchical display.
37
- * Extends the base taxonomyTermDoc with a children array.
38
- */
39
- const taxonomyTermWithChildren: ReturnType<typeof v.object> = v.object({
40
- ...taxonomyTermDoc.fields,
41
- children: v.array(v.any()), // Recursive type - will contain taxonomyTermWithChildren
42
- });
43
-
44
- // =============================================================================
45
- // Get Taxonomy Query
46
- // =============================================================================
47
-
48
- /**
49
- * Query to retrieve a single taxonomy by ID or name.
50
- *
51
- * @param id - The taxonomy ID for direct lookup (most efficient)
52
- * @param name - The machine-readable name for index-based lookup
53
- * @param includeDeleted - Whether to return soft-deleted taxonomies (default: false)
54
- *
55
- * @returns The taxonomy document, or null if not found
56
- *
57
- * @example
58
- * ```typescript
59
- * // Get by ID
60
- * const taxonomy = await ctx.runQuery(api.taxonomies.get, { id: taxonomyId });
61
- *
62
- * // Get by name
63
- * const tagsTaxonomy = await ctx.runQuery(api.taxonomies.get, { name: "tags" });
64
- * ```
65
- */
66
- export const get = query({
67
- args: {
68
- id: v.optional(v.id("taxonomies")),
69
- name: v.optional(v.string()),
70
- includeDeleted: v.optional(v.boolean()),
71
- },
72
- returns: v.union(taxonomyDoc, v.null()),
73
- handler: async (ctx, args) => {
74
- const { id, name, includeDeleted = false } = args;
75
-
76
- if (!id && !name) {
77
- return null;
78
- }
79
-
80
- let taxonomy;
81
-
82
- if (id) {
83
- taxonomy = await ctx.db.get(id);
84
- } else if (name) {
85
- taxonomy = await ctx.db
86
- .query("taxonomies")
87
- .withIndex("by_name", (q) => q.eq("name", name))
88
- .first();
89
- }
90
-
91
- if (!taxonomy) {
92
- return null;
93
- }
94
-
95
- if (!includeDeleted && isDeleted(taxonomy)) {
96
- return null;
97
- }
98
-
99
- return taxonomy;
100
- },
101
- });
102
-
103
- // =============================================================================
104
- // List Taxonomies Query
105
- // =============================================================================
106
-
107
- /**
108
- * Query to list all taxonomies with optional filtering.
109
- *
110
- * @param isActive - Filter by active status
111
- * @param isHierarchical - Filter by hierarchical type
112
- * @param includeDeleted - Whether to include soft-deleted taxonomies
113
- * @param paginationOpts - Standard Convex pagination options
114
- *
115
- * @returns Paginated list of taxonomy documents
116
- *
117
- * @example
118
- * ```typescript
119
- * // List all active taxonomies
120
- * const taxonomies = await ctx.runQuery(api.taxonomies.list, {
121
- * isActive: true,
122
- * paginationOpts: { numItems: 20 },
123
- * });
124
- *
125
- * // List only flat taxonomies (like tags)
126
- * const flatTaxonomies = await ctx.runQuery(api.taxonomies.list, {
127
- * isHierarchical: false,
128
- * paginationOpts: { numItems: 20 },
129
- * });
130
- * ```
131
- */
132
- export const list = query({
133
- args: {
134
- isActive: v.optional(v.boolean()),
135
- isHierarchical: v.optional(v.boolean()),
136
- includeDeleted: v.optional(v.boolean()),
137
- paginationOpts: v.optional(paginationOptsValidator),
138
- },
139
- returns: v.object({
140
- page: v.array(taxonomyDoc),
141
- continueCursor: v.union(v.string(), v.null()),
142
- isDone: v.boolean(),
143
- }),
144
- handler: async (ctx, args) => {
145
- const {
146
- isActive,
147
- isHierarchical,
148
- includeDeleted = false,
149
- paginationOpts,
150
- } = args;
151
-
152
- const numItems = paginationOpts
153
- ? Math.min(
154
- Math.max(1, paginationOpts.numItems ?? DEFAULT_NUM_ITEMS),
155
- MAX_NUM_ITEMS,
156
- )
157
- : MAX_NUM_ITEMS;
158
-
159
- let results;
160
-
161
- if (isActive !== undefined) {
162
- results = await ctx.db
163
- .query("taxonomies")
164
- .withIndex("by_active", (q) => q.eq("isActive", isActive))
165
- .collect();
166
- } else {
167
- results = await ctx.db.query("taxonomies").collect();
168
- }
169
-
170
- // Apply post-filters
171
- if (!includeDeleted) {
172
- results = results.filter((t) => !isDeleted(t));
173
- }
174
-
175
- if (isHierarchical !== undefined) {
176
- results = results.filter((t) => t.isHierarchical === isHierarchical);
177
- }
178
-
179
- // Sort by sortOrder, then name
180
- results.sort((a, b) => {
181
- const orderA = a.sortOrder ?? 999;
182
- const orderB = b.sortOrder ?? 999;
183
- if (orderA !== orderB) return orderA - orderB;
184
- return a.name.localeCompare(b.name);
185
- });
186
-
187
- // Handle pagination
188
- let startIndex = 0;
189
- if (paginationOpts?.cursor) {
190
- const cursorIndex = results.findIndex(
191
- (t) => t._id === paginationOpts.cursor,
192
- );
193
- if (cursorIndex !== -1) {
194
- startIndex = cursorIndex + 1;
195
- }
196
- }
197
-
198
- const pageResults = results.slice(startIndex, startIndex + numItems + 1);
199
- const isDone = pageResults.length <= numItems;
200
- const page = isDone ? pageResults : pageResults.slice(0, numItems);
201
- const continueCursor =
202
- !isDone && page.length > 0 ? page[page.length - 1]._id : null;
203
-
204
- return { page, continueCursor, isDone };
205
- },
206
- });
207
-
208
- // =============================================================================
209
- // Get Term Query
210
- // =============================================================================
211
-
212
- /**
213
- * Query to retrieve a single taxonomy term by ID or slug.
214
- *
215
- * @param id - The term ID for direct lookup
216
- * @param taxonomyId - The taxonomy ID (required when looking up by slug)
217
- * @param slug - The term slug for lookup within a taxonomy
218
- * @param includeDeleted - Whether to return soft-deleted terms
219
- *
220
- * @returns The term document, or null if not found
221
- */
222
- export const getTerm = query({
223
- args: {
224
- id: v.optional(v.id("taxonomyTerms")),
225
- taxonomyId: v.optional(v.id("taxonomies")),
226
- slug: v.optional(v.string()),
227
- includeDeleted: v.optional(v.boolean()),
228
- },
229
- returns: v.union(taxonomyTermDoc, v.null()),
230
- handler: async (ctx, args) => {
231
- const { id, taxonomyId, slug, includeDeleted = false } = args;
232
-
233
- if (!id && (!taxonomyId || !slug)) {
234
- return null;
235
- }
236
-
237
- let term;
238
-
239
- if (id) {
240
- term = await ctx.db.get(id);
241
- } else if (taxonomyId && slug) {
242
- term = await ctx.db
243
- .query("taxonomyTerms")
244
- .withIndex("by_taxonomy_and_slug", (q) =>
245
- q.eq("taxonomyId", taxonomyId).eq("slug", slug),
246
- )
247
- .first();
248
- }
249
-
250
- if (!term) {
251
- return null;
252
- }
253
-
254
- if (!includeDeleted && isDeleted(term)) {
255
- return null;
256
- }
257
-
258
- return term;
259
- },
260
- });
261
-
262
- // =============================================================================
263
- // List Terms Query
264
- // =============================================================================
265
-
266
- /**
267
- * Query to list terms within a taxonomy.
268
- *
269
- * @param taxonomyId - The taxonomy to list terms from (required)
270
- * @param parentId - Filter by parent term (for hierarchical navigation)
271
- * @param rootOnly - Only return root-level terms (depth = 0)
272
- * @param search - Search terms by name
273
- * @param includeDeleted - Whether to include soft-deleted terms
274
- * @param sortBy - Sort field: "name", "usageCount", "sortOrder"
275
- * @param sortDirection - Sort direction
276
- * @param paginationOpts - Standard Convex pagination options
277
- *
278
- * @returns Paginated list of term documents
279
- *
280
- * @example
281
- * ```typescript
282
- * // List all tags in a taxonomy
283
- * const tags = await ctx.runQuery(api.taxonomies.listTerms, {
284
- * taxonomyId: tagsTaxonomyId,
285
- * paginationOpts: { numItems: 50 },
286
- * });
287
- *
288
- * // List root categories only
289
- * const rootCategories = await ctx.runQuery(api.taxonomies.listTerms, {
290
- * taxonomyId: categoriesTaxonomyId,
291
- * rootOnly: true,
292
- * paginationOpts: { numItems: 20 },
293
- * });
294
- *
295
- * // List children of a category
296
- * const children = await ctx.runQuery(api.taxonomies.listTerms, {
297
- * taxonomyId: categoriesTaxonomyId,
298
- * parentId: parentCategoryId,
299
- * paginationOpts: { numItems: 20 },
300
- * });
301
- *
302
- * // Sort by popularity (usage count)
303
- * const popularTags = await ctx.runQuery(api.taxonomies.listTerms, {
304
- * taxonomyId: tagsTaxonomyId,
305
- * sortBy: "usageCount",
306
- * sortDirection: "desc",
307
- * paginationOpts: { numItems: 20 },
308
- * });
309
- * ```
310
- */
311
- export const listTerms = query({
312
- args: {
313
- taxonomyId: v.id("taxonomies"),
314
- parentId: v.optional(v.id("taxonomyTerms")),
315
- rootOnly: v.optional(v.boolean()),
316
- search: v.optional(v.string()),
317
- includeDeleted: v.optional(v.boolean()),
318
- sortBy: v.optional(
319
- v.union(
320
- v.literal("name"),
321
- v.literal("usageCount"),
322
- v.literal("sortOrder"),
323
- ),
324
- ),
325
- sortDirection: v.optional(v.union(v.literal("asc"), v.literal("desc"))),
326
- paginationOpts: v.optional(paginationOptsValidator),
327
- },
328
- returns: v.object({
329
- page: v.array(taxonomyTermDoc),
330
- continueCursor: v.union(v.string(), v.null()),
331
- isDone: v.boolean(),
332
- }),
333
- handler: async (ctx, args) => {
334
- const {
335
- taxonomyId,
336
- parentId,
337
- rootOnly,
338
- search,
339
- includeDeleted = false,
340
- sortBy = "name",
341
- sortDirection = "asc",
342
- paginationOpts,
343
- } = args;
344
-
345
- const numItems = paginationOpts
346
- ? Math.min(
347
- Math.max(1, paginationOpts.numItems ?? DEFAULT_NUM_ITEMS),
348
- MAX_NUM_ITEMS,
349
- )
350
- : MAX_NUM_ITEMS;
351
-
352
- let results;
353
-
354
- // Use search index if searching
355
- if (search && search.trim().length > 0) {
356
- results = await ctx.db
357
- .query("taxonomyTerms")
358
- .withSearchIndex("search_terms", (q) =>
359
- q.search("searchText", search.trim()).eq("taxonomyId", taxonomyId),
360
- )
361
- .take(numItems * 4); // Fetch extra for post-filtering
362
- } else if (parentId !== undefined) {
363
- // Filter by parent
364
- results = await ctx.db
365
- .query("taxonomyTerms")
366
- .withIndex("by_parent", (q) => q.eq("parentId", parentId))
367
- .collect();
368
- // Additional filter for taxonomy (parent could be cross-taxonomy in theory)
369
- results = results.filter((t) => t.taxonomyId === taxonomyId);
370
- } else {
371
- // Get all terms in taxonomy
372
- results = await ctx.db
373
- .query("taxonomyTerms")
374
- .withIndex("by_taxonomy", (q) => q.eq("taxonomyId", taxonomyId))
375
- .collect();
376
- }
377
-
378
- // Apply post-filters
379
- if (!includeDeleted) {
380
- results = results.filter((t) => !isDeleted(t));
381
- }
382
-
383
- if (rootOnly) {
384
- results = results.filter((t) => t.depth === 0);
385
- }
386
-
387
- // Sort results
388
- results.sort((a, b) => {
389
- let comparison = 0;
390
- switch (sortBy) {
391
- case "usageCount":
392
- comparison = a.usageCount - b.usageCount;
393
- break;
394
- case "sortOrder":
395
- comparison = (a.sortOrder ?? 999) - (b.sortOrder ?? 999);
396
- break;
397
- case "name":
398
- default:
399
- comparison = a.name.toLowerCase().localeCompare(b.name.toLowerCase());
400
- break;
401
- }
402
- return sortDirection === "asc" ? comparison : -comparison;
403
- });
404
-
405
- // Handle pagination
406
- let startIndex = 0;
407
- if (paginationOpts?.cursor) {
408
- const cursorIndex = results.findIndex(
409
- (t) => t._id === paginationOpts.cursor,
410
- );
411
- if (cursorIndex !== -1) {
412
- startIndex = cursorIndex + 1;
413
- }
414
- }
415
-
416
- const pageResults = results.slice(startIndex, startIndex + numItems + 1);
417
- const isDone = pageResults.length <= numItems;
418
- const page = isDone ? pageResults : pageResults.slice(0, numItems);
419
- const continueCursor =
420
- !isDone && page.length > 0 ? page[page.length - 1]._id : null;
421
-
422
- return { page, continueCursor, isDone };
423
- },
424
- });
425
-
426
- // =============================================================================
427
- // Get Hierarchical Terms Query
428
- // =============================================================================
429
-
430
- /**
431
- * Query to get all terms in a taxonomy as a hierarchical tree structure.
432
- *
433
- * This is useful for rendering nested category selectors or tree views.
434
- * Returns terms with their children nested in a tree structure.
435
- *
436
- * @param taxonomyId - The taxonomy to get terms from
437
- * @param includeDeleted - Whether to include soft-deleted terms
438
- *
439
- * @returns Array of root terms with nested children
440
- *
441
- * @example
442
- * ```typescript
443
- * const tree = await ctx.runQuery(api.taxonomies.getTermsHierarchy, {
444
- * taxonomyId: categoriesTaxonomyId,
445
- * });
446
- * // Returns: [
447
- * // { name: "Tech", children: [{ name: "Web Dev", children: [...] }] },
448
- * // { name: "Design", children: [...] },
449
- * // ]
450
- * ```
451
- */
452
- export const getTermsHierarchy = query({
453
- args: {
454
- taxonomyId: v.id("taxonomies"),
455
- includeDeleted: v.optional(v.boolean()),
456
- },
457
- returns: v.array(taxonomyTermWithChildren),
458
- handler: async (ctx, args) => {
459
- const { taxonomyId, includeDeleted = false } = args;
460
-
461
- // Get all terms in the taxonomy
462
- let terms = await ctx.db
463
- .query("taxonomyTerms")
464
- .withIndex("by_taxonomy", (q) => q.eq("taxonomyId", taxonomyId))
465
- .collect();
466
-
467
- // Filter deleted if needed
468
- if (!includeDeleted) {
469
- terms = terms.filter((t) => !isDeleted(t));
470
- }
471
-
472
- // Sort by sortOrder, then name
473
- terms.sort((a, b) => {
474
- const orderA = a.sortOrder ?? 999;
475
- const orderB = b.sortOrder ?? 999;
476
- if (orderA !== orderB) return orderA - orderB;
477
- return a.name.localeCompare(b.name);
478
- });
479
-
480
- // Build tree structure
481
- const termMap = new Map<string, any>();
482
- const rootTerms: any[] = [];
483
-
484
- // First pass: create term objects with empty children
485
- for (const term of terms) {
486
- termMap.set(term._id, { ...term, children: [] });
487
- }
488
-
489
- // Second pass: link parents to children
490
- for (const term of terms) {
491
- const termWithChildren = termMap.get(term._id);
492
- if (term.parentId && termMap.has(term.parentId)) {
493
- const parent = termMap.get(term.parentId);
494
- parent.children.push(termWithChildren);
495
- } else {
496
- rootTerms.push(termWithChildren);
497
- }
498
- }
499
-
500
- return rootTerms;
501
- },
502
- });
503
-
504
- // =============================================================================
505
- // Suggest Terms Query
506
- // =============================================================================
507
-
508
- /**
509
- * Query to get term suggestions based on partial input.
510
- *
511
- * This is useful for autocomplete functionality when users are selecting
512
- * or creating tags. Returns matching terms sorted by relevance and usage.
513
- *
514
- * @param taxonomyId - The taxonomy to search within
515
- * @param query - The partial input to match against term names
516
- * @param limit - Maximum number of suggestions to return (default: 10)
517
- * @param excludeIds - Term IDs to exclude from suggestions (already selected)
518
- *
519
- * @returns Array of matching terms
520
- *
521
- * @example
522
- * ```typescript
523
- * const suggestions = await ctx.runQuery(api.taxonomies.suggestTerms, {
524
- * taxonomyId: tagsTaxonomyId,
525
- * query: "java",
526
- * limit: 5,
527
- * excludeIds: alreadySelectedTagIds,
528
- * });
529
- * // Returns: [{ name: "JavaScript" }, { name: "Java" }, { name: "JavaFX" }]
530
- * ```
531
- */
532
- export const suggestTerms = query({
533
- args: {
534
- taxonomyId: v.id("taxonomies"),
535
- query: v.string(),
536
- limit: v.optional(v.number()),
537
- excludeIds: v.optional(v.array(v.id("taxonomyTerms"))),
538
- },
539
- returns: v.array(taxonomyTermDoc),
540
- handler: async (ctx, args) => {
541
- const {
542
- taxonomyId,
543
- query: searchQuery,
544
- limit = 10,
545
- excludeIds = [],
546
- } = args;
547
-
548
- const excludeSet = new Set(excludeIds);
549
-
550
- if (!searchQuery || searchQuery.trim().length === 0) {
551
- // Return popular terms if no query
552
- const terms = await ctx.db
553
- .query("taxonomyTerms")
554
- .withIndex("by_taxonomy_and_usage", (q) =>
555
- q.eq("taxonomyId", taxonomyId),
556
- )
557
- .order("desc")
558
- .take(limit * 2);
559
-
560
- return terms
561
- .filter((t) => !isDeleted(t) && !excludeSet.has(t._id))
562
- .slice(0, limit);
563
- }
564
-
565
- // Search for matching terms
566
- const terms = await ctx.db
567
- .query("taxonomyTerms")
568
- .withSearchIndex("search_terms", (q) =>
569
- q.search("searchText", searchQuery.trim()).eq("taxonomyId", taxonomyId),
570
- )
571
- .take(limit * 2);
572
-
573
- // Filter and limit
574
- const filtered = terms.filter(
575
- (t) => !isDeleted(t) && !excludeSet.has(t._id),
576
- );
577
-
578
- // Sort by: exact prefix match first, then usage count
579
- const query = searchQuery.toLowerCase();
580
- filtered.sort((a, b) => {
581
- const aExact = a.name.toLowerCase().startsWith(query) ? 0 : 1;
582
- const bExact = b.name.toLowerCase().startsWith(query) ? 0 : 1;
583
- if (aExact !== bExact) return aExact - bExact;
584
- return b.usageCount - a.usageCount;
585
- });
586
-
587
- return filtered.slice(0, limit);
588
- },
589
- });
590
-
591
- // =============================================================================
592
- // Get Terms by Entry Query
593
- // =============================================================================
594
-
595
- /**
596
- * Query to get all taxonomy terms associated with a content entry.
597
- *
598
- * @param entryId - The content entry ID
599
- * @param taxonomyId - Optional taxonomy filter
600
- * @param fieldName - Optional field name filter
601
- *
602
- * @returns Array of terms associated with the entry
603
- *
604
- * @example
605
- * ```typescript
606
- * // Get all tags for an entry
607
- * const entryTags = await ctx.runQuery(api.taxonomies.getTermsByEntry, {
608
- * entryId: blogPostId,
609
- * });
610
- *
611
- * // Get only tags from a specific field
612
- * const primaryTags = await ctx.runQuery(api.taxonomies.getTermsByEntry, {
613
- * entryId: blogPostId,
614
- * fieldName: "tags",
615
- * });
616
- * ```
617
- */
618
- export const getTermsByEntry = query({
619
- args: {
620
- entryId: v.id("contentEntries"),
621
- taxonomyId: v.optional(v.id("taxonomies")),
622
- fieldName: v.optional(v.string()),
623
- },
624
- returns: v.array(
625
- v.object({
626
- ...taxonomyTermDoc.fields,
627
- fieldName: v.string(),
628
- sortOrder: v.optional(v.number()),
629
- }),
630
- ),
631
- handler: async (ctx, args) => {
632
- const { entryId, taxonomyId, fieldName } = args;
633
-
634
- // Get the junction table entries
635
- const junctionQuery = ctx.db
636
- .query("contentEntryTags")
637
- .withIndex("by_entry", (q) => q.eq("entryId", entryId));
638
-
639
- const junctionEntries = await junctionQuery.collect();
640
-
641
- // Filter by taxonomy or field if specified
642
- let filtered = junctionEntries;
643
- if (taxonomyId) {
644
- filtered = filtered.filter((j) => j.taxonomyId === taxonomyId);
645
- }
646
- if (fieldName) {
647
- filtered = filtered.filter((j) => j.fieldName === fieldName);
648
- }
649
-
650
- // Sort by sortOrder
651
- filtered.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
652
-
653
- // Fetch the actual terms
654
- const results = [];
655
- for (const junction of filtered) {
656
- const term = await ctx.db.get(junction.termId);
657
- if (term && !isDeleted(term)) {
658
- results.push({
659
- ...term,
660
- fieldName: junction.fieldName,
661
- sortOrder: junction.sortOrder,
662
- });
663
- }
664
- }
665
-
666
- return results;
667
- },
668
- });
669
-
670
- // =============================================================================
671
- // Get Entries by Term Query
672
- // =============================================================================
673
-
674
- /**
675
- * Query to get content entries that have a specific term.
676
- *
677
- * @param termId - The term ID to search for
678
- * @param status - Optional entry status filter
679
- * @param paginationOpts - Standard Convex pagination options
680
- *
681
- * @returns Paginated list of entry IDs with the term
682
- *
683
- * @example
684
- * ```typescript
685
- * // Get all entries with a specific tag
686
- * const entriesWithTag = await ctx.runQuery(api.taxonomies.getEntriesByTerm, {
687
- * termId: javascriptTagId,
688
- * status: "published",
689
- * paginationOpts: { numItems: 20 },
690
- * });
691
- * ```
692
- */
693
- export const getEntriesByTerm = query({
694
- args: {
695
- termId: v.id("taxonomyTerms"),
696
- status: v.optional(
697
- v.union(
698
- v.literal("draft"),
699
- v.literal("published"),
700
- v.literal("archived"),
701
- v.literal("scheduled"),
702
- ),
703
- ),
704
- paginationOpts: v.optional(paginationOptsValidator),
705
- },
706
- returns: v.object({
707
- page: v.array(v.id("contentEntries")),
708
- continueCursor: v.union(v.string(), v.null()),
709
- isDone: v.boolean(),
710
- }),
711
- handler: async (ctx, args) => {
712
- const { termId, status, paginationOpts } = args;
713
-
714
- const numItems = paginationOpts
715
- ? Math.min(
716
- Math.max(1, paginationOpts.numItems ?? DEFAULT_NUM_ITEMS),
717
- MAX_NUM_ITEMS,
718
- )
719
- : DEFAULT_NUM_ITEMS;
720
-
721
- // Get junction entries for this term
722
- const junctionEntries = await ctx.db
723
- .query("contentEntryTags")
724
- .withIndex("by_term", (q) => q.eq("termId", termId))
725
- .collect();
726
-
727
- // Get unique entry IDs
728
- const entryIds = [...new Set(junctionEntries.map((j) => j.entryId))];
729
-
730
- // Filter by status if needed
731
- let filteredEntryIds = entryIds;
732
- if (status) {
733
- const validEntryIds: typeof entryIds = [];
734
- for (const entryId of entryIds) {
735
- const entry = await ctx.db.get(entryId);
736
- if (entry && entry.status === status && !isDeleted(entry)) {
737
- validEntryIds.push(entryId);
738
- }
739
- }
740
- filteredEntryIds = validEntryIds;
741
- }
742
-
743
- // Handle pagination
744
- let startIndex = 0;
745
- if (paginationOpts?.cursor) {
746
- const cursorIndex = filteredEntryIds.findIndex(
747
- (id) => id === paginationOpts.cursor,
748
- );
749
- if (cursorIndex !== -1) {
750
- startIndex = cursorIndex + 1;
751
- }
752
- }
753
-
754
- const pageResults = filteredEntryIds.slice(
755
- startIndex,
756
- startIndex + numItems + 1,
757
- );
758
- const isDone = pageResults.length <= numItems;
759
- const page = isDone ? pageResults : pageResults.slice(0, numItems);
760
- const continueCursor =
761
- !isDone && page.length > 0 ? page[page.length - 1] : null;
762
-
763
- return { page, continueCursor, isDone };
764
- },
765
- });
766
-
767
- // =============================================================================
768
- // Count Terms Query
769
- // =============================================================================
770
-
771
- /**
772
- * Query to count terms in a taxonomy.
773
- *
774
- * @param taxonomyId - The taxonomy to count terms in
775
- * @param includeDeleted - Whether to include soft-deleted terms
776
- *
777
- * @returns Object containing the count
778
- */
779
- export const countTerms = query({
780
- args: {
781
- taxonomyId: v.id("taxonomies"),
782
- includeDeleted: v.optional(v.boolean()),
783
- },
784
- returns: v.object({
785
- count: v.number(),
786
- }),
787
- handler: async (ctx, args) => {
788
- const { taxonomyId, includeDeleted = false } = args;
789
-
790
- const terms = await ctx.db
791
- .query("taxonomyTerms")
792
- .withIndex("by_taxonomy", (q) => q.eq("taxonomyId", taxonomyId))
793
- .collect();
794
-
795
- const filteredTerms = includeDeleted
796
- ? terms
797
- : terms.filter((t) => !isDeleted(t));
798
-
799
- return { count: filteredTerms.length };
800
- },
801
- });
802
-
803
- // =============================================================================
804
- // Media Asset Taxonomy Queries
805
- // =============================================================================
806
-
807
- /**
808
- * Query to get all taxonomy terms associated with a media asset.
809
- *
810
- * @param mediaId - The media asset ID
811
- * @param taxonomyId - Optional taxonomy filter
812
- *
813
- * @returns Array of terms associated with the media asset
814
- *
815
- * @example
816
- * ```typescript
817
- * // Get all terms for a media asset
818
- * const mediaTags = await ctx.runQuery(api.taxonomies.getTermsByMedia, {
819
- * mediaId: imageId,
820
- * });
821
- *
822
- * // Get only terms from a specific taxonomy
823
- * const categories = await ctx.runQuery(api.taxonomies.getTermsByMedia, {
824
- * mediaId: imageId,
825
- * taxonomyId: categoriesTaxonomyId,
826
- * });
827
- * ```
828
- */
829
- export const getTermsByMedia = query({
830
- args: {
831
- mediaId: v.id("mediaItems"),
832
- taxonomyId: v.optional(v.id("taxonomies")),
833
- },
834
- returns: v.array(
835
- v.object({
836
- ...taxonomyTermDoc.fields,
837
- sortOrder: v.optional(v.number()),
838
- }),
839
- ),
840
- handler: async (ctx, args) => {
841
- const { mediaId, taxonomyId } = args;
842
-
843
- const junctionEntries = await ctx.db
844
- .query("mediaAssetTags")
845
- .withIndex("by_media", (q) => q.eq("mediaId", mediaId))
846
- .collect();
847
-
848
- let filtered = junctionEntries;
849
- if (taxonomyId) {
850
- filtered = filtered.filter((j) => j.taxonomyId === taxonomyId);
851
- }
852
-
853
- filtered.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
854
-
855
- const results = [];
856
- for (const junction of filtered) {
857
- const term = await ctx.db.get(junction.termId);
858
- if (term && !isDeleted(term)) {
859
- results.push({
860
- ...term,
861
- sortOrder: junction.sortOrder,
862
- });
863
- }
864
- }
865
-
866
- return results;
867
- },
868
- });
869
-
870
- /**
871
- * Query to get media assets that have a specific term.
872
- *
873
- * @param termId - The term ID to search for
874
- * @param includeDeleted - Whether to include soft-deleted media
875
- * @param paginationOpts - Standard Convex pagination options
876
- *
877
- * @returns Paginated list of media asset IDs with the term
878
- *
879
- * @example
880
- * ```typescript
881
- * // Get all media with a specific category
882
- * const mediaWithCategory = await ctx.runQuery(api.taxonomies.getMediaByTerm, {
883
- * termId: landscapeCategoryId,
884
- * paginationOpts: { numItems: 20 },
885
- * });
886
- * ```
887
- */
888
- export const getMediaByTerm = query({
889
- args: {
890
- termId: v.id("taxonomyTerms"),
891
- includeDeleted: v.optional(v.boolean()),
892
- paginationOpts: v.optional(paginationOptsValidator),
893
- },
894
- returns: v.object({
895
- page: v.array(v.id("mediaItems")),
896
- continueCursor: v.union(v.string(), v.null()),
897
- isDone: v.boolean(),
898
- }),
899
- handler: async (ctx, args) => {
900
- const { termId, includeDeleted = false, paginationOpts } = args;
901
-
902
- const numItems = paginationOpts
903
- ? Math.min(
904
- Math.max(1, paginationOpts.numItems ?? DEFAULT_NUM_ITEMS),
905
- MAX_NUM_ITEMS,
906
- )
907
- : DEFAULT_NUM_ITEMS;
908
-
909
- const junctionEntries = await ctx.db
910
- .query("mediaAssetTags")
911
- .withIndex("by_term", (q) => q.eq("termId", termId))
912
- .collect();
913
-
914
- const mediaIds = [...new Set(junctionEntries.map((j) => j.mediaId))];
915
-
916
- let filteredMediaIds = mediaIds;
917
- if (!includeDeleted) {
918
- const validMediaIds: typeof mediaIds = [];
919
- for (const mediaId of mediaIds) {
920
- const media = await ctx.db.get(mediaId);
921
- if (media && !isDeleted(media)) {
922
- validMediaIds.push(mediaId);
923
- }
924
- }
925
- filteredMediaIds = validMediaIds;
926
- }
927
-
928
- let startIndex = 0;
929
- if (paginationOpts?.cursor) {
930
- const cursorIndex = filteredMediaIds.findIndex(
931
- (id) => id === paginationOpts.cursor,
932
- );
933
- if (cursorIndex !== -1) {
934
- startIndex = cursorIndex + 1;
935
- }
936
- }
937
-
938
- const pageResults = filteredMediaIds.slice(
939
- startIndex,
940
- startIndex + numItems + 1,
941
- );
942
- const isDone = pageResults.length <= numItems;
943
- const page = isDone ? pageResults : pageResults.slice(0, numItems);
944
- const continueCursor =
945
- !isDone && page.length > 0 ? page[page.length - 1] : null;
946
-
947
- return { page, continueCursor, isDone };
948
- },
949
- });