@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.2

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 (241) hide show
  1. package/README.md +12 -0
  2. package/dist/_exports/_internal.d.ts +163 -0
  3. package/dist/_exports/_internal.d.ts.map +1 -0
  4. package/dist/_exports/_internal.js +332 -0
  5. package/dist/_exports/_internal.js.map +1 -0
  6. package/dist/_exports/agent.d.ts +65 -0
  7. package/dist/_exports/agent.d.ts.map +1 -0
  8. package/dist/_exports/agent.js +71 -0
  9. package/dist/_exports/agent.js.map +1 -0
  10. package/dist/_exports/comlink.d.ts +118 -0
  11. package/dist/_exports/comlink.d.ts.map +1 -0
  12. package/dist/_exports/comlink.js +69 -0
  13. package/dist/_exports/comlink.js.map +1 -0
  14. package/dist/authStore-CtE9bLHW.js +754 -0
  15. package/dist/authStore-CtE9bLHW.js.map +1 -0
  16. package/dist/authStore-DKFX8fn7.d.ts +3179 -0
  17. package/dist/authStore-DKFX8fn7.d.ts.map +1 -0
  18. package/dist/clientStore-BtE2-q2i.js +90 -0
  19. package/dist/clientStore-BtE2-q2i.js.map +1 -0
  20. package/dist/createSanityInstance-DUI1bGIg.d.ts +278 -0
  21. package/dist/createSanityInstance-DUI1bGIg.d.ts.map +1 -0
  22. package/dist/createStateSourceAction-BOKtqFNm.js +300 -0
  23. package/dist/createStateSourceAction-BOKtqFNm.js.map +1 -0
  24. package/dist/getEnv-D5RGkQLu.js +8 -0
  25. package/dist/getEnv-D5RGkQLu.js.map +1 -0
  26. package/dist/getNodeState-CR3__B2D.js +67 -0
  27. package/dist/getNodeState-CR3__B2D.js.map +1 -0
  28. package/dist/index.d.ts +3 -2332
  29. package/dist/index.js +4171 -4332
  30. package/dist/index.js.map +1 -1
  31. package/dist/logger-DAtJb5RQ.js +173 -0
  32. package/dist/logger-DAtJb5RQ.js.map +1 -0
  33. package/dist/object-CxXlSVg7.js +83 -0
  34. package/dist/object-CxXlSVg7.js.map +1 -0
  35. package/dist/queryStore-DPf9sN3V.js +1346 -0
  36. package/dist/queryStore-DPf9sN3V.js.map +1 -0
  37. package/dist/rolldown-runtime-vyAXikos.js +9 -0
  38. package/dist/setCleanupTimeout-BiM6q1KF.js +31 -0
  39. package/dist/setCleanupTimeout-BiM6q1KF.js.map +1 -0
  40. package/dist/telemetryManager-Dq_qCkeI.js +127 -0
  41. package/dist/telemetryManager-Dq_qCkeI.js.map +1 -0
  42. package/dist/version-BChjsi4u.js +9 -0
  43. package/dist/version-BChjsi4u.js.map +1 -0
  44. package/package.json +47 -52
  45. package/src/_exports/_internal.ts +27 -0
  46. package/src/_exports/agent.ts +19 -0
  47. package/src/_exports/comlink.ts +16 -0
  48. package/src/_exports/index.ts +125 -64
  49. package/src/access/checkPermissions.test.ts +55 -0
  50. package/src/access/checkPermissions.ts +54 -0
  51. package/src/agent/agentActions.test.ts +16 -60
  52. package/src/agent/agentActions.ts +25 -38
  53. package/src/applications/applications.test-d.ts +88 -0
  54. package/src/applications/applications.test.ts +130 -0
  55. package/src/applications/applications.ts +367 -0
  56. package/src/auth/authLogger.ts +30 -0
  57. package/src/auth/authStore.test.ts +194 -13
  58. package/src/auth/authStore.ts +58 -26
  59. package/src/auth/getOrganizationVerificationState.test.ts +18 -17
  60. package/src/auth/getOrganizationVerificationState.ts +4 -4
  61. package/src/auth/handleAuthCallback.test.ts +35 -1
  62. package/src/auth/handleAuthCallback.ts +25 -6
  63. package/src/auth/logout.test.ts +75 -2
  64. package/src/auth/logout.ts +23 -4
  65. package/src/auth/refreshStampedToken.test.ts +219 -469
  66. package/src/auth/refreshStampedToken.ts +79 -195
  67. package/src/auth/studioAuth.ts +1 -1
  68. package/src/auth/studioModeAuth.ts +2 -1
  69. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
  70. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
  71. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
  72. package/src/client/clientStore.test.ts +62 -37
  73. package/src/client/clientStore.ts +45 -75
  74. package/src/client/liveEvents.test.ts +151 -0
  75. package/src/client/liveEvents.ts +107 -0
  76. package/src/comlink/controller/actions/destroyController.test.ts +4 -1
  77. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
  78. package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
  79. package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
  80. package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
  81. package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
  82. package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
  83. package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
  84. package/src/comlink/node/actions/releaseNode.test.ts +7 -4
  85. package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
  86. package/src/comlink/node/getNodeState.test.ts +1 -1
  87. package/src/comlink/node/getNodeState.ts +2 -1
  88. package/src/comments/addonDatasetStore.test.ts +215 -0
  89. package/src/comments/addonDatasetStore.ts +249 -0
  90. package/src/comments/buildCommentThreads.test.ts +83 -0
  91. package/src/comments/buildCommentThreads.ts +54 -0
  92. package/src/comments/commentActions.test.ts +540 -0
  93. package/src/comments/commentActions.ts +555 -0
  94. package/src/comments/commentFieldPath.test.ts +40 -0
  95. package/src/comments/commentFieldPath.ts +44 -0
  96. package/src/comments/commentsConstants.ts +80 -0
  97. package/src/comments/commentsStore.test.ts +419 -0
  98. package/src/comments/commentsStore.ts +483 -0
  99. package/src/comments/normalizeComment.test.ts +116 -0
  100. package/src/comments/normalizeComment.ts +45 -0
  101. package/src/comments/observeComments.test.ts +193 -0
  102. package/src/comments/observeComments.ts +113 -0
  103. package/src/comments/reducers.test.ts +277 -0
  104. package/src/comments/reducers.ts +316 -0
  105. package/src/comments/types.ts +249 -0
  106. package/src/comments/weakenReferences.test.ts +47 -0
  107. package/src/comments/weakenReferences.ts +29 -0
  108. package/src/config/__tests__/handles.test.ts +18 -12
  109. package/src/config/handles.ts +25 -7
  110. package/src/config/sanityConfig.ts +40 -82
  111. package/src/datasets/datasets.test.ts +2 -2
  112. package/src/datasets/datasets.ts +10 -4
  113. package/src/document/actions.test.ts +115 -33
  114. package/src/document/actions.ts +212 -15
  115. package/src/document/applyDocumentActions.test.ts +35 -13
  116. package/src/document/applyDocumentActions.ts +30 -9
  117. package/src/document/documentConstants.ts +15 -0
  118. package/src/document/documentStore.concurrency.test.ts +1306 -0
  119. package/src/document/documentStore.test.ts +753 -273
  120. package/src/document/documentStore.ts +197 -75
  121. package/src/document/events.test.ts +57 -2
  122. package/src/document/events.ts +82 -26
  123. package/src/document/listen.test.ts +56 -0
  124. package/src/document/listen.ts +74 -21
  125. package/src/document/listenerEventOperators.test.ts +311 -0
  126. package/src/document/listenerEventOperators.ts +217 -0
  127. package/src/document/patchOperations.test.ts +76 -2
  128. package/src/document/patchOperations.ts +60 -1
  129. package/src/document/permissions.test.ts +107 -60
  130. package/src/document/permissions.ts +44 -19
  131. package/src/document/processActions/create.ts +138 -0
  132. package/src/document/processActions/delete.ts +100 -0
  133. package/src/document/processActions/discard.ts +63 -0
  134. package/src/document/processActions/edit.ts +154 -0
  135. package/src/document/processActions/processActions.ts +218 -0
  136. package/src/document/processActions/publish.ts +120 -0
  137. package/src/document/processActions/releaseArchive.ts +77 -0
  138. package/src/document/processActions/releaseCreate.ts +59 -0
  139. package/src/document/processActions/releaseDelete.ts +65 -0
  140. package/src/document/processActions/releaseEdit.ts +37 -0
  141. package/src/document/processActions/releasePublish.ts +45 -0
  142. package/src/document/processActions/releaseSchedule.ts +87 -0
  143. package/src/document/processActions/releaseUtil.ts +31 -0
  144. package/src/document/processActions/shared.ts +308 -0
  145. package/src/document/processActions/unpublish.ts +85 -0
  146. package/src/document/processActions.test.ts +1129 -121
  147. package/src/document/reducers.test.ts +772 -64
  148. package/src/document/reducers.ts +205 -51
  149. package/src/document/resourceRules.test.ts +178 -0
  150. package/src/document/resourceRules.ts +117 -0
  151. package/src/document/sharedListener.test.ts +16 -14
  152. package/src/document/sharedListener.ts +24 -8
  153. package/src/document/util.test.ts +46 -0
  154. package/src/document/util.ts +50 -0
  155. package/src/favorites/favorites.test.ts +5 -6
  156. package/src/installations/installations.test-d.ts +73 -0
  157. package/src/installations/installations.test.ts +62 -0
  158. package/src/installations/installations.ts +206 -0
  159. package/src/organization/organization.test-d.ts +36 -0
  160. package/src/organization/organization.test.ts +138 -0
  161. package/src/organization/organization.ts +138 -0
  162. package/src/organizations/organizations.test-d.ts +56 -0
  163. package/src/organizations/organizations.test.ts +150 -0
  164. package/src/organizations/organizations.ts +107 -0
  165. package/src/presence/bifurTransport.test.ts +201 -9
  166. package/src/presence/bifurTransport.ts +98 -14
  167. package/src/presence/paths.test.ts +70 -0
  168. package/src/presence/paths.ts +41 -0
  169. package/src/presence/presenceStore.test.ts +915 -24
  170. package/src/presence/presenceStore.ts +576 -104
  171. package/src/presence/types.ts +175 -3
  172. package/src/preview/previewProjectionUtils.test.ts +9 -22
  173. package/src/preview/previewProjectionUtils.ts +7 -3
  174. package/src/preview/types.ts +0 -15
  175. package/src/project/project.test-d.ts +37 -0
  176. package/src/project/project.test.ts +157 -12
  177. package/src/project/project.ts +138 -25
  178. package/src/projection/getProjectionState.test.ts +18 -20
  179. package/src/projection/getProjectionState.ts +23 -3
  180. package/src/projection/projectionQuery.ts +5 -5
  181. package/src/projection/projectionStore.test.ts +1 -9
  182. package/src/projection/resolveProjection.test.ts +1 -2
  183. package/src/projection/resolveProjection.ts +16 -0
  184. package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
  185. package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
  186. package/src/projects/projects.test-d.ts +32 -0
  187. package/src/projects/projects.test.ts +108 -42
  188. package/src/projects/projects.ts +63 -27
  189. package/src/query/queryStore.test.ts +259 -123
  190. package/src/query/queryStore.ts +85 -69
  191. package/src/query/reducers.test.ts +0 -42
  192. package/src/query/reducers.ts +2 -18
  193. package/src/releases/getPerspectiveState.test.ts +17 -23
  194. package/src/releases/getPerspectiveState.ts +8 -20
  195. package/src/releases/observeReleases.test.ts +127 -0
  196. package/src/releases/observeReleases.ts +102 -0
  197. package/src/releases/releasesStore.test.ts +100 -53
  198. package/src/releases/releasesStore.ts +69 -44
  199. package/src/releases/utils/sortReleases.test.ts +38 -2
  200. package/src/releases/utils/sortReleases.ts +15 -10
  201. package/src/store/createActionBinder.test.ts +109 -90
  202. package/src/store/createActionBinder.ts +36 -28
  203. package/src/store/createSanityInstance.test.ts +15 -13
  204. package/src/store/createSanityInstance.ts +14 -2
  205. package/src/store/createStateSourceAction.test.ts +64 -2
  206. package/src/store/createStateSourceAction.ts +38 -43
  207. package/src/store/createStoreInstance.test.ts +4 -2
  208. package/src/store/fetcherStore.test-d.ts +121 -0
  209. package/src/store/fetcherStore.test.ts +299 -0
  210. package/src/store/fetcherStore.ts +801 -0
  211. package/src/telemetry/environment.test.ts +119 -0
  212. package/src/telemetry/environment.ts +92 -0
  213. package/src/telemetry/events.ts +42 -0
  214. package/src/telemetry/initTelemetry.test.ts +432 -0
  215. package/src/telemetry/initTelemetry.ts +218 -0
  216. package/src/telemetry/telemetryManager.test.ts +327 -0
  217. package/src/telemetry/telemetryManager.ts +199 -0
  218. package/src/users/reducers.test.ts +3 -1
  219. package/src/users/reducers.ts +5 -6
  220. package/src/users/types.ts +4 -4
  221. package/src/users/usersStore.test.ts +16 -12
  222. package/src/users/usersStore.ts +5 -1
  223. package/src/utils/buildQuery.test.ts +14 -0
  224. package/src/utils/buildQuery.ts +17 -0
  225. package/src/utils/createFetcherStore.test.ts +7 -5
  226. package/src/utils/createFetcherStore.ts +8 -5
  227. package/src/utils/getStagingApiHost.test.ts +21 -0
  228. package/src/utils/getStagingApiHost.ts +14 -0
  229. package/src/utils/ids.test.ts +1 -29
  230. package/src/utils/ids.ts +0 -10
  231. package/src/utils/includeShape.ts +36 -0
  232. package/src/utils/isImportError.test.ts +72 -0
  233. package/src/utils/isImportError.ts +34 -0
  234. package/src/utils/logger.test.ts +12 -0
  235. package/src/utils/logger.ts +8 -3
  236. package/src/utils/object.test.ts +95 -0
  237. package/src/utils/object.ts +142 -0
  238. package/src/utils/setCleanupTimeout.ts +37 -0
  239. package/src/document/processActions.ts +0 -707
  240. package/src/utils/defineIntent.test.ts +0 -477
  241. package/src/utils/defineIntent.ts +0 -244
