@sanity/sdk 3.0.0-rc.1 → 3.0.0

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 (128) hide show
  1. package/README.md +12 -0
  2. package/dist/_exports/_internal.d.ts +71 -3
  3. package/dist/_exports/_internal.d.ts.map +1 -1
  4. package/dist/_exports/_internal.js +332 -24
  5. package/dist/_exports/_internal.js.map +1 -1
  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-C0o1NOxW.d.ts +3165 -0
  15. package/dist/authStore-C0o1NOxW.d.ts.map +1 -0
  16. package/dist/authStore-CtE9bLHW.js +754 -0
  17. package/dist/authStore-CtE9bLHW.js.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 -2335
  29. package/dist/index.js +4048 -3878
  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-C7yMHPUI.js +1346 -0
  36. package/dist/queryStore-C7yMHPUI.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-UIEiMtC0.js +127 -0
  41. package/dist/telemetryManager-UIEiMtC0.js.map +1 -0
  42. package/dist/version-D8RP_Q59.js +9 -0
  43. package/dist/version-D8RP_Q59.js.map +1 -0
  44. package/package.json +11 -17
  45. package/src/_exports/_internal.ts +1 -0
  46. package/src/_exports/agent.ts +19 -0
  47. package/src/_exports/comlink.ts +16 -0
  48. package/src/_exports/index.ts +45 -90
  49. package/src/applications/applications.test-d.ts +5 -0
  50. package/src/applications/applications.ts +5 -2
  51. package/src/auth/authMode.test.ts +0 -25
  52. package/src/auth/authMode.ts +3 -6
  53. package/src/auth/authStore.test.ts +1 -1
  54. package/src/auth/authStore.ts +0 -1
  55. package/src/comments/addonDatasetStore.test.ts +215 -0
  56. package/src/comments/addonDatasetStore.ts +249 -0
  57. package/src/comments/buildCommentThreads.test.ts +83 -0
  58. package/src/comments/buildCommentThreads.ts +54 -0
  59. package/src/comments/commentActions.test.ts +540 -0
  60. package/src/comments/commentActions.ts +555 -0
  61. package/src/comments/commentFieldPath.test.ts +40 -0
  62. package/src/comments/commentFieldPath.ts +44 -0
  63. package/src/comments/commentsConstants.ts +80 -0
  64. package/src/comments/commentsStore.test.ts +419 -0
  65. package/src/comments/commentsStore.ts +483 -0
  66. package/src/comments/normalizeComment.test.ts +116 -0
  67. package/src/comments/normalizeComment.ts +45 -0
  68. package/src/comments/observeComments.test.ts +193 -0
  69. package/src/comments/observeComments.ts +113 -0
  70. package/src/comments/reducers.test.ts +277 -0
  71. package/src/comments/reducers.ts +316 -0
  72. package/src/comments/types.ts +249 -0
  73. package/src/comments/weakenReferences.test.ts +47 -0
  74. package/src/comments/weakenReferences.ts +29 -0
  75. package/src/config/sanityConfig.ts +3 -75
  76. package/src/datasets/datasets.test.ts +44 -9
  77. package/src/datasets/datasets.ts +24 -23
  78. package/src/document/applyDocumentActions.test.ts +8 -12
  79. package/src/document/documentConstants.ts +17 -0
  80. package/src/document/documentStore.test.ts +173 -19
  81. package/src/document/documentStore.ts +36 -5
  82. package/src/document/patchOperations.test.ts +27 -0
  83. package/src/document/patchOperations.ts +10 -1
  84. package/src/document/permissions.test.ts +3 -1
  85. package/src/document/reducers.test.ts +357 -0
  86. package/src/document/reducers.ts +125 -27
  87. package/src/document/util.test.ts +46 -0
  88. package/src/document/util.ts +37 -1
  89. package/src/favorites/favorites.test.ts +64 -24
  90. package/src/favorites/favorites.ts +73 -22
  91. package/src/installations/installations.test-d.ts +5 -0
  92. package/src/installations/installations.ts +4 -0
  93. package/src/presence/bifurTransport.test.ts +155 -3
  94. package/src/presence/bifurTransport.ts +79 -12
  95. package/src/presence/paths.test.ts +70 -0
  96. package/src/presence/paths.ts +41 -0
  97. package/src/presence/presenceStore.test.ts +822 -2
  98. package/src/presence/presenceStore.ts +518 -102
  99. package/src/presence/types.ts +175 -3
  100. package/src/preview/types.ts +0 -25
  101. package/src/projection/projectionStore.test.ts +0 -8
  102. package/src/projection/types.ts +0 -8
  103. package/src/query/queryStore.ts +7 -6
  104. package/src/store/createActionBinder.test.ts +51 -29
  105. package/src/store/createActionBinder.ts +16 -62
  106. package/src/store/createSanityInstance.test.ts +0 -68
  107. package/src/store/createSanityInstance.ts +0 -63
  108. package/src/store/createStateSourceAction.ts +1 -1
  109. package/src/store/fetcherStore.test.ts +23 -1
  110. package/src/store/fetcherStore.ts +3 -3
  111. package/src/users/reducers.test.ts +0 -3
  112. package/dist/_chunks-dts/createGroqSearchFilter.d.ts +0 -1147
  113. package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +0 -1
  114. package/dist/_chunks-es/_internal.js +0 -134
  115. package/dist/_chunks-es/_internal.js.map +0 -1
  116. package/dist/_chunks-es/createGroqSearchFilter.js +0 -1864
  117. package/dist/_chunks-es/createGroqSearchFilter.js.map +0 -1
  118. package/dist/_chunks-es/telemetryManager.js +0 -93
  119. package/dist/_chunks-es/telemetryManager.js.map +0 -1
  120. package/dist/index.d.ts.map +0 -1
  121. package/src/applications/userApplications.test.ts +0 -126
  122. package/src/applications/userApplications.ts +0 -261
  123. package/src/preview/getPreviewState.test.ts +0 -137
  124. package/src/preview/getPreviewState.ts +0 -69
  125. package/src/preview/resolvePreview.test.ts +0 -64
  126. package/src/preview/resolvePreview.ts +0 -43
  127. package/src/utils/createFetcherStore.test.ts +0 -171
  128. package/src/utils/createFetcherStore.ts +0 -284
