@sanity/sdk 0.0.0-alpha.21 → 0.0.0-alpha.23

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 (127) hide show
  1. package/dist/index.d.ts +428 -325
  2. package/dist/index.js +1618 -1553
  3. package/dist/index.js.map +1 -1
  4. package/package.json +6 -7
  5. package/src/_exports/index.ts +31 -30
  6. package/src/auth/authStore.test.ts +149 -104
  7. package/src/auth/authStore.ts +51 -100
  8. package/src/auth/handleAuthCallback.test.ts +67 -34
  9. package/src/auth/handleAuthCallback.ts +8 -7
  10. package/src/auth/logout.test.ts +61 -29
  11. package/src/auth/logout.ts +26 -28
  12. package/src/auth/refreshStampedToken.test.ts +9 -9
  13. package/src/auth/refreshStampedToken.ts +62 -56
  14. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +5 -5
  15. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +45 -47
  16. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +4 -5
  17. package/src/auth/subscribeToStorageEventsAndSetToken.ts +22 -24
  18. package/src/client/clientStore.test.ts +131 -67
  19. package/src/client/clientStore.ts +117 -116
  20. package/src/comlink/controller/actions/destroyController.test.ts +38 -13
  21. package/src/comlink/controller/actions/destroyController.ts +11 -15
  22. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +56 -27
  23. package/src/comlink/controller/actions/getOrCreateChannel.ts +37 -35
  24. package/src/comlink/controller/actions/getOrCreateController.test.ts +27 -16
  25. package/src/comlink/controller/actions/getOrCreateController.ts +23 -22
  26. package/src/comlink/controller/actions/releaseChannel.test.ts +37 -13
  27. package/src/comlink/controller/actions/releaseChannel.ts +22 -21
  28. package/src/comlink/controller/comlinkControllerStore.test.ts +65 -36
  29. package/src/comlink/controller/comlinkControllerStore.ts +44 -5
  30. package/src/comlink/node/actions/getOrCreateNode.test.ts +31 -15
  31. package/src/comlink/node/actions/getOrCreateNode.ts +30 -29
  32. package/src/comlink/node/actions/releaseNode.test.ts +75 -55
  33. package/src/comlink/node/actions/releaseNode.ts +19 -21
  34. package/src/comlink/node/comlinkNodeStore.test.ts +6 -11
  35. package/src/comlink/node/comlinkNodeStore.ts +22 -5
  36. package/src/config/authConfig.ts +79 -0
  37. package/src/config/sanityConfig.ts +48 -0
  38. package/src/datasets/datasets.test.ts +2 -2
  39. package/src/datasets/datasets.ts +18 -5
  40. package/src/document/actions.test.ts +22 -10
  41. package/src/document/actions.ts +44 -56
  42. package/src/document/applyDocumentActions.test.ts +96 -36
  43. package/src/document/applyDocumentActions.ts +140 -99
  44. package/src/document/documentStore.test.ts +103 -155
  45. package/src/document/documentStore.ts +247 -237
  46. package/src/document/listen.ts +56 -55
  47. package/src/document/patchOperations.ts +0 -43
  48. package/src/document/permissions.test.ts +25 -12
  49. package/src/document/permissions.ts +11 -4
  50. package/src/document/processActions.test.ts +41 -8
  51. package/src/document/reducers.test.ts +87 -16
  52. package/src/document/reducers.ts +2 -2
  53. package/src/document/sharedListener.test.ts +34 -16
  54. package/src/document/sharedListener.ts +33 -11
  55. package/src/preview/getPreviewState.test.ts +40 -39
  56. package/src/preview/getPreviewState.ts +68 -56
  57. package/src/preview/previewConstants.ts +43 -0
  58. package/src/preview/previewQuery.test.ts +1 -1
  59. package/src/preview/previewQuery.ts +4 -5
  60. package/src/preview/previewStore.test.ts +13 -58
  61. package/src/preview/previewStore.ts +7 -21
  62. package/src/preview/resolvePreview.test.ts +33 -104
  63. package/src/preview/resolvePreview.ts +11 -21
  64. package/src/preview/subscribeToStateAndFetchBatches.test.ts +96 -97
  65. package/src/preview/subscribeToStateAndFetchBatches.ts +85 -81
  66. package/src/preview/util.ts +1 -0
  67. package/src/project/project.test.ts +3 -3
  68. package/src/project/project.ts +28 -5
  69. package/src/projection/getProjectionState.test.ts +69 -49
  70. package/src/projection/getProjectionState.ts +42 -50
  71. package/src/projection/projectionQuery.ts +1 -1
  72. package/src/projection/projectionStore.test.ts +13 -51
  73. package/src/projection/projectionStore.ts +6 -18
  74. package/src/projection/resolveProjection.test.ts +32 -127
  75. package/src/projection/resolveProjection.ts +15 -28
  76. package/src/projection/subscribeToStateAndFetchBatches.test.ts +105 -90
  77. package/src/projection/subscribeToStateAndFetchBatches.ts +94 -81
  78. package/src/projection/util.ts +2 -0
  79. package/src/projects/projects.test.ts +13 -4
  80. package/src/projects/projects.ts +6 -1
  81. package/src/query/queryStore.test.ts +10 -47
  82. package/src/query/queryStore.ts +151 -133
  83. package/src/query/queryStoreConstants.ts +2 -0
  84. package/src/store/createActionBinder.test.ts +153 -0
  85. package/src/store/createActionBinder.ts +176 -0
  86. package/src/store/createSanityInstance.test.ts +84 -0
  87. package/src/store/createSanityInstance.ts +124 -0
  88. package/src/store/createStateSourceAction.test.ts +196 -0
  89. package/src/store/createStateSourceAction.ts +260 -0
  90. package/src/store/createStoreInstance.test.ts +81 -0
  91. package/src/store/createStoreInstance.ts +80 -0
  92. package/src/store/createStoreState.test.ts +85 -0
  93. package/src/store/createStoreState.ts +92 -0
  94. package/src/store/defineStore.test.ts +18 -0
  95. package/src/store/defineStore.ts +81 -0
  96. package/src/users/reducers.test.ts +318 -0
  97. package/src/users/reducers.ts +88 -0
  98. package/src/users/types.ts +46 -4
  99. package/src/users/usersConstants.ts +4 -0
  100. package/src/users/usersStore.test.ts +350 -223
  101. package/src/users/usersStore.ts +285 -149
  102. package/src/utils/createFetcherStore.test.ts +6 -7
  103. package/src/utils/createFetcherStore.ts +150 -153
  104. package/src/{common/util.test.ts → utils/hashString.test.ts} +1 -1
  105. package/src/auth/fetchLoginUrls.test.ts +0 -163
  106. package/src/auth/fetchLoginUrls.ts +0 -74
  107. package/src/common/createLiveEventSubscriber.test.ts +0 -121
  108. package/src/common/createLiveEventSubscriber.ts +0 -55
  109. package/src/common/types.ts +0 -4
  110. package/src/instance/identity.test.ts +0 -46
  111. package/src/instance/identity.ts +0 -29
  112. package/src/instance/sanityInstance.test.ts +0 -77
  113. package/src/instance/sanityInstance.ts +0 -57
  114. package/src/instance/types.ts +0 -37
  115. package/src/preview/getPreviewProjection.ts +0 -45
  116. package/src/resources/README.md +0 -370
  117. package/src/resources/createAction.test.ts +0 -101
  118. package/src/resources/createAction.ts +0 -44
  119. package/src/resources/createResource.test.ts +0 -112
  120. package/src/resources/createResource.ts +0 -102
  121. package/src/resources/createStateSourceAction.test.ts +0 -114
  122. package/src/resources/createStateSourceAction.ts +0 -83
  123. package/src/resources/createStore.test.ts +0 -67
  124. package/src/resources/createStore.ts +0 -46
  125. package/src/store/createStore.test.ts +0 -108
  126. package/src/store/createStore.ts +0 -106
  127. /package/src/{common/util.ts → utils/hashString.ts} +0 -0