@@ -1,27 +1,199 @@
1
1
  import {type SanityClient} from '@sanity/client'
2
+ import {type Path} from '@sanity/types'
2
3
  import {type Observable} from 'rxjs'
3
4
 
5
+ import {type PerspectiveHandle} from '../config/sanityConfig'
4
6
  import {type SanityUser} from '../users/types'
5
7
 
6
- /** @public */
8
+ /**
9
+ * One end of a text selection: a Portable Text path plus a character offset
10
+ * within the span it addresses.
11
+ * @public
12
+ */
13
+ export interface PresenceSelectionPoint {
14
+ path: Path
15
+ offset: number
16
+ }
17
+
18
+ /**
19
+ * A Portable Text caret or selected range, as exchanged with the Studio.
20
+ *
21
+ * Structurally identical to `EditorSelection` from `@portabletext/editor`, and
22
+ * declared separately so that `@sanity/sdk` stays free of React dependencies. A
23
+ * value from the editor can be passed straight through, and vice versa.
24
+ *
25
+ * `offset` is a character offset within the span that `path` addresses.
26
+ * @public
27
+ */
28
+ export type PresenceSelection = {
29
+ anchor: PresenceSelectionPoint
30
+ focus: PresenceSelectionPoint
31
+ backward?: boolean
32
+ } | null
33
+
34
+ /**
35
+ * Somewhere a participant is, as it arrives over the wire.
36
+ *
37
+ * Prefer {@link DocumentPresence} from `getDocumentPresence`, which is scoped to one
38
+ * document, flattened for rendering, and types `path` accurately.
39
+ * @public
40
+ */
7
41
  export interface PresenceLocation {
8
42
  type: 'document'
43
+ /**
44
+ * The specific document the participant is in, so a draft, published, or version
45
+ * id rather than a bare document id.
46
+ */
9
47
  documentId: string
48
+ /**
49
+ * The focused field path, empty when they are in the document but not a field.
50
+ *
51
+ * Declared as `string[]`, but segments can also be numbers or `{_key: string}`
52
+ * objects when the focus is inside an array or a Portable Text field, because
53
+ * that is what other clients send. Guard for those rather than assuming strings.
54
+ * Widening the declared type is a breaking change, so it waits for the next major;
55
+ * {@link DocumentPresence.path} is typed correctly today.
56
+ */
10
57
  path: string[]
58
+ /**
59
+ * When the participant said they were last active, on *their* clock.
60
+ *
61
+ * Fine for display, unreliable for deciding whether someone is still here: it is
62
+ * subject to clock skew between machines. Presence already drops participants that
63
+ * stop announcing, so a session appearing here is one the SDK still considers
64
+ * live.
65
+ */
11
66
  lastActiveAt: string
67
+ /** A Portable Text caret, when the focused field is a Portable Text field. */
68
+ selection?: PresenceSelection
12
69
  }
