convex-cms 0.0.1 → 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 (267) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +99 -0
  3. package/admin-dist/nitro.json +15 -0
  4. package/admin-dist/public/assets/CmsEmptyState-CRswfTzk.js +5 -0
  5. package/admin-dist/public/assets/CmsPageHeader-CirpXndm.js +1 -0
  6. package/admin-dist/public/assets/CmsStatusBadge-CbEUpQu-.js +1 -0
  7. package/admin-dist/public/assets/CmsToolbar-BI2nZOXp.js +1 -0
  8. package/admin-dist/public/assets/ContentEntryEditor-CBeCyK_m.js +4 -0
  9. package/admin-dist/public/assets/ErrorState-BIVaWmom.js +1 -0
  10. package/admin-dist/public/assets/TaxonomyFilter-ChaY6Y_x.js +1 -0
  11. package/admin-dist/public/assets/_contentTypeId-DQ8k_Rvw.js +1 -0
  12. package/admin-dist/public/assets/_entryId-CKU_glsK.js +1 -0
  13. package/admin-dist/public/assets/alert-BXjTqrwQ.js +1 -0
  14. package/admin-dist/public/assets/badge-hvUOzpVZ.js +1 -0
  15. package/admin-dist/public/assets/circle-check-big-CF_pR17r.js +1 -0
  16. package/admin-dist/public/assets/command-DU82cJlt.js +1 -0
  17. package/admin-dist/public/assets/content-_LXl3pp7.js +1 -0
  18. package/admin-dist/public/assets/content-types-KjxaXGxY.js +2 -0
  19. package/admin-dist/public/assets/globals-CS6BZ0zp.css +1 -0
  20. package/admin-dist/public/assets/index-DNGIZHL-.js +1 -0
  21. package/admin-dist/public/assets/label-KNtpL71g.js +1 -0
  22. package/admin-dist/public/assets/link-2-Bw2aI4V4.js +1 -0
  23. package/admin-dist/public/assets/list-sYepHjt_.js +1 -0
  24. package/admin-dist/public/assets/main-CKj5yfEi.js +97 -0
  25. package/admin-dist/public/assets/media-Bkrkffm7.js +1 -0
  26. package/admin-dist/public/assets/new._contentTypeId-C3LstjNs.js +1 -0
  27. package/admin-dist/public/assets/plus-DUn8v_Xf.js +1 -0
  28. package/admin-dist/public/assets/rotate-ccw-DJEoHcRI.js +1 -0
  29. package/admin-dist/public/assets/scroll-area-DfIlT0in.js +1 -0
  30. package/admin-dist/public/assets/search-MuAUDJKR.js +1 -0
  31. package/admin-dist/public/assets/select-BD29IXCI.js +1 -0
  32. package/admin-dist/public/assets/settings-DmMyn_6A.js +1 -0
  33. package/admin-dist/public/assets/switch-h3Rrnl5i.js +1 -0
  34. package/admin-dist/public/assets/tabs-imc8h-Dp.js +1 -0
  35. package/admin-dist/public/assets/taxonomies-dAsrT65H.js +1 -0
  36. package/admin-dist/public/assets/textarea-BTy7nwzR.js +1 -0
  37. package/admin-dist/public/assets/trash-SAWKZZHv.js +1 -0
  38. package/admin-dist/public/assets/triangle-alert-E52Vfeuh.js +1 -0
  39. package/admin-dist/public/assets/useBreadcrumbLabel-BECBMCzM.js +1 -0
  40. package/admin-dist/public/assets/usePermissions-Basjs9BT.js +1 -0
  41. package/admin-dist/public/favicon.ico +0 -0
  42. package/admin-dist/server/_chunks/_libs/@date-fns/tz.mjs +217 -0
  43. package/admin-dist/server/_chunks/_libs/@floating-ui/core.mjs +719 -0
  44. package/admin-dist/server/_chunks/_libs/@floating-ui/dom.mjs +622 -0
  45. package/admin-dist/server/_chunks/_libs/@floating-ui/react-dom.mjs +292 -0
  46. package/admin-dist/server/_chunks/_libs/@floating-ui/utils.mjs +320 -0
  47. package/admin-dist/server/_chunks/_libs/@radix-ui/number.mjs +6 -0
  48. package/admin-dist/server/_chunks/_libs/@radix-ui/primitive.mjs +11 -0
  49. package/admin-dist/server/_chunks/_libs/@radix-ui/react-arrow.mjs +23 -0
  50. package/admin-dist/server/_chunks/_libs/@radix-ui/react-avatar.mjs +119 -0
  51. package/admin-dist/server/_chunks/_libs/@radix-ui/react-checkbox.mjs +270 -0
  52. package/admin-dist/server/_chunks/_libs/@radix-ui/react-collection.mjs +69 -0
  53. package/admin-dist/server/_chunks/_libs/@radix-ui/react-compose-refs.mjs +39 -0
  54. package/admin-dist/server/_chunks/_libs/@radix-ui/react-context.mjs +137 -0
  55. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dialog.mjs +325 -0
  56. package/admin-dist/server/_chunks/_libs/@radix-ui/react-direction.mjs +9 -0
  57. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dismissable-layer.mjs +210 -0
  58. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dropdown-menu.mjs +253 -0
  59. package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-guards.mjs +29 -0
  60. package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-scope.mjs +206 -0
  61. package/admin-dist/server/_chunks/_libs/@radix-ui/react-id.mjs +14 -0
  62. package/admin-dist/server/_chunks/_libs/@radix-ui/react-label.mjs +23 -0
  63. package/admin-dist/server/_chunks/_libs/@radix-ui/react-menu.mjs +812 -0
  64. package/admin-dist/server/_chunks/_libs/@radix-ui/react-popover.mjs +300 -0
  65. package/admin-dist/server/_chunks/_libs/@radix-ui/react-popper.mjs +286 -0
  66. package/admin-dist/server/_chunks/_libs/@radix-ui/react-portal.mjs +16 -0
  67. package/admin-dist/server/_chunks/_libs/@radix-ui/react-presence.mjs +128 -0
  68. package/admin-dist/server/_chunks/_libs/@radix-ui/react-primitive.mjs +141 -0
  69. package/admin-dist/server/_chunks/_libs/@radix-ui/react-roving-focus.mjs +224 -0
  70. package/admin-dist/server/_chunks/_libs/@radix-ui/react-scroll-area.mjs +721 -0
  71. package/admin-dist/server/_chunks/_libs/@radix-ui/react-select.mjs +1163 -0
  72. package/admin-dist/server/_chunks/_libs/@radix-ui/react-separator.mjs +28 -0
  73. package/admin-dist/server/_chunks/_libs/@radix-ui/react-slot.mjs +601 -0
  74. package/admin-dist/server/_chunks/_libs/@radix-ui/react-switch.mjs +152 -0
  75. package/admin-dist/server/_chunks/_libs/@radix-ui/react-tabs.mjs +189 -0
  76. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-callback-ref.mjs +11 -0
  77. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-controllable-state.mjs +69 -0
  78. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-effect-event.mjs +1 -0
  79. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-escape-keydown.mjs +17 -0
  80. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-is-hydrated.mjs +15 -0
  81. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-layout-effect.mjs +6 -0
  82. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-previous.mjs +14 -0
  83. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-size.mjs +39 -0
  84. package/admin-dist/server/_chunks/_libs/@radix-ui/react-visually-hidden.mjs +33 -0
  85. package/admin-dist/server/_chunks/_libs/@tanstack/history.mjs +409 -0
  86. package/admin-dist/server/_chunks/_libs/@tanstack/react-router.mjs +1711 -0
  87. package/admin-dist/server/_chunks/_libs/@tanstack/react-store.mjs +56 -0
  88. package/admin-dist/server/_chunks/_libs/@tanstack/router-core.mjs +4829 -0
  89. package/admin-dist/server/_chunks/_libs/@tanstack/store.mjs +134 -0
  90. package/admin-dist/server/_chunks/_libs/react-dom.mjs +10781 -0
  91. package/admin-dist/server/_chunks/_libs/react.mjs +513 -0
  92. package/admin-dist/server/_libs/aria-hidden.mjs +122 -0
  93. package/admin-dist/server/_libs/class-variance-authority.mjs +44 -0
  94. package/admin-dist/server/_libs/clsx.mjs +16 -0
  95. package/admin-dist/server/_libs/cmdk.mjs +315 -0
  96. package/admin-dist/server/_libs/convex.mjs +4841 -0
  97. package/admin-dist/server/_libs/cookie-es.mjs +58 -0
  98. package/admin-dist/server/_libs/croner.mjs +1 -0
  99. package/admin-dist/server/_libs/crossws.mjs +1 -0
  100. package/admin-dist/server/_libs/date-fns.mjs +1716 -0
  101. package/admin-dist/server/_libs/detect-node-es.mjs +1 -0
  102. package/admin-dist/server/_libs/get-nonce.mjs +9 -0
  103. package/admin-dist/server/_libs/h3-v2.mjs +277 -0
  104. package/admin-dist/server/_libs/h3.mjs +401 -0
  105. package/admin-dist/server/_libs/hookable.mjs +1 -0
  106. package/admin-dist/server/_libs/isbot.mjs +20 -0
  107. package/admin-dist/server/_libs/lucide-react.mjs +850 -0
  108. package/admin-dist/server/_libs/ohash.mjs +1 -0
  109. package/admin-dist/server/_libs/react-day-picker.mjs +2201 -0
  110. package/admin-dist/server/_libs/react-remove-scroll-bar.mjs +82 -0
  111. package/admin-dist/server/_libs/react-remove-scroll.mjs +328 -0
  112. package/admin-dist/server/_libs/react-style-singleton.mjs +69 -0
  113. package/admin-dist/server/_libs/rou3.mjs +8 -0
  114. package/admin-dist/server/_libs/seroval-plugins.mjs +58 -0
  115. package/admin-dist/server/_libs/seroval.mjs +1765 -0
  116. package/admin-dist/server/_libs/srvx.mjs +719 -0
  117. package/admin-dist/server/_libs/tailwind-merge.mjs +3010 -0
  118. package/admin-dist/server/_libs/tiny-invariant.mjs +12 -0
  119. package/admin-dist/server/_libs/tiny-warning.mjs +5 -0
  120. package/admin-dist/server/_libs/tslib.mjs +39 -0
  121. package/admin-dist/server/_libs/ufo.mjs +54 -0
  122. package/admin-dist/server/_libs/unctx.mjs +1 -0
  123. package/admin-dist/server/_libs/unstorage.mjs +1 -0
  124. package/admin-dist/server/_libs/use-callback-ref.mjs +66 -0
  125. package/admin-dist/server/_libs/use-sidecar.mjs +106 -0
  126. package/admin-dist/server/_libs/use-sync-external-store.mjs +139 -0
  127. package/admin-dist/server/_libs/zod.mjs +4223 -0
  128. package/admin-dist/server/_ssr/CmsEmptyState-DU7-7-mV.mjs +290 -0
  129. package/admin-dist/server/_ssr/CmsPageHeader-CseW0AHm.mjs +24 -0
  130. package/admin-dist/server/_ssr/CmsStatusBadge-B_pi4KCp.mjs +127 -0
  131. package/admin-dist/server/_ssr/CmsToolbar-X75ex6ek.mjs +49 -0
  132. package/admin-dist/server/_ssr/ContentEntryEditor-CepusRsA.mjs +3720 -0
  133. package/admin-dist/server/_ssr/ErrorState-cI-bKLez.mjs +89 -0
  134. package/admin-dist/server/_ssr/TaxonomyFilter-Bwrq0-cz.mjs +188 -0
  135. package/admin-dist/server/_ssr/_contentTypeId-BqYKEcLr.mjs +379 -0
  136. package/admin-dist/server/_ssr/_entryId-CRfnqeDf.mjs +161 -0
  137. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-BwDlABVk.mjs +4 -0
  138. package/admin-dist/server/_ssr/alert-CVt45UUP.mjs +92 -0
  139. package/admin-dist/server/_ssr/badge-6BsP37vG.mjs +125 -0
  140. package/admin-dist/server/_ssr/command-fy8epIKf.mjs +128 -0
  141. package/admin-dist/server/_ssr/config.server-D7JHDcDv.mjs +117 -0
  142. package/admin-dist/server/_ssr/content-B5RhL7uW.mjs +532 -0
  143. package/admin-dist/server/_ssr/content-types-BIOqCQYN.mjs +1166 -0
  144. package/admin-dist/server/_ssr/index-DHSHDPt1.mjs +193 -0
  145. package/admin-dist/server/_ssr/index.mjs +1275 -0
  146. package/admin-dist/server/_ssr/label-C8Dko1j7.mjs +22 -0
  147. package/admin-dist/server/_ssr/media-CSx3XttC.mjs +1832 -0
  148. package/admin-dist/server/_ssr/new._contentTypeId-DzanEZQM.mjs +144 -0
  149. package/admin-dist/server/_ssr/router-DDWcF-kt.mjs +1556 -0
  150. package/admin-dist/server/_ssr/scroll-area-bjPYwhXN.mjs +59 -0
  151. package/admin-dist/server/_ssr/select-BUhDDf4T.mjs +142 -0
  152. package/admin-dist/server/_ssr/settings-DAsxnw2q.mjs +348 -0
  153. package/admin-dist/server/_ssr/start-HYkvq4Ni.mjs +4 -0
  154. package/admin-dist/server/_ssr/switch-BgyRtQ1Z.mjs +31 -0
  155. package/admin-dist/server/_ssr/tabs-DzMdRB1A.mjs +628 -0
  156. package/admin-dist/server/_ssr/taxonomies-C8j8g5Q5.mjs +915 -0
  157. package/admin-dist/server/_ssr/textarea-9jNeYJSc.mjs +18 -0
  158. package/admin-dist/server/_ssr/trash-DYMxwhZB.mjs +291 -0
  159. package/admin-dist/server/_ssr/useBreadcrumbLabel-FNSAr2Ha.mjs +16 -0
  160. package/admin-dist/server/_ssr/usePermissions-BJGGahrJ.mjs +68 -0
  161. package/admin-dist/server/favicon.ico +0 -0
  162. package/admin-dist/server/index.mjs +627 -0
  163. package/dist/cli/index.js +0 -0
  164. package/dist/client/admin-config.d.ts +0 -1
  165. package/dist/client/admin-config.d.ts.map +1 -1
  166. package/dist/client/admin-config.js +0 -1
  167. package/dist/client/admin-config.js.map +1 -1
  168. package/dist/client/adminApi.d.ts.map +1 -1
  169. package/dist/client/agentTools.d.ts +1237 -135
  170. package/dist/client/agentTools.d.ts.map +1 -1
  171. package/dist/client/agentTools.js +33 -9
  172. package/dist/client/agentTools.js.map +1 -1
  173. package/dist/client/index.d.ts +1 -1
  174. package/dist/client/index.d.ts.map +1 -1
  175. package/dist/client/index.js.map +1 -1
  176. package/dist/component/_generated/component.d.ts +9 -0
  177. package/dist/component/_generated/component.d.ts.map +1 -1
  178. package/dist/component/mediaAssets.d.ts +35 -0
  179. package/dist/component/mediaAssets.d.ts.map +1 -1
  180. package/dist/component/mediaAssets.js +81 -0
  181. package/dist/component/mediaAssets.js.map +1 -1
  182. package/dist/test.d.ts.map +1 -1
  183. package/dist/test.js +2 -1
  184. package/dist/test.js.map +1 -1
  185. package/package.json +24 -9
  186. package/dist/component/auditLog.d.ts +0 -410
  187. package/dist/component/auditLog.d.ts.map +0 -1
  188. package/dist/component/auditLog.js +0 -607
  189. package/dist/component/auditLog.js.map +0 -1
  190. package/dist/component/types.d.ts +0 -4
  191. package/dist/component/types.d.ts.map +0 -1
  192. package/dist/component/types.js +0 -2
  193. package/dist/component/types.js.map +0 -1
  194. package/src/cli/commands/admin.ts +0 -104
  195. package/src/cli/index.ts +0 -21
  196. package/src/cli/utils/detectConvexUrl.ts +0 -54
  197. package/src/cli/utils/openBrowser.ts +0 -16
  198. package/src/client/admin-config.ts +0 -138
  199. package/src/client/adminApi.ts +0 -942
  200. package/src/client/agentTools.ts +0 -1311
  201. package/src/client/argTypes.ts +0 -316
  202. package/src/client/field-types.ts +0 -187
  203. package/src/client/index.ts +0 -1301
  204. package/src/client/queryBuilder.ts +0 -1100
  205. package/src/client/schema/codegen.ts +0 -500
  206. package/src/client/schema/defineContentType.ts +0 -501
  207. package/src/client/schema/index.ts +0 -169
  208. package/src/client/schema/schemaDrift.ts +0 -574
  209. package/src/client/schema/typedClient.ts +0 -688
  210. package/src/client/schema/types.ts +0 -666
  211. package/src/client/types.ts +0 -723
  212. package/src/client/workflows.ts +0 -141
  213. package/src/client/wrapper.ts +0 -4304
  214. package/src/component/_generated/api.ts +0 -140
  215. package/src/component/_generated/component.ts +0 -5029
  216. package/src/component/_generated/dataModel.ts +0 -60
  217. package/src/component/_generated/server.ts +0 -156
  218. package/src/component/authorization.ts +0 -647
  219. package/src/component/authorizationHooks.ts +0 -668
  220. package/src/component/bulkOperations.ts +0 -687
  221. package/src/component/contentEntries.ts +0 -1976
  222. package/src/component/contentEntryMutations.ts +0 -1223
  223. package/src/component/contentEntryValidation.ts +0 -707
  224. package/src/component/contentLock.ts +0 -550
  225. package/src/component/contentTypeMigration.ts +0 -1064
  226. package/src/component/contentTypeMutations.ts +0 -969
  227. package/src/component/contentTypes.ts +0 -346
  228. package/src/component/convex.config.ts +0 -44
  229. package/src/component/documentTypes.ts +0 -240
  230. package/src/component/eventEmitter.ts +0 -485
  231. package/src/component/exportImport.ts +0 -1169
  232. package/src/component/index.ts +0 -491
  233. package/src/component/lib/deepReferenceResolver.ts +0 -999
  234. package/src/component/lib/errors.ts +0 -816
  235. package/src/component/lib/index.ts +0 -145
  236. package/src/component/lib/mediaReferenceResolver.ts +0 -495
  237. package/src/component/lib/metadataExtractor.ts +0 -792
  238. package/src/component/lib/mutationAuth.ts +0 -199
  239. package/src/component/lib/queries.ts +0 -79
  240. package/src/component/lib/ragContentChunker.ts +0 -1371
  241. package/src/component/lib/referenceResolver.ts +0 -430
  242. package/src/component/lib/slugGenerator.ts +0 -262
  243. package/src/component/lib/slugUniqueness.ts +0 -333
  244. package/src/component/lib/softDelete.ts +0 -44
  245. package/src/component/localeFallbackChain.ts +0 -673
  246. package/src/component/localeFields.ts +0 -896
  247. package/src/component/mediaAssetMutations.ts +0 -725
  248. package/src/component/mediaAssets.ts +0 -932
  249. package/src/component/mediaFolderMutations.ts +0 -1046
  250. package/src/component/mediaUploadMutations.ts +0 -224
  251. package/src/component/mediaVariantMutations.ts +0 -900
  252. package/src/component/mediaVariants.ts +0 -793
  253. package/src/component/ragContentIndexer.ts +0 -1067
  254. package/src/component/rateLimitHooks.ts +0 -572
  255. package/src/component/roles.ts +0 -1360
  256. package/src/component/scheduledPublish.ts +0 -358
  257. package/src/component/schema.ts +0 -617
  258. package/src/component/taxonomies.ts +0 -949
  259. package/src/component/taxonomyMutations.ts +0 -1210
  260. package/src/component/trash.ts +0 -724
  261. package/src/component/userContext.ts +0 -898
  262. package/src/component/validation.ts +0 -1388
  263. package/src/component/validators.ts +0 -949
  264. package/src/component/versionMutations.ts +0 -392
  265. package/src/component/webhookTrigger.ts +0 -1922
  266. package/src/react/index.ts +0 -898
  267. 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
- }