@sanity/sdk 2.18.0 → 3.0.0-rc.1

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 (53) hide show
  1. package/dist/_chunks-dts/createGroqSearchFilter.d.ts +233 -12
  2. package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -1
  3. package/dist/_chunks-es/_internal.js +1 -0
  4. package/dist/_chunks-es/_internal.js.map +1 -1
  5. package/dist/_chunks-es/createGroqSearchFilter.js +284 -6
  6. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
  7. package/dist/_chunks-es/telemetryManager.js +2 -2
  8. package/dist/_exports/_internal.d.ts +2 -2
  9. package/dist/_exports/_internal.d.ts.map +1 -1
  10. package/dist/_exports/_internal.js +3 -1
  11. package/dist/index.d.ts +599 -185
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +492 -337
  14. package/dist/index.js.map +1 -1
  15. package/package.json +7 -7
  16. package/src/_exports/_internal.ts +11 -0
  17. package/src/_exports/index.ts +57 -12
  18. package/src/access/checkPermissions.test.ts +55 -0
  19. package/src/access/checkPermissions.ts +54 -0
  20. package/src/applications/applications.test-d.ts +83 -0
  21. package/src/applications/applications.test.ts +130 -0
  22. package/src/applications/applications.ts +364 -0
  23. package/src/applications/userApplications.test.ts +126 -0
  24. package/src/applications/userApplications.ts +261 -0
  25. package/src/auth/getOrganizationVerificationState.test.ts +7 -7
  26. package/src/auth/getOrganizationVerificationState.ts +4 -4
  27. package/src/document/documentStore.concurrency.test.ts +313 -22
  28. package/src/installations/installations.test-d.ts +68 -0
  29. package/src/installations/installations.test.ts +62 -0
  30. package/src/installations/installations.ts +202 -0
  31. package/src/organization/organization.test-d.ts +18 -84
  32. package/src/organization/organization.test.ts +11 -11
  33. package/src/organization/organization.ts +26 -54
  34. package/src/organizations/organizations.test-d.ts +11 -32
  35. package/src/organizations/organizations.test.ts +8 -8
  36. package/src/organizations/organizations.ts +29 -54
  37. package/src/project/project.test-d.ts +19 -75
  38. package/src/project/project.test.ts +57 -10
  39. package/src/project/project.ts +33 -52
  40. package/src/projects/projects.test-d.ts +15 -21
  41. package/src/projects/projects.test.ts +8 -8
  42. package/src/projects/projects.ts +25 -49
  43. package/src/store/fetcherStore.test-d.ts +121 -0
  44. package/src/store/fetcherStore.test.ts +299 -0
  45. package/src/store/fetcherStore.ts +801 -0
  46. package/src/utils/buildQuery.test.ts +14 -0
  47. package/src/utils/buildQuery.ts +17 -0
  48. package/src/utils/includeShape.ts +36 -0
  49. package/src/utils/setCleanupTimeout.ts +21 -8
  50. package/dist/_chunks-es/version.js +0 -7
  51. package/dist/_chunks-es/version.js.map +0 -1
  52. package/src/utils/defineIntent.test.ts +0 -477
  53. package/src/utils/defineIntent.ts +0 -244