@@ -14,15 +14,14 @@ import {
14
14
  tap,
15
15
  } from 'rxjs/operators'
16
16
 
17
- import {type SanityInstance} from '../instance/types'
17
+ import {bindActionGlobally, type BoundStoreAction} from '../store/createActionBinder'
18
+ import {type SanityInstance} from '../store/createSanityInstance'
18
19
  import {
19
- type ActionContext,
20
- createAction,
21
- createInternalAction,
22
- type ResourceAction,
23
- } from '../resources/createAction'
24
- import {createResource} from '../resources/createResource'
25
- import {createStateSourceAction, type StateSource} from '../resources/createStateSourceAction'
20
+ createStateSourceAction,
21
+ type SelectorContext,
22
+ type StateSource,
23
+ } from '../store/createStateSourceAction'
24
+ import {defineStore, type StoreContext} from '../store/defineStore'
26
25
  import {insecureRandomId} from '../utils/ids'
27
26
 
28
27
  interface CreateFetcherStoreOptions<TParams extends unknown[], TData> {
@@ -38,7 +37,7 @@ interface CreateFetcherStoreOptions<TParams extends unknown[], TData> {
38
37
  * The function used to convert the params into keys that state related to
39
38
  * those params will be stored.
40
39
  */
41
- getKey: (...params: TParams) => string
40
+ getKey: (instance: SanityInstance, ...params: TParams) => string
42
41
  /**
43
42
  * Delay in ms before clearing state after the last subscription is removed.
44
43
  * This results in react components suspending again due to no previous state
@@ -53,6 +52,7 @@ interface CreateFetcherStoreOptions<TParams extends unknown[], TData> {
53
52
 
54
53
  interface StoreEntry<TParams extends unknown[], TData> {
55
54
  params: TParams
55
+ instance: SanityInstance
56
56
  key: string
57
57
  data?: TData
58
58
  error?: unknown
@@ -74,12 +74,12 @@ export interface FetcherStoreState<TParams extends unknown[], TData> {
74
74
  * @public
75
75
  */
76
76
  export interface FetcherStore<TParams extends unknown[], TData> {
77
- getState: ResourceAction<
77
+ getState: BoundStoreAction<
78
78
  FetcherStoreState<TParams, TData>,
79
79
  TParams,
80
80
  StateSource<TData | undefined>
81
81
  >
82
- resolveState: ResourceAction<FetcherStoreState<TParams, TData>, TParams, Promise<TData>>
82
+ resolveState: BoundStoreAction<FetcherStoreState<TParams, TData>, TParams, Promise<TData>>
83
83
  }
84
84
 
85
85
  /**
@@ -108,13 +108,13 @@ export function createFetcherStore<TParams extends unknown[], TData>({
108
108
  fetchThrottleInternal = 1000,
109
109
  stateExpirationDelay = 5000,
110
110
  }: CreateFetcherStoreOptions<TParams, TData>): FetcherStore<TParams, TData> {
111
- const store = createResource<FetcherStoreState<TParams, TData>>({
111
+ const store = defineStore<FetcherStoreState<TParams, TData>>({
112
112
  name,
113
113
  getInitialState: () => ({
114
114
  stateByParams: {},
115
115
  }),
116
- initialize() {
117
- const subscription = subscribeToSubscriptionsAndFetch(this)
116
+ initialize: (context) => {
117
+ const subscription = subscribeToSubscriptionsAndFetch(context)
118
118
  return () => subscription.unsubscribe()
119
119
  },
120
120
  })
@@ -125,160 +125,157 @@ export function createFetcherStore<TParams extends unknown[], TData>({
125
125
  * and if enough time has elapsed since the last fetch, we update the timestamp
126
126
  * and call the factory function for that key.
127
127
  */
128
- const subscribeToSubscriptionsAndFetch = createInternalAction(
129
- ({instance, state}: ActionContext<FetcherStoreState<TParams, TData>>) => {
130
- return function () {
131
- const factoryFn = getObservable(instance)
128
+ const subscribeToSubscriptionsAndFetch = ({
129
+ state,
130
+ }: StoreContext<FetcherStoreState<TParams, TData>>) => {
131
+ return state.observable
132
+ .pipe(
133
+ // Map the state to an array of [serialized, entry] pairs.
134
+ switchMap((s: FetcherStoreState<TParams, TData>) => {
135
+ const entries = Object.entries(s.stateByParams)
136
+ return entries.length > 0 ? from(entries) : EMPTY
137
+ }),
138
+ // Group by the serialized key.
139
+ groupBy(([key]) => key),
140
+ mergeMap((group$) =>
141
+ group$.pipe(
142
+ // Emit an initial value for pairwise comparisons.
143
+ startWith<[string, StoreEntry<TParams, TData> | undefined]>([group$.key, undefined]),
144
+ pairwise(),
145
+ // Trigger only when the subscriptions array grows.
146
+ filter(([[, prevEntry], [, currEntry]]) => {
147
+ const prevSubs = prevEntry?.subscriptions ?? []
148
+ const currSubs = currEntry?.subscriptions ?? []
149
+ return currSubs.length > prevSubs.length
150
+ }),
151
+ map(([, [, currEntry]]) => currEntry),
132
152
 
133
- return state.observable
134
- .pipe(
135
- // Map the state to an array of [serialized, entry] pairs.
136
- switchMap((s: FetcherStoreState<TParams, TData>) => {
137
- const entries = Object.entries(s.stateByParams)
138
- return entries.length > 0 ? from(entries) : EMPTY
153
+ // Only trigger if we haven't fetched recently.
154
+ filter((entry) => {
155
+ const lastFetch = entry?.lastFetchInitiatedAt
156
+ if (!lastFetch) return true
157
+ return Date.now() - new Date(lastFetch).getTime() >= fetchThrottleInternal
139
158
  }),
140
- // Group by the serialized key.
141
- groupBy(([key]) => key),
142
- mergeMap((group$) =>
143
- group$.pipe(
144
- // Emit an initial value for pairwise comparisons.
145
- startWith<[string, StoreEntry<TParams, TData> | undefined]>([
146
- group$.key,
147
- undefined,
148
- ]),
149
- pairwise(),
150
- // Trigger only when the subscriptions array grows.
151
- filter(([[, prevEntry], [, currEntry]]) => {
152
- const prevSubs = prevEntry?.subscriptions ?? []
153
- const currSubs = currEntry?.subscriptions ?? []
154
- return currSubs.length > prevSubs.length
155
- }),
156
- map(([, [, currEntry]]) => currEntry),
159
+ switchMap((entry) => {
160
+ // Retrieve params from the entry
161
+ if (!entry) return EMPTY
157
162
 
158
- // Only trigger if we haven't fetched recently.
159
- filter((entry) => {
160
- const lastFetch = entry?.lastFetchInitiatedAt
161
- if (!lastFetch) return true
162
- return Date.now() - new Date(lastFetch).getTime() >= fetchThrottleInternal
163
- }),
164
- switchMap((entry) => {
165
- // Retrieve params from the entry
166
- if (!entry) return EMPTY
163
+ // Record that a fetch is being initiated.
164
+ state.set('setLastFetchInitiatedAt', (prev: FetcherStoreState<TParams, TData>) => ({
165
+ stateByParams: {
166
+ ...prev.stateByParams,
167
+ [entry.key]: {
168
+ ...entry,
169
+ ...prev.stateByParams[entry.key],
170
+ lastFetchInitiatedAt: new Date().toISOString(),
171
+ },
172
+ },
173
+ }))
167
174
 
168
- // Record that a fetch is being initiated.
169
- state.set(
170
- 'setLastFetchInitiatedAt',
171
- (prev: FetcherStoreState<TParams, TData>) => ({
172
- stateByParams: {
173
- ...prev.stateByParams,
174
- [entry.key]: {
175
- ...entry,
176
- ...prev.stateByParams[entry.key],
177
- lastFetchInitiatedAt: new Date().toISOString(),
178
- },
175
+ const factoryFn = getObservable(entry.instance)
176
+ return factoryFn(...entry.params).pipe(
177
+ // the `createStateSourceAction` util requires the update
178
+ // to
179
+ delay(0, asapScheduler),
180
+ tap((data: TData) =>
181
+ state.set('setData', (prev: FetcherStoreState<TParams, TData>) => ({
182
+ stateByParams: {
183
+ ...prev.stateByParams,
184
+ [entry.key]: {
185
+ ...omit(entry, 'error'),
186
+ ...omit(prev.stateByParams[entry.key], 'error'),
187
+ data,
179
188
  },
180
- }),
181
- )
182
-
183
- return factoryFn(...entry.params).pipe(
184
- // the `createStateSourceAction` util requires the update
185
- // to
186
- delay(0, asapScheduler),
187
- tap((data: TData) =>
188
- state.set('setData', (prev: FetcherStoreState<TParams, TData>) => ({
189
- stateByParams: {
190
- ...prev.stateByParams,
191
- [entry.key]: {
192
- ...omit(entry, 'error'),
193
- ...omit(prev.stateByParams[entry.key], 'error'),
194
- data,
195
- },
196
- },
197
- })),
198
- ),
199
- catchError((error) => {
200
- state.set('setError', (prev) => ({
201
- stateByParams: {
202
- ...prev.stateByParams,
203
- [entry.key]: {
204
- ...entry,
205
- ...prev.stateByParams[entry.key],
206
- error,
207
- },
208
- },
209
- }))
189
+ },
190
+ })),
191
+ ),
192
+ catchError((error) => {
193
+ state.set('setError', (prev) => ({
194
+ stateByParams: {
195
+ ...prev.stateByParams,
196
+ [entry.key]: {
197
+ ...entry,
198
+ ...prev.stateByParams[entry.key],
199
+ error,
200
+ },
201
+ },
202
+ }))
210
203
 
211
- return EMPTY
212
- }),
213
- )
204
+ return EMPTY
214
205
  }),
215
- ),
216
- ),
217
- )
218
- .subscribe({
219
- error: (error) => state.set('setError', {error}),
220
- })
221
- }
222
- },
223
- )
206
+ )
207
+ }),
208
+ ),
209
+ ),
210
+ )
211
+ .subscribe({
212
+ error: (error) => state.set('setError', {error}),
213
+ })
214
+ }
224
215
 
225
- const getState = createStateSourceAction(store, {
226
- selector: ({stateByParams, error}: FetcherStoreState<TParams, TData>, ...params: TParams) => {
227
- if (error) throw error
228
- const key = getKey(...params)
229
- const entry = stateByParams[key]
230
- if (entry?.error) throw entry.error
231
- return entry?.data
232
- },
233
- onSubscribe: ({state}, ...params: TParams) => {
234
- const subscriptionId = insecureRandomId()
235
- const key = getKey(...params)
216
+ const getState = bindActionGlobally(
217
+ store,
218
+ createStateSourceAction({
219
+ selector: (
220
+ {
221
+ instance,
222
+ state: {stateByParams, error},
223
+ }: SelectorContext<FetcherStoreState<TParams, TData>>,
224
+ ...params: TParams
225
+ ) => {
226
+ if (error) throw error
227
+ const key = getKey(instance, ...params)
228
+ const entry = stateByParams[key]
229
+ if (entry?.error) throw entry.error
230
+ return entry?.data
231
+ },
232
+ onSubscribe: ({instance, state}, ...params: TParams) => {
233
+ const subscriptionId = insecureRandomId()
234
+ const key = getKey(instance, ...params)
236
235
 
237
- state.set('addSubscription', (prev: FetcherStoreState<TParams, TData>) => ({
238
- stateByParams: {
239
- ...prev.stateByParams,
240
- [key]: {
241
- ...prev.stateByParams[key],
242
- key,
243
- params: prev.stateByParams[key]?.params || params,
244
- subscriptions: [...(prev.stateByParams[key]?.subscriptions || []), subscriptionId],
236
+ state.set('addSubscription', (prev: FetcherStoreState<TParams, TData>) => ({
237
+ stateByParams: {
238
+ ...prev.stateByParams,
239
+ [key]: {
240
+ ...prev.stateByParams[key],
241
+ instance,
242
+ key,
243
+ params: prev.stateByParams[key]?.params || params,
244
+ subscriptions: [...(prev.stateByParams[key]?.subscriptions || []), subscriptionId],
245
+ },
245
246
  },
246
- },
247
- }))
247
+ }))
248
248
 
249
- return () => {
250
- setTimeout(() => {
251
- state.set('removeSubscription', (prev: FetcherStoreState<TParams, TData>) => {
252
- const entry = prev.stateByParams[key]
253
- if (!entry) return prev
249
+ return () => {
250
+ setTimeout(() => {
251
+ state.set('removeSubscription', (prev: FetcherStoreState<TParams, TData>) => {
252
+ const entry = prev.stateByParams[key]
253
+ if (!entry) return prev
254
254
 
255
- const newSubs = (entry.subscriptions || []).filter((id) => id !== subscriptionId)
256
- if (newSubs.length === 0) {
257
- return {stateByParams: omit(prev.stateByParams, key)}
258
- }
255
+ const newSubs = (entry.subscriptions || []).filter((id) => id !== subscriptionId)
256
+ if (newSubs.length === 0) {
257
+ return {stateByParams: omit(prev.stateByParams, key)}
258
+ }
259
259
 
260
- return {
261
- stateByParams: {
262
- ...prev.stateByParams,
263
- [key]: {
264
- ...entry,
265
- subscriptions: newSubs,
260
+ return {
261
+ stateByParams: {
262
+ ...prev.stateByParams,
263
+ [key]: {
264
+ ...entry,
265
+ subscriptions: newSubs,
266
+ },
266
267
  },
267
- },
268
- }
269
- })
270
- }, stateExpirationDelay)
271
- }
272
- },
273
- })
268
+ }
269
+ })
270
+ }, stateExpirationDelay)
271
+ }
272
+ },
273
+ }),
274
+ )
274
275
 
275
- const resolveState = createAction(store, () => {
276
- return function (...params: TParams) {
277
- return firstValueFrom(
278
- getState(this, ...params).observable.pipe(first((i) => i !== undefined)),
279
- )
280
- }
281
- })
276
+ const resolveState = bindActionGlobally(store, ({instance}, ...params: TParams) =>
277
+ firstValueFrom(getState(instance, ...params).observable.pipe(first((i) => i !== undefined))),
278
+ )
282
279
 
283
280
  return {getState, resolveState}
284
281
  }
@@ -1,6 +1,6 @@
1
1
  import {describe, expect, it} from 'vitest'
2
2
 
3
- import {hashString} from './util'
3
+ import {hashString} from './hashString'
4
4
 
5
5
  describe('hashString', () => {
6
6
  it('should generate consistent hashes for the same input', () => {
@@ -1,163 +0,0 @@
1
- import {createSanityInstance} from '../instance/sanityInstance'
2
- import {createResourceState} from '../resources/createResource'
3
- import {authStore} from './authStore'
4
- import {fetchLoginUrls} from './fetchLoginUrls'
5
-
6
- describe('fetchLoginUrls', () => {
7
- it('returns providers with updated URLs', async () => {
8
- const mockRequest = vi.fn().mockResolvedValue({
9
- providers: [
10
- {title: 'Provider A', url: 'https://auth.example.com/a'},
11
- {title: 'Provider B', url: 'https://auth.example.com/b'},
12
- ],
13
- })
14
- const clientFactory = vi.fn().mockReturnValue({request: mockRequest})
15
- const instance = createSanityInstance({
16
- projectId: 'p',
17
- dataset: 'd',
18
- auth: {clientFactory},
19
- })
20
- const state = createResourceState(authStore.getInitialState(instance))
21
- const providers = await fetchLoginUrls({instance, state})
22
-
23
- expect(providers.length).toBe(2)
24
- expect(providers[0].url).toContain('withSid=true')
25
- expect(providers[1].url).toContain('withSid=true')
26
- })
27
-
28
- it('caches the providers and early returns', async () => {
29
- const clientFactory = vi.fn()
30
- const instance = createSanityInstance({
31
- projectId: 'p',
32
- dataset: 'd',
33
- auth: {clientFactory},
34
- })
35
- const state = createResourceState(authStore.getInitialState(instance))
36
-
37
- const provider = {
38
- name: 'cached-provided',
39
- title: 'cached provider',
40
- url: 'https://auth.example.com#withSid=true',
41
- }
42
- state.set('setInitialProviders', {providers: [provider]})
43
-
44
- const providers = await fetchLoginUrls({instance, state})
45
-
46
- expect(providers.length).toBe(1)
47
- expect(providers[0].url).toContain('https://auth.example.com#withSid=true')
48
- })
49
-
50
- it('handles providers as a static array and merges/replaces accordingly', async () => {
51
- const mockRequest = vi.fn().mockResolvedValue({
52
- providers: [
53
- {title: 'Provider A', name: 'provider-a', url: 'https://auth.example.com/a'},
54
- {title: 'Provider B', name: 'provider-b', url: 'https://auth.example.com/b'},
55
- ],
56
- })
57
- const clientFactory = vi.fn().mockReturnValue({request: mockRequest})
58
- const instance = createSanityInstance({
59
- projectId: 'p',
60
- dataset: 'd',
61
- auth: {
62
- clientFactory,
63
- providers: [
64
- {
65
- title: 'Custom Provider B',
66
- name: 'custom-provider-b',
67
- url: 'https://auth.example.com/b',
68
- },
69
- {title: 'Provider C', name: 'provider-c', url: 'https://auth.example.com/c'},
70
- ],
71
- },
72
- })
73
- const state = createResourceState(authStore.getInitialState(instance))
74
- const providers = await fetchLoginUrls({instance, state})
75
-
76
- expect(providers.find((p) => p.title === 'Provider A')).toBeTruthy()
77
- expect(providers.find((p) => p.title === 'Custom Provider B')).toBeTruthy()
78
- expect(providers.find((p) => p.title === 'Provider C')).toBeTruthy()
79
- expect(providers.find((p) => p.title === 'Provider B')).toBeFalsy()
80
- })
81
-
82
- it('allows custom provider function modification', async () => {
83
- const mockRequest = vi.fn().mockResolvedValue({
84
- providers: [{title: 'Provider A', url: 'https://auth.example.com/a'}],
85
- })
86
- const clientFactory = vi.fn().mockReturnValue({request: mockRequest})
87
- const instance = createSanityInstance({
88
- projectId: 'p',
89
- dataset: 'd',
90
- auth: {
91
- clientFactory,
92
- providers: (defaults) => defaults.map((p) => ({...p, title: 'Modified ' + p.title})),
93
- },
94
- })
95
- const state = createResourceState(authStore.getInitialState(instance))
96
- const providers = await fetchLoginUrls({instance, state})
97
-
98
- expect(providers[0].title).toBe('Modified Provider A')
99
- })
100
-
101
- it('uses default providers if none are specified', async () => {
102
- const mockRequest = vi.fn().mockResolvedValue({
103
- providers: [{title: 'Provider A', url: 'https://auth.example.com/a'}],
104
- })
105
- const clientFactory = vi.fn().mockReturnValue({request: mockRequest})
106
- const instance = createSanityInstance({
107
- projectId: 'p',
108
- dataset: 'd',
109
- auth: {
110
- clientFactory,
111
- },
112
- })
113
- const state = createResourceState(authStore.getInitialState(instance))
114
- const providers = await fetchLoginUrls({instance, state})
115
-
116
- expect(providers.length).toBe(1)
117
- expect(providers[0].title).toBe('Provider A')
118
- })
119
-
120
- it('includes callbackUrl in provider URLs if set', async () => {
121
- const mockRequest = vi.fn().mockResolvedValue({
122
- providers: [{title: 'Provider A', url: 'https://auth.example.com/a'}],
123
- })
124
- const clientFactory = vi.fn().mockReturnValue({request: mockRequest})
125
- const instance = createSanityInstance({
126
- projectId: 'p',
127
- dataset: 'd',
128
- auth: {
129
- clientFactory,
130
- callbackUrl: 'http://localhost/callback',
131
- },
132
- })
133
- const state = createResourceState(authStore.getInitialState(instance))
134
- const providers = await fetchLoginUrls({instance, state})
135
- expect(providers[0].url).toContain('origin=http%3A%2F%2Flocalhost%2Fcallback')
136
- })
137
-
138
- it('should allow async custom provider function', async () => {
139
- const mockRequest = vi.fn().mockResolvedValue({
140
- providers: [{title: 'Provider A', url: 'https://auth.example.com/a'}],
141
- })
142
- const clientFactory = vi.fn().mockReturnValue({request: mockRequest})
143
- const instance = createSanityInstance({
144
- projectId: 'p',
145
- dataset: 'd',
146
- auth: {
147
- clientFactory,
148
- providers: async (defaults) => {
149
- await new Promise((r) => setTimeout(r, 10))
150
- return defaults.concat([
151
- {title: 'Provider C', name: 'provider-c', url: 'https://auth.example.com/c'},
152
- ])
153
- },
154
- },
155
- })
156
- const state = createResourceState(authStore.getInitialState(instance))
157
-
158
- const providers = await fetchLoginUrls({instance, state})
159
-
160
- expect(providers.length).toBe(2)
161
- expect(providers.some((p) => p.title === 'Provider C')).toBe(true)
162
- })
163
- })
@@ -1,74 +0,0 @@
1
- import {type AuthProvider} from '@sanity/client'
2
-
3
- import {createAction} from '../resources/createAction'
4
- import {DEFAULT_API_VERSION, REQUEST_TAG_PREFIX} from './authConstants'
5
- import {authStore} from './authStore'
6
- import {getDefaultLocation} from './utils'
7
-
8
- /**
9
- * @public
10
- */
11
- export const fetchLoginUrls = createAction(authStore, ({state}) => {
12
- const {callbackUrl, clientFactory, apiHost, customProviders} = state.get().options
13
- const client = clientFactory({
14
- apiVersion: DEFAULT_API_VERSION,
15
- requestTagPrefix: REQUEST_TAG_PREFIX,
16
- useProjectHostname: false,
17
- ...(apiHost && {apiHost}),
18
- })
19
-
20
- return async function () {
21
- const cachedProviders = state.get().providers
22
- if (cachedProviders) return cachedProviders
23
-
24
- const {providers: defaultProviders} = await client.request<{providers: AuthProvider[]}>({
25
- uri: '/auth/providers',
26
- tag: 'fetch-providers',
27
- })
28
-
29
- let providers: AuthProvider[]
30
-
31
- if (typeof customProviders === 'function') {
32
- providers = await customProviders(defaultProviders)
33
- } else if (!customProviders?.length) {
34
- providers = defaultProviders
35
- } else {
36
- const customProviderUrls = new Set(customProviders.map((p) => p.url))
37
- providers = defaultProviders
38
- .filter((official) => !customProviderUrls.has(official.url))
39
- .concat(customProviders)
40
- }
41
-
42
- const configuredProviders = providers.map((provider) => {
43
- const url = new URL(provider.url)
44
- const origin = new URL(
45
- callbackUrl
46
- ? new URL(callbackUrl, new URL(getDefaultLocation()).origin).toString()
47
- : getDefaultLocation(),
48
- )
49
-
50
- // `getDefaultLocation()` may be populated with an `sid` from a previous
51
- // failed login attempt and should be omitted from the next login URL
52
- const hashParams = new URLSearchParams(origin.hash.slice(1))
53
- hashParams.delete('sid')
54
- origin.hash = hashParams.toString()
55
- origin.searchParams.delete('sid')
56
- origin.searchParams.delete('url')
57
-
58
- // similarly, the origin may be populated with an `error` query param if
59
- // the auth provider redirects back to the application. this should also
60
- // be omitted from the origin sent
61
- origin.searchParams.delete('error')
62
-
63
- url.searchParams.set('origin', origin.toString())
64
- url.searchParams.set('withSid', 'true')
65
- url.searchParams.set('type', 'stampedToken')
66
-
67
- return {...provider, url: url.toString()}
68
- })
69
-
70
- state.set('fetchedLoginUrls', {providers: configuredProviders})
71
-
72
- return configuredProviders
73
- }
74
- })