13
70
 
14
- /** @public */
71
+ /**
72
+ * A location as it travels over the wire. Identical in shape to
73
+ * {@link PresenceLocation}, except `path` is a full `Path`, which is what is
74
+ * really exchanged. See the note on {@link PresenceLocation.path}.
75
+ *
76
+ * Internal: nothing in the public API accepts or returns this. Apps describe
77
+ * where they are with {@link ReportPresenceOptions} and read presence as
78
+ * {@link PresenceLocation}.
79
+ * @internal
80
+ */
81
+ export interface WirePresenceLocation extends Omit<PresenceLocation, 'path'> {
82
+ path: Path
83
+ }
84
+
85
+ /**
86
+ * Which specific document a presence id resolves to.
87
+ *
88
+ * Presence compares exact document ids, so a client in a draft and a client in the
89
+ * published document are in different places even for the same `documentId`. Pass
90
+ * the perspective you are editing under and the SDK resolves it; the React hooks
91
+ * take it from `ResourceProvider` when you do not pass one.
92
+ * @public
93
+ */
94
+ export interface PresencePerspectiveOptions extends PerspectiveHandle {
95
+ /** Live-edit documents have no draft, so presence resolves to the published id. */
96
+ liveEdit?: boolean
97
+ }
98
+
99
+ /**
100
+ * What an app reports about where the current user is.
101
+ *
102
+ * Omit `path` for document-level presence, or pass one for field-level presence.
103
+ * @public
104
+ */
105
+ export interface ReportPresenceOptions extends PresencePerspectiveOptions {
106
+ /**
107
+ * The document the user is in.
108
+ *
109
+ * Pass the plain document id; the perspective decides which specific document that
110
+ * resolves to. An already-specific id is accepted and re-resolved, so passing
111
+ * `drafts.abc` while the perspective is `published` reports the published document.
112
+ */
113
+ documentId: string
114
+ /**
115
+ * The focused field path. Supports keyed and numeric segments, so array items
116
+ * and Portable Text spans can be addressed.
117
+ */
118
+ path?: Path
119
+ /** A Portable Text caret, when the focused field is a Portable Text field. */
120
+ selection?: PresenceSelection
121
+ }
122
+
123
+ /**
124
+ * One participant's session and everywhere it currently is.
125
+ *
126
+ * Keyed by session, not by user: the same person in two tabs is two entries with
127
+ * the same `user`.
128
+ * @public
129
+ */
15
130
  export interface UserPresence {
16
131
  user: SanityUser
17
132
  locations: PresenceLocation[]
133
+ /** Identifies one tab. Stable while that tab is open, and not reused. */
134
+ sessionId: string
135
+ }
136
+
137
+ /**
138
+ * One participant at one location within a single document, flattened so it can
139
+ * be rendered directly against a field.
140
+ *
141
+ * Unlike {@link PresenceLocation.path}, `path` here is typed as a full `Path`, so
142
+ * keyed and numeric segments are described rather than cast away.
143
+ * @beta
144
+ */
145
+ export interface DocumentPresence {
146
+ user: SanityUser
147
+ /** Identifies one tab, so the same person in two tabs appears twice. */
18
148
  sessionId: string
149
+ /**
150
+ * The specific document id this participant is in: a draft, published, or
151
+ * version id, depending on which perspective they are editing.
152
+ */
153
+ documentId: string
154
+ /** The focused field path. Empty when they are in the document but no field. */
155
+ path: Path
156
+ /**
157
+ * When they said they were last active, on *their* clock. Fine for display,
158
+ * unreliable for liveness. See {@link PresenceLocation.lastActiveAt}.
159
+ */
160
+ lastActiveAt: string
161
+ /** The Portable Text caret, when they are focused in a Portable Text field. */
162
+ selection?: PresenceSelection
163
+ }
164
+
165
+ /** @beta */
166
+ export interface DocumentPresenceOptions extends PresencePerspectiveOptions {
167
+ /**
168
+ * The document to look at. Resolved through the perspective exactly as
169
+ * {@link ReportPresenceOptions.documentId} is, so reads match writes.
170
+ */
171
+ documentId: string
172
+ /**
173
+ * Narrows to participants at or below this field path. Omit it for everyone in
174
+ * the document.
175
+ */
176
+ path?: Path
177
+ /**
178
+ * By default a draft, its published version, and any release versions are
179
+ * treated as the same document, which is what document lists want. Set this to
180
+ * compare ids exactly, which is what field-level indicators want so that a
181
+ * draft and a release version do not bleed into each other.
182
+ */
183
+ excludeVersions?: boolean
19
184
  }