@@ -0,0 +1,249 @@
1
+ import {type SanityClient} from '@sanity/client'
2
+ import {
3
+ catchError,
4
+ distinctUntilChanged,
5
+ EMPTY,
6
+ filter,
7
+ firstValueFrom,
8
+ map,
9
+ type Observable,
10
+ of,
11
+ switchMap,
12
+ take,
13
+ } from 'rxjs'
14
+
15
+ import {getClient, getClientState} from '../client/clientStore'
16
+ import {
17
+ type DatasetResource,
18
+ type DocumentResource,
19
+ isDatasetResource,
20
+ } from '../config/sanityConfig'
21
+ import {bindActionByResource, type BoundResourceKey} from '../store/createActionBinder'
22
+ import {type SanityInstance} from '../store/createSanityInstance'
23
+ import {createStateSourceAction, type StateSource} from '../store/createStateSourceAction'
24
+ import {defineStore, type StoreContext} from '../store/defineStore'
25
+ import {
26
+ ADDON_DATASET_API_VERSION,
27
+ ADDON_DATASET_PROFILE,
28
+ COMMENTS_API_VERSION,
29
+ } from './commentsConstants'
30
+
31
+ interface AddonDatasetStoreState {
32
+ /**
33
+ * `unknown` until discovery answers. `missing` means the project has no
34
+ * comments dataset yet, which is the normal state until someone writes a
35
+ * first comment.
36
+ */
37
+ status: 'unknown' | 'resolved' | 'missing'
38
+ datasetName?: string
39
+ /** In flight provisioning, so concurrent first comments issue one POST. */
40
+ provisioning?: Promise<string>
41
+ }
42
+
43
+ /**
44
+ * Comments need a project and a dataset. There is nowhere to put them for a
45
+ * media library or a canvas, and no Studio to interoperate with either.
46
+ *
47
+ * @internal
48
+ */
49
+ export function assertDatasetResource(resource: DocumentResource): DatasetResource {
50
+ if (!isDatasetResource(resource)) {
51
+ throw new Error(
52
+ `Comments are only supported for dataset resources, received: ${JSON.stringify(resource)}`,
53
+ )
54
+ }
55
+ return resource
56
+ }
57
+
58
+ function isMissingOrForbidden(error: unknown): boolean {
59
+ if (typeof error !== 'object' || error === null || !('statusCode' in error)) return false
60
+ const {statusCode} = error as {statusCode?: unknown}
61
+ return statusCode === 403 || statusCode === 404
62
+ }
63
+
64
+ function requestAddonDataset(
65
+ client: SanityClient,
66
+ {projectId, dataset}: DatasetResource,
67
+ ): Observable<string | undefined> {
68
+ return client.observable
69
+ .request<{name: string}[] | undefined>({
70
+ uri: `/projects/${projectId}/datasets?datasetProfile=${ADDON_DATASET_PROFILE}&addonFor=${dataset}`,
71
+ tag: 'comments.addon-dataset.list',
72
+ })
73
+ .pipe(map((datasets) => datasets?.[0]?.name))
74
+ }
75
+
76
+ function discover(
77
+ instance: SanityInstance,
78
+ resource: DatasetResource,
79
+ ): Observable<string | undefined> {
80
+ return getClientState(instance, {
81
+ apiVersion: ADDON_DATASET_API_VERSION,
82
+ projectId: resource.projectId,
83
+ useProjectHostname: true,
84
+ }).observable.pipe(
85
+ switchMap((client) =>
86
+ requestAddonDataset(client, resource).pipe(
87
+ // A missing dataset and a dataset this user cannot see both mean there
88
+ // are no comments to read. Transient failures stay pending so a new
89
+ // authenticated client can retry discovery.
90
+ catchError((error: unknown) => (isMissingOrForbidden(error) ? of(undefined) : EMPTY)),
91
+ ),
92
+ ),
93
+ )
94
+ }
95
+
96
+ async function discoverOnce(
97
+ instance: SanityInstance,
98
+ resource: DatasetResource,
99
+ ): Promise<string | undefined> {
100
+ const client = await firstValueFrom(
101
+ getClientState(instance, {
102
+ apiVersion: ADDON_DATASET_API_VERSION,
103
+ projectId: resource.projectId,
104
+ useProjectHostname: true,
105
+ }).observable.pipe(take(1)),
106
+ )
107
+
108
+ try {
109
+ return await firstValueFrom(requestAddonDataset(client, resource))
110
+ } catch (error) {
111
+ if (isMissingOrForbidden(error)) return undefined
112
+ throw error
113
+ }
114
+ }
115
+
116
+ const addonDatasetStore = defineStore<AddonDatasetStoreState, BoundResourceKey>({
117
+ name: 'CommentsAddonDataset',
118
+ getInitialState: () => ({status: 'unknown'}),
119
+ initialize: ({instance, state, key}) => {
120
+ const resource = assertDatasetResource(key.resource)
121
+
122
+ const subscription = discover(instance, resource).subscribe((datasetName) =>
123
+ state.set(
124
+ 'setAddonDataset',
125
+ datasetName ? {status: 'resolved', datasetName} : {status: 'missing'},
126
+ ),
127
+ )
128
+
129
+ return () => subscription.unsubscribe()
130
+ },
131
+ })
132
+
133
+ /**
134
+ * The addon dataset's name: `undefined` while discovery is in flight, `null`
135
+ * when the project has none yet.
136
+ *
137
+ * @internal
138
+ */
139
+ export const getAddonDatasetState: (
140
+ instance: SanityInstance,
141
+ options: {resource?: DocumentResource},
142
+ ) => StateSource<string | null | undefined> = bindActionByResource(
143
+ addonDatasetStore,
144
+ createStateSourceAction(({state}: {state: AddonDatasetStoreState}) => {
145
+ if (state.status === 'unknown') return undefined
146
+ return state.datasetName ?? null
147
+ }),
148
+ )
149
+
150
+ /**
151
+ * Resolves the addon dataset, creating it if the project has none.
152
+ *
153
+ * Safe to call concurrently: the first call owns the request and later ones
154
+ * await the same promise. Provisioning is a one-time, project-wide side effect,
155
+ * so this is only called from the write path, when we know there is a comment
156
+ * to store.
157
+ *
158
+ * @internal
159
+ */
160
+ export const provisionAddonDataset: (
161
+ instance: SanityInstance,
162
+ options: {resource?: DocumentResource},
163
+ ) => Promise<string> = bindActionByResource(
164
+ addonDatasetStore,
165
+ ({instance, state, key}: StoreContext<AddonDatasetStoreState, BoundResourceKey>) => {
166
+ const current = state.get()
167
+ if (current.datasetName) return Promise.resolve(current.datasetName)
168
+ if (current.provisioning) return current.provisioning
169
+
170
+ const resource = assertDatasetResource(key.resource)
171
+
172
+ const provisioning = (async () => {
173
+ // Another user may have provisioned it since this client started up, in
174
+ // which case creating it again would fail. Ask before writing.
175
+ const existing = await discoverOnce(instance, resource)
176
+ if (existing) return existing
177
+
178
+ const client = getClient(instance, {
179
+ apiVersion: ADDON_DATASET_API_VERSION,
180
+ projectId: resource.projectId,
181
+ useProjectHostname: true,
182
+ })
183
+
184
+ const response = await client.request<{datasetName?: string} | undefined>({
185
+ uri: `/comments/${resource.dataset}/setup`,
186
+ method: 'POST',
187
+ tag: 'comments.addon-dataset.setup',
188
+ })
189
+
190
+ if (!response?.datasetName) {
191
+ throw new Error('Creating the comments addon dataset returned no dataset name.')
192
+ }
193
+
194
+ return response.datasetName
195
+ })()
196
+ .then((datasetName) => {
197
+ state.set('setAddonDataset', {
198
+ status: 'resolved',
199
+ datasetName,
200
+ provisioning: undefined,
201
+ })
202
+ return datasetName
203
+ })
204
+ .catch((error: unknown) => {
205
+ // Clear the in-flight promise so a retry is not permanently stuck on
206
+ // this failure.
207
+ state.set('clearProvisioning', {provisioning: undefined})
208
+ throw error
209
+ })
210
+
211
+ state.set('startProvisioning', {provisioning})
212
+
213
+ return provisioning
214
+ },
215
+ )
216
+
217
+ /**
218
+ * A client pointed at the addon dataset, or `null` while the project has none.
219
+ *
220
+ * Emits again whenever the dataset is discovered or provisioned, and whenever
221
+ * the auth token changes. Long-lived readers must follow it rather than holding
222
+ * a client, because the client store drops every cached client on a token
223
+ * change.
224
+ *
225
+ * @internal
226
+ */
227
+ export const observeAddonDatasetClient: (
228
+ instance: SanityInstance,
229
+ options: {resource?: DocumentResource},
230
+ ) => Observable<SanityClient | null> = bindActionByResource(
231
+ addonDatasetStore,
232
+ ({instance, key}: StoreContext<AddonDatasetStoreState, BoundResourceKey>) => {
233
+ const {projectId} = assertDatasetResource(key.resource)
234
+
235
+ return getAddonDatasetState(instance, {resource: key.resource}).observable.pipe(
236
+ distinctUntilChanged(),
237
+ filter((datasetName): datasetName is string | null => datasetName !== undefined),
238
+ switchMap((datasetName) =>
239
+ typeof datasetName === 'string'
240
+ ? getClientState(instance, {
241
+ apiVersion: COMMENTS_API_VERSION,
242
+ projectId,
243
+ dataset: datasetName,
244
+ }).observable
245
+ : of(null),
246
+ ),
247
+ )
248
+ },
249
+ )
@@ -0,0 +1,83 @@
1
+ import {describe, expect, it} from 'vitest'
2
+
3
+ import {buildCommentThreads} from './buildCommentThreads'
4
+ import {type Comment} from './types'
5
+
6
+ function comment(overrides: Partial<Comment> & Pick<Comment, 'id'>): Comment {
7
+ return {
8
+ createdAt: '2026-01-01T00:00:00Z',
9
+ authorId: 'user-1',
10
+ message: null,
11
+ threadId: 'thread-1',
12
+ status: 'open',
13
+ reactions: [],
14
+ documentId: 'doc-1',
15
+ documentType: 'author',
16
+ fieldPath: '',
17
+ ...overrides,
18
+ }
19
+ }
20
+
21
+ describe('buildCommentThreads', () => {
22
+ it('returns nothing for an empty list', () => {
23
+ expect(buildCommentThreads([])).toEqual([])
24
+ })
25
+
26
+ it('groups a parent with its replies, oldest reply first', () => {
27
+ const parent = comment({id: 'a', createdAt: '2026-01-01T00:00:00Z'})
28
+ const second = comment({id: 'c', createdAt: '2026-01-03T00:00:00Z', parentCommentId: 'a'})
29
+ const first = comment({id: 'b', createdAt: '2026-01-02T00:00:00Z', parentCommentId: 'a'})
30
+
31
+ expect(buildCommentThreads([second, first, parent])).toEqual([
32
+ {
33
+ threadId: 'thread-1',
34
+ fieldPath: '',
35
+ parentComment: parent,
36
+ replies: [first, second],
37
+ commentsCount: 3,
38
+ status: 'open',
39
+ lastActivityAt: '2026-01-03T00:00:00Z',
40
+ },
41
+ ])
42
+ })
43
+
44
+ it('takes fieldPath from the parent', () => {
45
+ const parent = comment({id: 'a', fieldPath: 'body[_key=="intro"].content'})
46
+
47
+ expect(buildCommentThreads([parent])[0].fieldPath).toBe('body[_key=="intro"].content')
48
+ })
49
+
50
+ it('falls back to the parent createdAt when there are no replies', () => {
51
+ const parent = comment({id: 'a', createdAt: '2026-02-02T00:00:00Z'})
52
+
53
+ expect(buildCommentThreads([parent])[0]).toMatchObject({
54
+ commentsCount: 1,
55
+ lastActivityAt: '2026-02-02T00:00:00Z',
56
+ replies: [],
57
+ })
58
+ })
59
+
60
+ it('drops replies whose parent is missing', () => {
61
+ // Happens between a parent being deleted and its replies disappearing.
62
+ const orphan = comment({id: 'b', parentCommentId: 'gone'})
63
+
64
+ expect(buildCommentThreads([orphan])).toEqual([])
65
+ })
66
+
67
+ it('keeps threads in the order their parents arrive', () => {
68
+ const newer = comment({id: 'a', threadId: 'thread-a', createdAt: '2026-01-05T00:00:00Z'})
69
+ const older = comment({id: 'b', threadId: 'thread-b', createdAt: '2026-01-01T00:00:00Z'})
70
+
71
+ expect(buildCommentThreads([newer, older]).map((thread) => thread.threadId)).toEqual([
72
+ 'thread-a',
73
+ 'thread-b',
74
+ ])
75
+ })
76
+
77
+ it('reports the parent status for the thread', () => {
78
+ const parent = comment({id: 'a', status: 'resolved'})
79
+ const reply = comment({id: 'b', parentCommentId: 'a', status: 'resolved'})
80
+
81
+ expect(buildCommentThreads([parent, reply])[0].status).toBe('resolved')
82
+ })
83
+ })
@@ -0,0 +1,54 @@
1
+ import {type Comment, type CommentThread} from './types'
2
+
3
+ /**
4
+ * Groups a flat comment list into threads.
5
+ *
6
+ * A thread's parent is the comment with no `parentCommentId`; replies point at
7
+ * the parent's `id`. Replies whose parent is missing from the input are
8
+ * dropped, which is what happens when a parent is deleted but its replies have
9
+ * not disappeared yet.
10
+ *
11
+ * Threads come back in the order their parents appear in `comments`, so passing
12
+ * the store's newest-first list gives newest-thread-first. Replies within a
13
+ * thread are always oldest first, since that is reading order.
14
+ *
15
+ * @internal
16
+ */
17
+ export function buildCommentThreads(comments: Comment[]): CommentThread[] {
18
+ const repliesByParent = new Map<string, Comment[]>()
19
+
20
+ for (const comment of comments) {
21
+ if (!comment.parentCommentId) continue
22
+ const existing = repliesByParent.get(comment.parentCommentId)
23
+ if (existing) {
24
+ existing.push(comment)
25
+ } else {
26
+ repliesByParent.set(comment.parentCommentId, [comment])
27
+ }
28
+ }
29
+
30
+ const threads: CommentThread[] = []
31
+
32
+ for (const parentComment of comments) {
33
+ if (parentComment.parentCommentId) continue
34
+
35
+ // The array is built above and owned here, so sorting in place is safe.
36
+ const replies = (repliesByParent.get(parentComment.id) ?? []).sort((a, b) =>
37
+ a.createdAt.localeCompare(b.createdAt),
38
+ )
39
+
40
+ const lastReply = replies[replies.length - 1]
41
+
42
+ threads.push({
43
+ threadId: parentComment.threadId,
44
+ fieldPath: parentComment.fieldPath,
45
+ parentComment,
46
+ replies,
47
+ commentsCount: replies.length + 1,
48
+ status: parentComment.status,
49
+ lastActivityAt: lastReply ? lastReply.createdAt : parentComment.createdAt,
50
+ })
51
+ }
52
+
53
+ return threads
54
+ }