@@ -0,0 +1,121 @@
1
+ import {of} from 'rxjs'
2
+ import {expectTypeOf, test} from 'vitest'
3
+
4
+ import {type SanityInstance} from './createSanityInstance'
5
+ import {
6
+ defineFetcher,
7
+ defineMutation,
8
+ type Fetcher,
9
+ type FetcherSnapshot,
10
+ type MutationResult,
11
+ } from './fetcherStore'
12
+
13
+ declare const instance: SanityInstance
14
+
15
+ interface Widget {
16
+ id: string
17
+ label: string
18
+ }
19
+
20
+ // No explicit generics anywhere below: `TData` is inferred from `fetch`'s
21
+ // `Observable<T>` and `TParams` from the annotated `getKey`/`fetch` params —
22
+ // the whole handle is inferred from the definition, TanStack-style.
23
+
24
+ // A list fetcher: no params, resolves to Widget[].
25
+ const widgets = defineFetcher({
26
+ name: 'test-d-widgets',
27
+ getKey: () => 'all',
28
+ fetch: () => () => of<Widget[]>([]),
29
+ })
30
+
31
+ // A detail fetcher: one string param, resolves to Widget.
32
+ const widget = defineFetcher({
33
+ name: 'test-d-widget',
34
+ getKey: (_i, id) => id,
35
+ fetch: () => (id: string) => of<Widget>({id, label: ''}),
36
+ })
37
+
38
+ test('the handle is inferred from the definition — no explicit generics', () => {
39
+ expectTypeOf(widgets).toEqualTypeOf<Fetcher<[], Widget[]>>()
40
+ expectTypeOf(widget).toEqualTypeOf<Fetcher<[id: string], Widget>>()
41
+ })
42
+
43
+ test('unannotated getKey params are inferred (not any) from the fetcher', () => {
44
+ defineFetcher({
45
+ name: 'test-d-getkey-infer',
46
+ // `id` is not annotated here — it must be inferred as `string` from `fetch`.
47
+ getKey: (_i, id) => {
48
+ expectTypeOf(id).toEqualTypeOf<string>()
49
+ return id
50
+ },
51
+ fetch: () => (id: string) => of<Widget>({id, label: ''}),
52
+ })
53
+ })
54
+
55
+ test('getState carries TData through the snapshot envelope (not unknown)', () => {
56
+ expectTypeOf(widgets.getState(instance).getCurrent()).toEqualTypeOf<FetcherSnapshot<Widget[]>>()
57
+ })
58
+
59
+ test('the snapshot discriminates data by status', () => {
60
+ const snap = widget.getState(instance, 'w1').getCurrent()
61
+ if (snap.status === 'success') {
62
+ expectTypeOf(snap.data).toEqualTypeOf<Widget>()
63
+ expectTypeOf(snap.dataUpdatedAt).toEqualTypeOf<number>()
64
+ }
65
+ if (snap.status === 'pending') {
66
+ expectTypeOf(snap.data).toEqualTypeOf<undefined>()
67
+ }
68
+ })
69
+
70
+ test('resolveState and refetch resolve to TData', () => {
71
+ expectTypeOf(widgets.resolveState(instance)).resolves.toEqualTypeOf<Widget[]>()
72
+ expectTypeOf(widget.refetch(instance, 'w1')).resolves.toEqualTypeOf<Widget>()
73
+ })
74
+
75
+ test('the params tuple is enforced per fetcher', () => {
76
+ // @ts-expect-error — detail fetcher requires an id
77
+ void widget.getState(instance)
78
+ // @ts-expect-error — id must be a string
79
+ void widget.getState(instance, 123)
80
+ // @ts-expect-error — list fetcher takes no params
81
+ void widgets.getState(instance, 'nope')
82
+ })
83
+
84
+ test('setData updater is typed to TData', () => {
85
+ widget.setData(instance, ['w1'], {id: 'w1', label: 'x'})
86
+ widget.setData(instance, ['w1'], (current) => {
87
+ expectTypeOf(current).toEqualTypeOf<Widget | undefined>()
88
+ return {id: 'w1', label: 'y'}
89
+ })
90
+ // @ts-expect-error — value must be a full Widget
91
+ widget.setData(instance, ['w1'], {id: 'w1'})
92
+ // @ts-expect-error — updater function must return TData
93
+ widget.setData(instance, ['w1'], () => 42)
94
+ })
95
+
96
+ test('defineMutation resolves to a MutationResult of TResult with a typed input', () => {
97
+ const createWidget = defineMutation<{label: string}, Widget>({
98
+ name: 'test-d-create-widget',
99
+ mutationFn: () => (input) => {
100
+ expectTypeOf(input).toEqualTypeOf<{label: string}>()
101
+ return of<Widget>({id: 'x', label: input.label})
102
+ },
103
+ })
104
+ expectTypeOf(createWidget(instance, {label: 'a'})).resolves.toEqualTypeOf<
105
+ MutationResult<Widget>
106
+ >()
107
+ })
108
+
109
+ test('CacheWriter enforces the target fetcher’s TData and TParams', () => {
110
+ defineMutation<void, void>({
111
+ name: 'test-d-write-through',
112
+ mutationFn: () => () => of(undefined),
113
+ onMutate: (write) => {
114
+ write(widget, ['w1'], {id: 'w1', label: 'ok'})
115
+ // @ts-expect-error — value must match the fetcher's TData (Widget)
116
+ write(widget, ['w1'], {nope: true})
117
+ // @ts-expect-error — params must match the fetcher's TParams ([string])
118
+ write(widget, [], {id: 'w1', label: 'ok'})
119
+ },
120
+ })
121
+ })
@@ -0,0 +1,299 @@
1
+ import {NEVER, of, startWith, Subject, throwError} from 'rxjs'
2
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
3
+
4
+ import {createSanityInstance, type SanityInstance} from './createSanityInstance'
5
+ import {defineFetcher, defineMutation, type Fetcher} from './fetcherStore'
6
+
7
+ const flush = () => new Promise((resolve) => setTimeout(resolve, 0))
8
+
9
+ describe('fetcherStore', () => {
10
+ let instance: SanityInstance
11
+
12
+ beforeEach(() => {
13
+ instance = createSanityInstance({projectId: 'p', dataset: 'd'})
14
+ vi.useRealTimers()
15
+ })
16
+
17
+ afterEach(() => {
18
+ instance.dispose()
19
+ })
20
+
21
+ it('fetches on subscribe and exposes a success snapshot', async () => {
22
+ const fetcher = defineFetcher<[number], string>({
23
+ name: 'basic',
24
+ getKey: (_i, id) => `${id}`,
25
+ fetch: () => (id) => of(`data-${id}`),
26
+ })
27
+
28
+ const data = await fetcher.resolveState(instance, 1)
29
+ expect(data).toBe('data-1')
30
+ expect(fetcher.getState(instance, 1).getCurrent()).toMatchObject({
31
+ status: 'success',
32
+ data: 'data-1',
33
+ isFetching: false,
34
+ })
35
+ })
36
+
37
+ it('serves fresh data without refetching, and revalidates once stale', async () => {
38
+ const fetchSpy = vi.fn((id: number) => of(`v-${id}`))
39
+ const fetcher = defineFetcher<[number], string>({
40
+ name: 'swr',
41
+ staleTime: 10_000,
42
+ getKey: (_i, id) => `${id}`,
43
+ fetch: () => fetchSpy,
44
+ })
45
+
46
+ const first = fetcher.getState(instance, 1)
47
+ const unsub1 = first.subscribe()
48
+ await vi.waitFor(() => expect(first.getCurrent().status).toBe('success'))
49
+ expect(fetchSpy).toHaveBeenCalledTimes(1)
50
+ unsub1()
51
+
52
+ // Re-subscribe within staleTime — fresh, so no refetch.
53
+ const second = fetcher.getState(instance, 1)
54
+ const unsub2 = second.subscribe()
55
+ await flush()
56
+ expect(fetchSpy).toHaveBeenCalledTimes(1)
57
+ unsub2()
58
+
59
+ // Invalidation forces staleness — an active subscriber revalidates.
60
+ const third = fetcher.getState(instance, 1)
61
+ const unsub3 = third.subscribe()
62
+ await flush()
63
+ fetcher.invalidate(instance, 1)
64
+ await vi.waitFor(() => expect(fetchSpy).toHaveBeenCalledTimes(2))
65
+ unsub3()
66
+ })
67
+
68
+ it('invalidate refetches an actively-subscribed entry', async () => {
69
+ let n = 0
70
+ const fetcher = defineFetcher<[], number>({
71
+ name: 'invalidate',
72
+ getKey: () => 'k',
73
+ fetch: () => () => of(++n),
74
+ })
75
+
76
+ const source = fetcher.getState(instance)
77
+ const unsub = source.subscribe()
78
+ await vi.waitFor(() => expect(source.getCurrent().data).toBe(1))
79
+
80
+ fetcher.invalidate(instance)
81
+ await vi.waitFor(() => expect(source.getCurrent().data).toBe(2))
82
+ unsub()
83
+ })
84
+
85
+ it('refetch bypasses staleness and resolves with fresh data', async () => {
86
+ let n = 0
87
+ const fetcher = defineFetcher<[], number>({
88
+ name: 'refetch',
89
+ staleTime: 60_000,
90
+ getKey: () => 'k',
91
+ fetch: () => () => of(++n),
92
+ })
93
+
94
+ expect(await fetcher.resolveState(instance)).toBe(1)
95
+ expect(await fetcher.refetch(instance)).toBe(2)
96
+ })
97
+
98
+ it('surfaces an initial failure as an error snapshot, background failure on the success arm', async () => {
99
+ let attempt = 0
100
+ const fetcher = defineFetcher<[], string>({
101
+ name: 'errors',
102
+ getKey: () => 'k',
103
+ fetch: () => () => (attempt++ === 0 ? throwError(() => new Error('boom')) : of('recovered')),
104
+ })
105
+
106
+ await expect(fetcher.resolveState(instance)).rejects.toThrow('boom')
107
+ expect(fetcher.getState(instance).getCurrent().status).toBe('error')
108
+
109
+ // Now succeed, then fail a background revalidation: data stays, error attaches.
110
+ await fetcher.refetch(instance)
111
+ expect(fetcher.getState(instance).getCurrent()).toMatchObject({
112
+ status: 'success',
113
+ data: 'recovered',
114
+ })
115
+ })
116
+
117
+ it('pre-seeds an entry via initialData without fetching when the seed is fresh', async () => {
118
+ const fetchSpy = vi.fn(() => of('fetched'))
119
+ const fetcher = defineFetcher<[], string>({
120
+ name: 'seeded',
121
+ staleTime: 10_000,
122
+ getKey: () => 'k',
123
+ fetch: () => fetchSpy,
124
+ initialData: () => ({data: 'seeded', dataUpdatedAt: Date.now()}),
125
+ })
126
+
127
+ const source = fetcher.getState(instance)
128
+ const unsub = source.subscribe()
129
+ await flush()
130
+
131
+ expect(source.getCurrent()).toMatchObject({status: 'success', data: 'seeded'})
132
+ expect(fetchSpy).not.toHaveBeenCalled()
133
+ unsub()
134
+ })
135
+
136
+ it('setData writes optimistically and can be undone', () => {
137
+ const fetcher = defineFetcher<[], string>({
138
+ name: 'setdata',
139
+ getKey: () => 'k',
140
+ fetch: () => () => NEVER,
141
+ })
142
+
143
+ const {undo} = fetcher.setData(instance, [], 'written')
144
+ expect(fetcher.getState(instance).getCurrent().data).toBe('written')
145
+
146
+ undo()
147
+ expect(fetcher.getState(instance).getCurrent().status).toBe('pending')
148
+ })
149
+
150
+ it('garbage-collects an entry written without a subscriber after gcTime', async () => {
151
+ const fetcher = defineFetcher<[], string>({
152
+ name: 'gc-orphan',
153
+ gcTime: 5,
154
+ getKey: () => 'k',
155
+ fetch: () => () => NEVER,
156
+ })
157
+
158
+ // No subscriber ever holds this entry (as an optimistic mutation write would),
159
+ // so only setData's own GC scheduling can clean it up.
160
+ fetcher.setData(instance, [], 'written')
161
+ expect(fetcher.getState(instance).getCurrent().data).toBe('written')
162
+
163
+ await vi.waitFor(() => expect(fetcher.getState(instance).getCurrent().status).toBe('pending'))
164
+ })
165
+
166
+ it('rolls back optimistic writes when a mutation fails', async () => {
167
+ const fetcher = defineFetcher<[], string>({
168
+ name: 'mutate-rollback',
169
+ getKey: () => 'k',
170
+ fetch: () => () => NEVER,
171
+ })
172
+ fetcher.setData(instance, [], 'server')
173
+
174
+ const failing = defineMutation<string, never>({
175
+ name: 'rename-fail',
176
+ mutationFn: () => () => throwError(() => new Error('rejected')),
177
+ onMutate: (write, value) => write(fetcher, [], value),
178
+ })
179
+
180
+ await expect(failing(instance, 'optimistic')).rejects.toThrow('rejected')
181
+ expect(fetcher.getState(instance).getCurrent().data).toBe('server')
182
+ })
183
+
184
+ it('reconciles tagged entries after a successful mutation', async () => {
185
+ let n = 0
186
+ const list = defineFetcher<[], number>({
187
+ name: 'list',
188
+ getKey: () => 'all',
189
+ tags: [{type: 'thing', id: 'LIST'}],
190
+ fetch: () => () => of(++n),
191
+ })
192
+
193
+ const source = list.getState(instance)
194
+ const unsub = source.subscribe()
195
+ await vi.waitFor(() => expect(source.getCurrent().data).toBe(1))
196
+
197
+ const create = defineMutation<void, {ok: true}>({
198
+ name: 'create-thing',
199
+ mutationFn: () => () => of({ok: true}),
200
+ invalidates: [{type: 'thing', id: 'LIST'}],
201
+ })
202
+
203
+ const {invalidated} = await create(instance, undefined)
204
+ await invalidated
205
+ expect(source.getCurrent().data).toBe(2)
206
+ unsub()
207
+ })
208
+
209
+ it('discards a revalidation result that lands after an optimistic write', async () => {
210
+ const server = new Subject<string>()
211
+ const fetcher: Fetcher<[], string> = defineFetcher<[], string>({
212
+ name: 'version-guard',
213
+ staleTime: 0,
214
+ getKey: () => 'k',
215
+ fetch: () => () => server.pipe(startWith('initial')),
216
+ })
217
+
218
+ const source = fetcher.getState(instance)
219
+ const unsub = source.subscribe()
220
+ await vi.waitFor(() => expect(source.getCurrent().data).toBe('initial'))
221
+
222
+ // Force a refetch (in flight, no value yet), then write optimistically.
223
+ const refetching = fetcher.refetch(instance)
224
+ fetcher.setData(instance, [], 'optimistic')
225
+ expect(source.getCurrent().data).toBe('optimistic')
226
+
227
+ // Late server value must NOT clobber the optimistic write.
228
+ server.next('late-server')
229
+ await refetching
230
+ expect(source.getCurrent().data).toBe('optimistic')
231
+ unsub()
232
+ })
233
+
234
+ it('settles a pre-empted in-flight fetch when a forced refetch replaces it', async () => {
235
+ const server = new Subject<string>()
236
+ const fetcher = defineFetcher<[], string>({
237
+ name: 'preempt',
238
+ getKey: () => 'k',
239
+ fetch: () => () => server,
240
+ })
241
+
242
+ // First refetch is in flight with no value yet; a second forced refetch
243
+ // pre-empts it. Both promises must settle rather than hang.
244
+ const first = fetcher.refetch(instance)
245
+ const second = fetcher.refetch(instance)
246
+
247
+ server.next('data')
248
+ server.complete()
249
+
250
+ await expect(first).resolves.toBe('data')
251
+ await expect(second).resolves.toBe('data')
252
+ })
253
+
254
+ it('clears isFetching when a discarded revalidation runs on a non-completing source', async () => {
255
+ const server = new Subject<string>()
256
+ const fetcher = defineFetcher<[], string>({
257
+ name: 'version-guard-fetching',
258
+ staleTime: 0,
259
+ getKey: () => 'k',
260
+ fetch: () => () => server,
261
+ })
262
+
263
+ const source = fetcher.getState(instance)
264
+ const unsub = source.subscribe()
265
+ await vi.waitFor(() => expect(source.getCurrent().isFetching).toBe(true))
266
+
267
+ // Optimistic write bumps the version while the fetch is still in flight.
268
+ fetcher.setData(instance, [], 'optimistic')
269
+
270
+ // The late server value is discarded (version guard) — and because the source
271
+ // never completes, isFetching must be cleared here, not left stuck true.
272
+ server.next('late')
273
+ await flush()
274
+ expect(source.getCurrent()).toMatchObject({data: 'optimistic', isFetching: false})
275
+ unsub()
276
+ })
277
+
278
+ it('polls on refetchInterval while an entry has subscribers', async () => {
279
+ let n = 0
280
+ const fetcher = defineFetcher<[], number>({
281
+ name: 'polling',
282
+ staleTime: 60_000,
283
+ refetchInterval: 10,
284
+ getKey: () => 'k',
285
+ fetch: () => () => of(++n),
286
+ })
287
+
288
+ const source = fetcher.getState(instance)
289
+ const unsub = source.subscribe()
290
+ await vi.waitFor(() => expect((source.getCurrent().data ?? 0) >= 1).toBe(true))
291
+
292
+ // Fresh per staleTime, so any further increase can only come from the interval.
293
+ const before = source.getCurrent().data ?? 0
294
+ await vi.waitFor(() => expect((source.getCurrent().data ?? 0) > before).toBe(true), {
295
+ timeout: 500,
296
+ })
297
+ unsub()
298
+ })
299
+ })