20
185
 
21
186
  /** @public */
22
187
  export type PresenceTransport = [
23
188
  incomingEvents$: Observable<TransportEvent>,
24
189
  dispatchMessage: (message: TransportMessage) => Observable<void>,
190
+ /**
191
+ * Emits an incrementing generation number each time the connection becomes
192
+ * live. Reconnects with backoff, and subscribing keeps the connection alive.
193
+ */
194
+ connections$: Observable<number>,
195
+ /** Emits when the page is going away, so a disconnect can be announced. */
196
+ unload$: Observable<void>,
25
197
  ]
26
198
 
27
199
  /** @public */
@@ -54,7 +226,7 @@ export interface DisconnectEvent {
54
226
  /** @public */
55
227
  export type TransportMessage =
56
228
  | {type: 'rollCall'}
57
- | {type: 'state'; locations: PresenceLocation[]}
229
+ | {type: 'state'; locations: WirePresenceLocation[]}
58
230
  | {type: 'disconnect'}
59
231
 
60
232
  /** @public */
@@ -58,7 +58,10 @@ describe('normalizeMedia', () => {
58
58
  })
59
59
 
60
60
  describe('transformProjectionToPreview', () => {
61
- const instance = createSanityInstance()
61
+ const instance = createSanityInstance({
62
+ projectId: 'test-project',
63
+ dataset: 'test-dataset',
64
+ })
62
65
 
63
66
  beforeEach(async () => {
64
67
  vi.clearAllMocks()
@@ -78,11 +81,7 @@ describe('transformProjectionToPreview', () => {
78
81
  media: null,
79
82
  }
80
83
 
81
- const result = transformProjectionToPreview(
82
- instance,
83
- {projectId: 'p', dataset: 'd'},
84
- projectionResult,
85
- )
84
+ const result = transformProjectionToPreview(instance, projectionResult)
86
85
 
87
86
  expect(result).toEqual({
88
87
  title: 'My Title',
@@ -101,11 +100,7 @@ describe('transformProjectionToPreview', () => {
101
100
  media: null,
102
101
  }
103
102
 
104
- const result = transformProjectionToPreview(
105
- instance,
106
- {projectId: 'p', dataset: 'd'},
107
- projectionResult,
108
- )
103
+ const result = transformProjectionToPreview(instance, projectionResult)
109
104
 
110
105
  expect(result.title).toBe('article: doc1')
111
106
  expect(result.subtitle).toBeUndefined()
@@ -121,11 +116,7 @@ describe('transformProjectionToPreview', () => {
121
116
  media: {type: 'image-asset', _ref: 'image-abc123-200x200-png', url: ''},
122
117
  }
123
118
 
124
- const result = transformProjectionToPreview(
125
- instance,
126
- {projectId: 'p', dataset: 'd'},
127
- projectionResult,
128
- )
119
+ const result = transformProjectionToPreview(instance, projectionResult)
129
120
 
130
121
  expect(result).toEqual({
131
122
  title: 'My Title',
@@ -152,11 +143,7 @@ describe('transformProjectionToPreview', () => {
152
143
  },
153
144
  }
154
145
 
155
- const result = transformProjectionToPreview(
156
- instance,
157
- {projectId: 'p', dataset: 'd'},
158
- projectionResult,
159
- )
146
+ const result = transformProjectionToPreview(instance, projectionResult)
160
147
 
161
148
  expect(result).toEqual({
162
149
  title: 'My Title',
@@ -181,7 +168,7 @@ describe('transformProjectionToPreview', () => {
181
168
 
182
169
  const resource = {mediaLibraryId: 'test-library'}
183
170
 
184
- transformProjectionToPreview(instance, resource, projectionResult)
171
+ transformProjectionToPreview(instance, projectionResult, resource)
185
172
 
186
173
  const {getClient} = await import('../client/clientStore')
187
174
  expect(getClient).toHaveBeenCalledWith(instance, {
@@ -1,10 +1,10 @@
1
1
  import {type SanityClient} from '@sanity/client'
2
2
  import {createImageUrlBuilder} from '@sanity/image-url'
3
- import {isObject} from 'lodash-es'
4
3
 
5
4
  import {getClient} from '../client/clientStore'
6
5
  import {type DocumentResource} from '../config/sanityConfig'
7
6
  import {type SanityInstance} from '../store/createSanityInstance'
7
+ import {isObject} from '../utils/object'
8
8
  import {SUBTITLE_CANDIDATES, TITLE_CANDIDATES} from './previewConstants'
9
9
  import {type PreviewQueryResult, type PreviewValue} from './types'
10
10
 
@@ -71,13 +71,17 @@ function findFirstDefined(
71
71
  */
72
72
  export function transformProjectionToPreview(
73
73
  instance: SanityInstance,
74
- resource: DocumentResource,
75
74
  projectionResult: PreviewQueryResult,
75
+ resource?: DocumentResource,
76
76
  ): PreviewValue {
77
77
  const title = findFirstDefined(TITLE_CANDIDATES, projectionResult.titleCandidates)
78
78
  const subtitle = findFirstDefined(SUBTITLE_CANDIDATES, projectionResult.subtitleCandidates, title)
79
79
 
80
- const client = getClient(instance, {apiVersion: API_VERSION, resource})
80
+ // Get a client for the resource (if provided) or use the instance config
81
+ const client = getClient(instance, {
82
+ apiVersion: API_VERSION,
83
+ resource,
84
+ })
81
85
 
82
86
  return {
83
87
  title: String(title || `${projectionResult._type}: ${projectionResult._id}`),
@@ -59,18 +59,3 @@ export interface PreviewValue {
59
59
  lastEditedDraftAt?: string
60
60
  }
61
61
  }
62
-
63
- /**
64
- * Represents the current state of a preview value along with a flag indicating whether
65
- * the preview data is still being fetched or is fully resolved.
66
- *
67
- * The tuple contains a preview value or null, and a boolean indicating if the data is
68
- * pending. A `true` value means a fetch is ongoing; `false` indicates that the
69
- * currently provided preview value is up-to-date.
70
- *
71
- * @public
72
- */
73
- export type ValuePending<T> = {
74
- data: T | null
75
- isPending: boolean
76
- }
@@ -0,0 +1,37 @@
1
+ import {expectTypeOf, test} from 'vitest'
2
+
3
+ import {type SanityInstance} from '../store/createSanityInstance'
4
+ import {type StateSource} from '../store/createStateSourceAction'
5
+ import {type FetcherSnapshot} from '../store/fetcherStore'
6
+ import {type Project, project, type ProjectMember} from './project'
7
+
8
+ const instance = {} as SanityInstance
9
+
10
+ test('project.resolveState — resolves the wide-boolean detail shape', () => {
11
+ expectTypeOf(project.resolveState(instance, {projectId: 'p'})).resolves.toEqualTypeOf<
12
+ Project<boolean, boolean>
13
+ >()
14
+ })
15
+
16
+ test('Project — literal flags narrow members and features', () => {
17
+ expectTypeOf<Project<true, true>['members']>().toEqualTypeOf<ProjectMember[]>()
18
+ expectTypeOf<Project<true, true>['features']>().toEqualTypeOf<string[]>()
19
+ expectTypeOf<keyof Project<false, false> & ('members' | 'features')>().toEqualTypeOf<never>()
20
+ })
21
+
22
+ test('Project<boolean, boolean> — members and features are optional', () => {
23
+ type Item = Project<boolean, boolean>
24
+ expectTypeOf<Item['members']>().toEqualTypeOf<ProjectMember[] | undefined>()
25
+ expectTypeOf<Item['features']>().toEqualTypeOf<string[] | undefined>()
26
+ })
27
+
28
+ test('project.getState — exposes the snapshot envelope', () => {
29
+ expectTypeOf(project.getState(instance, {projectId: 'p'})).toEqualTypeOf<
30
+ StateSource<FetcherSnapshot<Project<boolean, boolean>>>
31
+ >()
32
+ })
33
+
34
+ test('project — rejects non-boolean flag values', () => {
35
+ // @ts-expect-error includeMembers must be a boolean
36
+ void project.resolveState(instance, {includeMembers: 'yes'})
37
+ })
@@ -1,33 +1,178 @@
1
1
  import {type SanityClient} from '@sanity/client'
2
2
  import {of} from 'rxjs'
3
- import {describe, it} from 'vitest'
3
+ import {afterEach, beforeEach, describe, it} from 'vitest'
4
4
 
5
5
  import {getClientState} from '../client/clientStore'
6
- import {createSanityInstance} from '../store/createSanityInstance'
6
+ import {projects} from '../projects/projects'
7
+ import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
7
8
  import {type StateSource} from '../store/createStateSourceAction'
8
- import {resolveProject} from './project'
9
+ import {getProjectCacheKey, project} from './project'
9
10
 
10
11
  vi.mock('../client/clientStore')
11
12
 
12
13
  describe('project', () => {
13
- it('calls the `client.observable.projects.getById` method on the client and returns the result', async () => {
14
- const instance = createSanityInstance()
14
+ let instance: SanityInstance
15
15
 
16
- const project = {id: 'a'}
17
- const getById = vi.fn().mockReturnValue(of(project))
16
+ beforeEach(() => {
17
+ instance = createSanityInstance({projectId: 'p', dataset: 'd'})
18
+ })
19
+
20
+ afterEach(() => {
21
+ instance.dispose()
22
+ })
23
+
24
+ it('calls `client.observable.request` against `/projects/<id>` and returns the result', async () => {
25
+ const detail = {id: 'a'}
26
+ const request = vi.fn().mockReturnValue(of(detail))
18
27
 
19
28
  const mockClient = {
20
- observable: {
21
- projects: {getById} as unknown as SanityClient['observable']['projects'],
29
+ observable: {request} as unknown as SanityClient['observable'],
30
+ } as SanityClient
31
+
32
+ vi.mocked(getClientState).mockReturnValue({
33
+ observable: of(mockClient),
34
+ } as StateSource<SanityClient>)
35
+
36
+ const result = await project.resolveState(instance, {projectId: 'a'})
37
+ expect(result).toEqual(detail)
38
+ expect(request).toHaveBeenCalledWith({
39
+ uri: '/projects/a',
40
+ query: {includeMembers: 'true', includeFeatures: 'true'},
41
+ tag: 'projects.get',
42
+ })
43
+ })
44
+
45
+ it('serializes query params (booleans → strings) and respects flags', async () => {
46
+ const request = vi.fn().mockReturnValue(of({id: 'a'}))
47
+ const mockClient = {
48
+ observable: {request} as unknown as SanityClient['observable'],
49
+ } as SanityClient
50
+
51
+ vi.mocked(getClientState).mockReturnValue({
52
+ observable: of(mockClient),
53
+ } as StateSource<SanityClient>)
54
+
55
+ await project.resolveState(instance, {
56
+ projectId: 'a',
57
+ includeMembers: false,
58
+ includeFeatures: false,
59
+ })
60
+
61
+ expect(request).toHaveBeenCalledWith({
62
+ uri: '/projects/a',
63
+ query: {
64
+ includeMembers: 'false',
65
+ includeFeatures: 'false',
22
66
  },
67
+ tag: 'projects.get',
68
+ })
69
+ })
70
+
71
+ it('falls back to the instance projectId when none is provided in options', async () => {
72
+ const request = vi.fn().mockReturnValue(of({id: 'p'}))
73
+ const mockClient = {
74
+ observable: {request} as unknown as SanityClient['observable'],
75
+ } as SanityClient
76
+
77
+ vi.mocked(getClientState).mockReturnValue({
78
+ observable: of(mockClient),
79
+ } as StateSource<SanityClient>)
80
+
81
+ await project.resolveState(instance)
82
+ expect(request).toHaveBeenCalledWith({
83
+ uri: '/projects/p',
84
+ query: {includeMembers: 'true', includeFeatures: 'true'},
85
+ tag: 'projects.get',
86
+ })
87
+ })
88
+
89
+ it('pre-seeds a memberless detail read from the cached default list without fetching', async () => {
90
+ const request = vi.fn().mockReturnValue(
91
+ of([
92
+ {id: 'p1', displayName: 'One'},
93
+ {id: 'p2', displayName: 'Two'},
94
+ ]),
95
+ )
96
+ const mockClient = {
97
+ observable: {request} as unknown as SanityClient['observable'],
23
98
  } as SanityClient
24
99
 
25
100
  vi.mocked(getClientState).mockReturnValue({
26
101
  observable: of(mockClient),
27
102
  } as StateSource<SanityClient>)
28
103
 
29
- const result = await resolveProject(instance, {projectId: 'a'})
30
- expect(result).toEqual(project)
31
- expect(getById).toHaveBeenCalledWith('a')
104
+ await projects.resolveState(instance)
105
+ expect(request).toHaveBeenCalledTimes(1)
106
+
107
+ const source = project.getState(instance, {projectId: 'p1', includeMembers: false})
108
+ const unsubscribe = source.subscribe()
109
+ await new Promise((resolve) => setTimeout(resolve, 0))
110
+
111
+ expect(source.getCurrent()).toMatchObject({status: 'success', data: {id: 'p1'}})
112
+ expect(request).toHaveBeenCalledTimes(1)
113
+ unsubscribe()
114
+ })
115
+
116
+ it('does not pre-seed a detail read that includes members', async () => {
117
+ const list = [{id: 'p1', displayName: 'One'}]
118
+ const detail = {id: 'p1', displayName: 'One', members: []}
119
+ const request = vi.fn().mockReturnValueOnce(of(list)).mockReturnValueOnce(of(detail))
120
+ const mockClient = {
121
+ observable: {request} as unknown as SanityClient['observable'],
122
+ } as SanityClient
123
+
124
+ vi.mocked(getClientState).mockReturnValue({
125
+ observable: of(mockClient),
126
+ } as StateSource<SanityClient>)
127
+
128
+ await projects.resolveState(instance)
129
+
130
+ const result = await project.resolveState(instance, {projectId: 'p1'})
131
+ expect(result).toEqual(detail)
132
+ expect(request).toHaveBeenCalledTimes(2)
133
+ })
134
+ })
135
+
136
+ describe('project cache key generation', () => {
137
+ const mockInstance = {config: {projectId: 'p'}} as SanityInstance
138
+
139
+ it('default call includes :members and :features (both default-true)', () => {
140
+ expect(getProjectCacheKey(mockInstance)).toBe('project:p:members:features')
141
+ })
142
+
143
+ it('treats undefined and the matching default as the same key', () => {
144
+ expect(getProjectCacheKey(mockInstance)).toBe(
145
+ getProjectCacheKey(mockInstance, {includeMembers: true, includeFeatures: true}),
146
+ )
147
+ })
148
+
149
+ it('explicit includeFeatures: false drops the :features segment', () => {
150
+ expect(getProjectCacheKey(mockInstance, {includeFeatures: false})).toBe('project:p:members')
151
+ })
152
+
153
+ it('explicit includeMembers: false drops the :members segment', () => {
154
+ expect(getProjectCacheKey(mockInstance, {includeMembers: false})).toBe('project:p:features')
155
+ })
156
+
157
+ it('combines all segments in order', () => {
158
+ expect(
159
+ getProjectCacheKey(mockInstance, {
160
+ projectId: 'a',
161
+ includeMembers: true,
162
+ includeFeatures: true,
163
+ }),
164
+ ).toBe('project:a:members:features')
165
+ })
166
+
167
+ it('produces distinct keys for each meaningful option permutation', () => {
168
+ const keys = new Set([
169
+ getProjectCacheKey(mockInstance),
170
+ getProjectCacheKey(mockInstance, {includeMembers: false}),
171
+ getProjectCacheKey(mockInstance, {includeFeatures: false}),
172
+ getProjectCacheKey(mockInstance, {includeMembers: false, includeFeatures: false}),
173
+ getProjectCacheKey(mockInstance, {projectId: 'a'}),
174
+ getProjectCacheKey(mockInstance, {projectId: 'b'}),
175
+ ])
176
+ expect(keys.size).toBe(6)
32
177
  })
33
178
  })