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,999 +0,0 @@
1
- /**
2
- * Deep Reference Resolution Utilities
3
- *
4
- * Provides functions for recursively resolving content and media references
5
- * within content entries. Supports depth limiting and circular reference
6
- * prevention to avoid infinite loops.
7
- *
8
- * This module extends the basic reference resolution with:
9
- * - Recursive resolution of nested references
10
- * - Configurable maximum depth
11
- * - Circular reference detection and prevention
12
- * - Combined content and media reference resolution
13
- * - Selective field resolution
14
- *
15
- * @example
16
- * ```typescript
17
- * // Resolve a blog post with author and related posts
18
- * const resolvedEntry = await resolveEntryReferences(ctx, entry, contentType.fields, {
19
- * maxDepth: 2,
20
- * resolveMedia: true,
21
- * publishedOnly: true,
22
- * });
23
- *
24
- * // The resolved entry will have:
25
- * // - entry.data.author resolved to full author entry
26
- * // - entry.data.author.data.profileImage resolved to media URL
27
- * // - entry.data.relatedPosts resolved to array of entries (depth 1)
28
- * // - entry.data.relatedPosts[].author NOT resolved (depth limit reached)
29
- * ```
30
- */
31
-
32
- import {
33
- // Doc,
34
- Id,
35
- } from "../_generated/dataModel.js";
36
- import { QueryCtx } from "../_generated/server.js";
37
- import { isDeleted } from "./softDelete.js";
38
- import {
39
- resolveReference,
40
- // resolveReferences,
41
- // ResolvedReference,
42
- // ResolveOptions,
43
- } from "./referenceResolver.js";
44
- import {
45
- resolveMediaReference,
46
- // resolveMediaReferences,
47
- ResolvedMediaReference,
48
- MediaResolveOptions,
49
- } from "./mediaReferenceResolver.js";
50
-
51
- // =============================================================================
52
- // Types
53
- // =============================================================================
54
-
55
- /**
56
- * Field definition subset needed for reference resolution.
57
- * This type matches the fields array in content types.
58
- */
59
- export interface FieldDefinitionForResolver {
60
- /** Field name in the data object */
61
- name: string;
62
- /** Field type identifier */
63
- type: string;
64
- /** Field-specific options */
65
- options?: {
66
- /** For reference fields: allowed content type names */
67
- allowedContentTypes?: string[];
68
- /** For reference/media fields: whether multiple values are allowed */
69
- multiple?: boolean;
70
- /** For media fields: allowed MIME types */
71
- allowedMimeTypes?: string[];
72
- };
73
- }
74
-
75
- /**
76
- * Options for deep reference resolution.
77
- */
78
- export interface DeepResolveOptions {
79
- /**
80
- * Maximum depth to resolve nested references.
81
- * - 0: Don't resolve any references (just return IDs)
82
- * - 1: Resolve immediate references only
83
- * - 2: Resolve references and their references
84
- * - etc.
85
- *
86
- * @default 1
87
- */
88
- maxDepth?: number;
89
-
90
- /**
91
- * Whether to resolve media references.
92
- * When true, media IDs are replaced with full asset data including URLs.
93
- *
94
- * @default true
95
- */
96
- resolveMedia?: boolean;
97
-
98
- /**
99
- * Whether to resolve content references.
100
- * When true, content entry IDs are replaced with full entry data.
101
- *
102
- * @default true
103
- */
104
- resolveContent?: boolean;
105
-
106
- /**
107
- * Only resolve references to published entries.
108
- * Useful for frontend/public API usage.
109
- *
110
- * @default false
111
- */
112
- publishedOnly?: boolean;
113
-
114
- /**
115
- * Include soft-deleted entries when resolving.
116
- *
117
- * @default false
118
- */
119
- includeDeleted?: boolean;
120
-
121
- /**
122
- * Specific fields to include from resolved entries.
123
- * If not specified, all fields are included.
124
- * Only applies to content references.
125
- */
126
- fields?: string[];
127
-
128
- /**
129
- * Specific field names to resolve references for.
130
- * If not specified, all reference/media fields are resolved.
131
- * Useful for selective resolution of expensive operations.
132
- */
133
- onlyFields?: string[];
134
-
135
- /**
136
- * Field names to skip when resolving references.
137
- * Useful for excluding specific fields from resolution.
138
- */
139
- excludeFields?: string[];
140
-
141
- /**
142
- * Whether to preserve the original reference ID alongside resolved data.
143
- * When true, resolved objects include an `_originalId` field.
144
- *
145
- * @default false
146
- */
147
- preserveOriginalIds?: boolean;
148
- }
149
-
150
- /**
151
- * A content entry with resolved references.
152
- * The data object will have reference fields replaced with resolved content.
153
- */
154
- export interface ResolvedContentEntry {
155
- /** The content entry ID */
156
- id: string;
157
- /** The content type name */
158
- contentTypeName: string;
159
- /** The content type display name */
160
- contentTypeDisplayName: string;
161
- /** The entry's URL slug */
162
- slug: string;
163
- /** The entry's publishing status (supports custom workflow states) */
164
- status: string;
165
- /** The entry's data with resolved references */
166
- data: Record<string, unknown>;
167
- /** Whether the entry exists */
168
- exists: boolean;
169
- /** Locale code if localized */
170
- locale?: string;
171
- /** Version number */
172
- version?: number;
173
- /** Fields that had circular references (were not resolved) */
174
- _circularReferences?: string[];
175
- /** Fields that had unresolved references (not found) */
176
- _unresolvedReferences?: Record<string, string[]>;
177
- /** Original entry ID (only if preserveOriginalIds is true) */
178
- _originalId?: string;
179
- }
180
-
181
- /**
182
- * Context for tracking resolution state during recursive resolution.
183
- * Used internally to prevent circular references.
184
- */
185
- interface ResolutionContext {
186
- /** Set of entry IDs currently being resolved (for circular detection) */
187
- visitedEntries: Set<string>;
188
- /** Set of media IDs currently being resolved */
189
- visitedMedia: Set<string>;
190
- /** Current resolution depth */
191
- currentDepth: number;
192
- /** Maximum allowed depth */
193
- maxDepth: number;
194
- /** Cache of already-resolved entries at this depth */
195
- resolvedCache: Map<string, ResolvedContentEntry | null>;
196
- /** Cache of already-resolved media assets */
197
- mediaCache: Map<string, ResolvedMediaReference | null>;
198
- /** Fields with detected circular references */
199
- circularReferences: string[];
200
- /** Fields with unresolved references */
201
- unresolvedReferences: Record<string, string[]>;
202
- }
203
-
204
- /**
205
- * Result of resolving references for multiple entries.
206
- */
207
- export interface BatchResolveResult {
208
- /** Successfully resolved entries */
209
- resolved: ResolvedContentEntry[];
210
- /** Entry IDs that could not be resolved */
211
- unresolved: string[];
212
- /** Summary of circular references detected */
213
- circularReferencesDetected: number;
214
- }
215
-
216
- // =============================================================================
217
- // Core Resolution Functions
218
- // =============================================================================
219
-
220
- /**
221
- * Resolve all references within a content entry's data.
222
- *
223
- * This function recursively resolves reference and media fields up to
224
- * the specified depth, while preventing circular references.
225
- *
226
- * @param ctx - Convex query context
227
- * @param entry - The content entry to resolve references for
228
- * @param fields - Field definitions from the content type
229
- * @param options - Resolution options
230
- * @returns The entry with resolved references
231
- *
232
- * @example
233
- * ```typescript
234
- * // Basic usage - resolve one level deep
235
- * const resolved = await resolveEntryReferences(ctx, blogPost, contentType.fields);
236
- *
237
- * // Resolve two levels deep with only published entries
238
- * const resolved = await resolveEntryReferences(ctx, blogPost, contentType.fields, {
239
- * maxDepth: 2,
240
- * publishedOnly: true,
241
- * });
242
- *
243
- * // Resolve only specific fields
244
- * const resolved = await resolveEntryReferences(ctx, blogPost, contentType.fields, {
245
- * onlyFields: ["author", "featuredImage"],
246
- * });
247
- * ```
248
- */
249
- export async function resolveEntryReferences(
250
- ctx: QueryCtx,
251
- entry: {
252
- _id: string;
253
- slug: string;
254
- status: string;
255
- data: Record<string, unknown>;
256
- contentTypeId?: string;
257
- locale?: string;
258
- version?: number;
259
- },
260
- fields: FieldDefinitionForResolver[],
261
- options: DeepResolveOptions = {},
262
- ): Promise<ResolvedContentEntry> {
263
- const {
264
- maxDepth = 1,
265
- resolveMedia = true,
266
- resolveContent = true,
267
- publishedOnly = false,
268
- includeDeleted = false,
269
- fields: selectFields,
270
- onlyFields,
271
- excludeFields,
272
- preserveOriginalIds = false,
273
- } = options;
274
-
275
- // Get content type info
276
- let contentTypeName = "";
277
- let contentTypeDisplayName = "";
278
-
279
- if (entry.contentTypeId) {
280
- try {
281
- const contentType = await ctx.db.get(
282
- entry.contentTypeId as Id<"contentTypes">,
283
- );
284
- if (contentType) {
285
- contentTypeName = contentType.name;
286
- contentTypeDisplayName = contentType.displayName;
287
- }
288
- } catch {
289
- // Content type not found, continue with empty names
290
- }
291
- }
292
-
293
- // If maxDepth is 0, return without resolving
294
- if (maxDepth === 0) {
295
- return {
296
- id: entry._id,
297
- contentTypeName,
298
- contentTypeDisplayName,
299
- slug: entry.slug,
300
- status: entry.status,
301
- data: entry.data,
302
- exists: true,
303
- locale: entry.locale,
304
- version: entry.version,
305
- ...(preserveOriginalIds && { _originalId: entry._id }),
306
- };
307
- }
308
-
309
- // Initialize resolution context
310
- const resolutionCtx: ResolutionContext = {
311
- visitedEntries: new Set([entry._id]),
312
- visitedMedia: new Set(),
313
- currentDepth: 0,
314
- maxDepth,
315
- resolvedCache: new Map(),
316
- mediaCache: new Map(),
317
- circularReferences: [],
318
- unresolvedReferences: {},
319
- };
320
-
321
- // Filter fields to resolve based on options
322
- const fieldsToResolve = filterFieldsToResolve(
323
- fields,
324
- onlyFields,
325
- excludeFields,
326
- );
327
-
328
- // Resolve the entry data
329
- const resolvedData = await resolveDataFields(
330
- ctx,
331
- entry.data,
332
- fieldsToResolve,
333
- resolutionCtx,
334
- {
335
- resolveMedia,
336
- resolveContent,
337
- publishedOnly,
338
- includeDeleted,
339
- selectFields,
340
- preserveOriginalIds,
341
- },
342
- );
343
-
344
- const result: ResolvedContentEntry = {
345
- id: entry._id,
346
- contentTypeName,
347
- contentTypeDisplayName,
348
- slug: entry.slug,
349
- status: entry.status,
350
- data: resolvedData,
351
- exists: true,
352
- locale: entry.locale,
353
- version: entry.version,
354
- };
355
-
356
- // Add metadata about resolution issues
357
- if (resolutionCtx.circularReferences.length > 0) {
358
- result._circularReferences = resolutionCtx.circularReferences;
359
- }
360
-
361
- if (Object.keys(resolutionCtx.unresolvedReferences).length > 0) {
362
- result._unresolvedReferences = resolutionCtx.unresolvedReferences;
363
- }
364
-
365
- if (preserveOriginalIds) {
366
- result._originalId = entry._id;
367
- }
368
-
369
- return result;
370
- }
371
-
372
- /**
373
- * Resolve references for multiple content entries in batch.
374
- *
375
- * More efficient than calling resolveEntryReferences multiple times
376
- * as it shares caches across entries.
377
- *
378
- * @param ctx - Convex query context
379
- * @param entries - Array of content entries to resolve
380
- * @param fields - Field definitions from the content type
381
- * @param options - Resolution options
382
- * @returns Batch result with resolved entries and unresolved IDs
383
- *
384
- * @example
385
- * ```typescript
386
- * const { page } = await cms.contentEntries.list(ctx, { ... });
387
- * const result = await resolveEntryReferencesBatch(ctx, page, contentType.fields, {
388
- * maxDepth: 1,
389
- * publishedOnly: true,
390
- * });
391
- * ```
392
- */
393
- export async function resolveEntryReferencesBatch(
394
- ctx: QueryCtx,
395
- entries: Array<{
396
- _id: string;
397
- slug: string;
398
- status: string;
399
- data: Record<string, unknown>;
400
- contentTypeId?: string;
401
- locale?: string;
402
- version?: number;
403
- }>,
404
- fields: FieldDefinitionForResolver[],
405
- options: DeepResolveOptions = {},
406
- ): Promise<BatchResolveResult> {
407
- const resolved: ResolvedContentEntry[] = [];
408
- const unresolved: string[] = [];
409
- let circularReferencesDetected = 0;
410
-
411
- // Resolve each entry in parallel
412
- const promises = entries.map(async (entry) => {
413
- try {
414
- const result = await resolveEntryReferences(ctx, entry, fields, options);
415
- if (result._circularReferences) {
416
- circularReferencesDetected += result._circularReferences.length;
417
- }
418
- return { success: true, result, id: entry._id };
419
- } catch {
420
- return { success: false, result: null, id: entry._id };
421
- }
422
- });
423
-
424
- const results = await Promise.all(promises);
425
-
426
- for (const { success, result, id } of results) {
427
- if (success && result) {
428
- resolved.push(result);
429
- } else {
430
- unresolved.push(id);
431
- }
432
- }
433
-
434
- return {
435
- resolved,
436
- unresolved,
437
- circularReferencesDetected,
438
- };
439
- }
440
-
441
- // =============================================================================
442
- // Internal Resolution Functions
443
- // =============================================================================
444
-
445
- /**
446
- * Filter fields based on onlyFields and excludeFields options.
447
- */
448
- function filterFieldsToResolve(
449
- fields: FieldDefinitionForResolver[],
450
- onlyFields?: string[],
451
- excludeFields?: string[],
452
- ): FieldDefinitionForResolver[] {
453
- let filtered = fields.filter(
454
- (f) => f.type === "reference" || f.type === "media",
455
- );
456
-
457
- if (onlyFields && onlyFields.length > 0) {
458
- filtered = filtered.filter((f) => onlyFields.includes(f.name));
459
- }
460
-
461
- if (excludeFields && excludeFields.length > 0) {
462
- filtered = filtered.filter((f) => !excludeFields.includes(f.name));
463
- }
464
-
465
- return filtered;
466
- }
467
-
468
- /**
469
- * Resolve all reference and media fields in a data object.
470
- */
471
- async function resolveDataFields(
472
- ctx: QueryCtx,
473
- data: Record<string, unknown>,
474
- fields: FieldDefinitionForResolver[],
475
- resolutionCtx: ResolutionContext,
476
- options: {
477
- resolveMedia: boolean;
478
- resolveContent: boolean;
479
- publishedOnly: boolean;
480
- includeDeleted: boolean;
481
- selectFields?: string[];
482
- preserveOriginalIds: boolean;
483
- },
484
- ): Promise<Record<string, unknown>> {
485
- const resolvedData = { ...data };
486
-
487
- // Process each resolvable field
488
- for (const field of fields) {
489
- const value = data[field.name];
490
-
491
- if (value === null || value === undefined) {
492
- continue;
493
- }
494
-
495
- if (field.type === "reference" && options.resolveContent) {
496
- const resolved = await resolveReferenceField(
497
- ctx,
498
- field,
499
- value,
500
- resolutionCtx,
501
- options,
502
- );
503
- resolvedData[field.name] = resolved;
504
- } else if (field.type === "media" && options.resolveMedia) {
505
- const resolved = await resolveMediaField(
506
- ctx,
507
- field,
508
- value,
509
- resolutionCtx,
510
- options,
511
- );
512
- resolvedData[field.name] = resolved;
513
- }
514
- }
515
-
516
- return resolvedData;
517
- }
518
-
519
- /**
520
- * Resolve a reference field value (single or multiple).
521
- */
522
- async function resolveReferenceField(
523
- ctx: QueryCtx,
524
- field: FieldDefinitionForResolver,
525
- value: unknown,
526
- resolutionCtx: ResolutionContext,
527
- options: {
528
- publishedOnly: boolean;
529
- includeDeleted: boolean;
530
- selectFields?: string[];
531
- preserveOriginalIds: boolean;
532
- },
533
- ): Promise<unknown> {
534
- const isMultiple = field.options?.multiple ?? false;
535
-
536
- if (isMultiple) {
537
- // Resolve array of references
538
- if (!Array.isArray(value)) {
539
- return value; // Invalid, return as-is
540
- }
541
-
542
- const resolvedArray: unknown[] = [];
543
- const unresolvedIds: string[] = [];
544
-
545
- for (const refId of value) {
546
- if (typeof refId !== "string") {
547
- resolvedArray.push(refId);
548
- continue;
549
- }
550
-
551
- const resolved = await resolveNestedReference(
552
- ctx,
553
- refId,
554
- field.name,
555
- resolutionCtx,
556
- options,
557
- );
558
-
559
- if (resolved) {
560
- resolvedArray.push(resolved);
561
- } else {
562
- unresolvedIds.push(refId);
563
- // Keep the original ID for unresolved references
564
- if (options.preserveOriginalIds) {
565
- resolvedArray.push({ _unresolvedId: refId });
566
- }
567
- }
568
- }
569
-
570
- if (unresolvedIds.length > 0) {
571
- resolutionCtx.unresolvedReferences[field.name] = unresolvedIds;
572
- }
573
-
574
- return resolvedArray;
575
- } else {
576
- // Resolve single reference
577
- if (typeof value !== "string") {
578
- return value; // Invalid, return as-is
579
- }
580
-
581
- const resolved = await resolveNestedReference(
582
- ctx,
583
- value,
584
- field.name,
585
- resolutionCtx,
586
- options,
587
- );
588
-
589
- if (!resolved) {
590
- resolutionCtx.unresolvedReferences[field.name] = [value];
591
- if (options.preserveOriginalIds) {
592
- return { _unresolvedId: value };
593
- }
594
- }
595
-
596
- return resolved ?? value;
597
- }
598
- }
599
-
600
- /**
601
- * Resolve a nested content reference with circular detection.
602
- */
603
- async function resolveNestedReference(
604
- ctx: QueryCtx,
605
- refId: string,
606
- fieldName: string,
607
- resolutionCtx: ResolutionContext,
608
- options: {
609
- publishedOnly: boolean;
610
- includeDeleted: boolean;
611
- selectFields?: string[];
612
- preserveOriginalIds: boolean;
613
- },
614
- ): Promise<ResolvedContentEntry | null> {
615
- // Check cache first
616
- if (resolutionCtx.resolvedCache.has(refId)) {
617
- return resolutionCtx.resolvedCache.get(refId) ?? null;
618
- }
619
-
620
- // Check for circular reference
621
- if (resolutionCtx.visitedEntries.has(refId)) {
622
- resolutionCtx.circularReferences.push(`${fieldName}:${refId}`);
623
- return null;
624
- }
625
-
626
- // Check depth limit
627
- if (resolutionCtx.currentDepth >= resolutionCtx.maxDepth) {
628
- // At max depth, just return the basic resolved reference without recursing
629
- const basicRef = await resolveReference(ctx, refId, {
630
- publishedOnly: options.publishedOnly,
631
- includeDeleted: options.includeDeleted,
632
- fields: options.selectFields,
633
- });
634
-
635
- if (!basicRef) {
636
- return null;
637
- }
638
-
639
- const result: ResolvedContentEntry = {
640
- id: basicRef.id,
641
- contentTypeName: basicRef.contentTypeName,
642
- contentTypeDisplayName: basicRef.contentTypeDisplayName,
643
- slug: basicRef.slug,
644
- status: basicRef.status,
645
- data: basicRef.data,
646
- exists: basicRef.exists,
647
- ...(options.preserveOriginalIds && { _originalId: refId }),
648
- };
649
-
650
- resolutionCtx.resolvedCache.set(refId, result);
651
- return result;
652
- }
653
-
654
- // Mark as visiting
655
- resolutionCtx.visitedEntries.add(refId);
656
- resolutionCtx.currentDepth++;
657
-
658
- try {
659
- // Get the referenced entry
660
- const entry = await ctx.db.get(refId as Id<"contentEntries">);
661
-
662
- if (!entry) {
663
- resolutionCtx.resolvedCache.set(refId, null);
664
- return null;
665
- }
666
-
667
- // Check soft-delete
668
- if (!options.includeDeleted && isDeleted(entry)) {
669
- resolutionCtx.resolvedCache.set(refId, null);
670
- return null;
671
- }
672
-
673
- // Check published status
674
- if (options.publishedOnly && entry.status !== "published") {
675
- resolutionCtx.resolvedCache.set(refId, null);
676
- return null;
677
- }
678
-
679
- // Get content type for field definitions
680
- const contentType = await ctx.db.get(entry.contentTypeId);
681
-
682
- if (!contentType || isDeleted(contentType)) {
683
- resolutionCtx.resolvedCache.set(refId, null);
684
- return null;
685
- }
686
-
687
- // Recursively resolve this entry's references
688
- const nestedFields = (contentType.fields as FieldDefinitionForResolver[]).filter(
689
- (f) => f.type === "reference" || f.type === "media",
690
- );
691
-
692
- const resolvedData = await resolveDataFields(
693
- ctx,
694
- entry.data as Record<string, unknown>,
695
- nestedFields,
696
- resolutionCtx,
697
- {
698
- resolveMedia: true,
699
- resolveContent: true,
700
- publishedOnly: options.publishedOnly,
701
- includeDeleted: options.includeDeleted,
702
- selectFields: options.selectFields,
703
- preserveOriginalIds: options.preserveOriginalIds,
704
- },
705
- );
706
-
707
- // Filter fields if specified
708
- let finalData = resolvedData;
709
- if (options.selectFields && options.selectFields.length > 0) {
710
- finalData = {};
711
- for (const field of options.selectFields) {
712
- if (field in resolvedData) {
713
- finalData[field] = resolvedData[field];
714
- }
715
- }
716
- }
717
-
718
- const result: ResolvedContentEntry = {
719
- id: refId,
720
- contentTypeName: contentType.name,
721
- contentTypeDisplayName: contentType.displayName,
722
- slug: entry.slug,
723
- status: entry.status,
724
- data: finalData,
725
- exists: true,
726
- locale: entry.locale,
727
- version: entry.version,
728
- ...(options.preserveOriginalIds && { _originalId: refId }),
729
- };
730
-
731
- resolutionCtx.resolvedCache.set(refId, result);
732
- return result;
733
- } finally {
734
- // Unmark as visiting (allow visiting again from different paths)
735
- resolutionCtx.visitedEntries.delete(refId);
736
- resolutionCtx.currentDepth--;
737
- }
738
- }
739
-
740
- /**
741
- * Resolve a media field value (single or multiple).
742
- */
743
- async function resolveMediaField(
744
- ctx: QueryCtx,
745
- field: FieldDefinitionForResolver,
746
- value: unknown,
747
- resolutionCtx: ResolutionContext,
748
- options: {
749
- preserveOriginalIds: boolean;
750
- includeDeleted: boolean;
751
- },
752
- ): Promise<unknown> {
753
- const isMultiple = field.options?.multiple ?? false;
754
- const mediaOptions: MediaResolveOptions = {
755
- includeDeleted: options.includeDeleted,
756
- };
757
-
758
- if (isMultiple) {
759
- // Resolve array of media references
760
- if (!Array.isArray(value)) {
761
- return value;
762
- }
763
-
764
- const resolvedArray: unknown[] = [];
765
- const unresolvedIds: string[] = [];
766
-
767
- for (const mediaId of value) {
768
- if (typeof mediaId !== "string") {
769
- resolvedArray.push(mediaId);
770
- continue;
771
- }
772
-
773
- // Check cache
774
- if (resolutionCtx.mediaCache.has(mediaId)) {
775
- const cached = resolutionCtx.mediaCache.get(mediaId);
776
- if (cached) {
777
- resolvedArray.push(
778
- options.preserveOriginalIds
779
- ? { ...cached, _originalId: mediaId }
780
- : cached,
781
- );
782
- } else {
783
- unresolvedIds.push(mediaId);
784
- }
785
- continue;
786
- }
787
-
788
- const resolved = await resolveMediaReference(ctx, mediaId, mediaOptions);
789
-
790
- if (resolved) {
791
- resolutionCtx.mediaCache.set(mediaId, resolved);
792
- resolvedArray.push(
793
- options.preserveOriginalIds
794
- ? { ...resolved, _originalId: mediaId }
795
- : resolved,
796
- );
797
- } else {
798
- resolutionCtx.mediaCache.set(mediaId, null);
799
- unresolvedIds.push(mediaId);
800
- if (options.preserveOriginalIds) {
801
- resolvedArray.push({ _unresolvedId: mediaId });
802
- }
803
- }
804
- }
805
-
806
- if (unresolvedIds.length > 0) {
807
- resolutionCtx.unresolvedReferences[field.name] = unresolvedIds;
808
- }
809
-
810
- return resolvedArray;
811
- } else {
812
- // Resolve single media reference
813
- if (typeof value !== "string") {
814
- return value;
815
- }
816
-
817
- // Check cache
818
- if (resolutionCtx.mediaCache.has(value)) {
819
- const cached = resolutionCtx.mediaCache.get(value);
820
- if (cached) {
821
- return options.preserveOriginalIds
822
- ? { ...cached, _originalId: value }
823
- : cached;
824
- }
825
- resolutionCtx.unresolvedReferences[field.name] = [value];
826
- return options.preserveOriginalIds ? { _unresolvedId: value } : value;
827
- }
828
-
829
- const resolved = await resolveMediaReference(ctx, value, mediaOptions);
830
-
831
- if (resolved) {
832
- resolutionCtx.mediaCache.set(value, resolved);
833
- return options.preserveOriginalIds
834
- ? { ...resolved, _originalId: value }
835
- : resolved;
836
- }
837
-
838
- resolutionCtx.mediaCache.set(value, null);
839
- resolutionCtx.unresolvedReferences[field.name] = [value];
840
- return options.preserveOriginalIds ? { _unresolvedId: value } : value;
841
- }
842
- }
843
-
844
- // =============================================================================
845
- // Utility Functions
846
- // =============================================================================
847
-
848
- /**
849
- * Check if a value contains circular reference markers.
850
- *
851
- * @param data - Data object to check
852
- * @returns Array of field paths with circular references
853
- */
854
- export function findCircularReferenceMarkers(
855
- data: Record<string, unknown>,
856
- ): string[] {
857
- const markers: string[] = [];
858
-
859
- function traverse(obj: unknown, path: string): void {
860
- if (obj === null || obj === undefined) {
861
- return;
862
- }
863
-
864
- if (typeof obj === "object") {
865
- if (Array.isArray(obj)) {
866
- obj.forEach((item, index) => traverse(item, `${path}[${index}]`));
867
- } else {
868
- const record = obj as Record<string, unknown>;
869
- if ("_circularReferences" in record) {
870
- markers.push(path);
871
- }
872
- for (const [key, value] of Object.entries(record)) {
873
- traverse(value, path ? `${path}.${key}` : key);
874
- }
875
- }
876
- }
877
- }
878
-
879
- traverse(data, "");
880
- return markers;
881
- }
882
-
883
- /**
884
- * Flatten resolved references to a simple lookup map.
885
- * Useful for deduplicating references across multiple entries.
886
- *
887
- * @param entries - Array of resolved entries
888
- * @returns Map of entry ID to resolved entry
889
- */
890
- export function flattenResolvedReferences(
891
- entries: ResolvedContentEntry[],
892
- ): Map<string, ResolvedContentEntry> {
893
- const map = new Map<string, ResolvedContentEntry>();
894
-
895
- function extractReferences(data: Record<string, unknown>): void {
896
- for (const value of Object.values(data)) {
897
- if (value === null || value === undefined) {
898
- continue;
899
- }
900
-
901
- if (typeof value === "object") {
902
- if (Array.isArray(value)) {
903
- for (const item of value) {
904
- if (isResolvedContentEntry(item)) {
905
- map.set(item.id, item);
906
- extractReferences(item.data);
907
- }
908
- }
909
- } else if (isResolvedContentEntry(value as Record<string, unknown>)) {
910
- const entry = value as ResolvedContentEntry;
911
- map.set(entry.id, entry);
912
- extractReferences(entry.data);
913
- }
914
- }
915
- }
916
- }
917
-
918
- for (const entry of entries) {
919
- map.set(entry.id, entry);
920
- extractReferences(entry.data);
921
- }
922
-
923
- return map;
924
- }
925
-
926
- /**
927
- * Type guard to check if a value is a resolved content entry.
928
- */
929
- function isResolvedContentEntry(value: unknown): value is ResolvedContentEntry {
930
- if (typeof value !== "object" || value === null) {
931
- return false;
932
- }
933
- const obj = value as Record<string, unknown>;
934
- return (
935
- "id" in obj &&
936
- "contentTypeName" in obj &&
937
- "slug" in obj &&
938
- "status" in obj &&
939
- "data" in obj &&
940
- "exists" in obj
941
- );
942
- }
943
-
944
- /**
945
- * Count the total number of references resolved in an entry.
946
- *
947
- * @param entry - Resolved entry to count
948
- * @returns Object with counts of content and media references
949
- */
950
- export function countResolvedReferences(
951
- entry: ResolvedContentEntry,
952
- ): {
953
- content: number;
954
- media: number;
955
- total: number;
956
- } {
957
- let content = 0;
958
- let media = 0;
959
-
960
- function count(value: unknown): void {
961
- if (value === null || value === undefined) {
962
- return;
963
- }
964
-
965
- if (typeof value === "object") {
966
- if (Array.isArray(value)) {
967
- for (const item of value) {
968
- count(item);
969
- }
970
- } else {
971
- const record = value as Record<string, unknown>;
972
-
973
- // Check if it's a resolved content entry
974
- if (isResolvedContentEntry(record)) {
975
- content++;
976
- count(record.data);
977
- }
978
- // Check if it's a resolved media reference
979
- else if (
980
- "storageId" in record &&
981
- "url" in record &&
982
- "mimeType" in record
983
- ) {
984
- media++;
985
- }
986
- // Otherwise recurse into nested objects
987
- else {
988
- for (const val of Object.values(record)) {
989
- count(val);
990
- }
991
- }
992
- }
993
- }
994
- }
995
-
996
- count(entry.data);
997
-
998
- return { content, media, total: content + media };
999
- }