@sanity/sdk 0.0.0-alpha.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 (36) hide show
  1. package/dist/index.d.ts +339 -0
  2. package/dist/index.js +492 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +77 -0
  5. package/src/_exports/index.ts +39 -0
  6. package/src/auth/authStore.test.ts +296 -0
  7. package/src/auth/authStore.ts +125 -0
  8. package/src/auth/getAuthStore.test.ts +14 -0
  9. package/src/auth/getInternalAuthStore.ts +20 -0
  10. package/src/auth/internalAuthStore.test.ts +334 -0
  11. package/src/auth/internalAuthStore.ts +519 -0
  12. package/src/client/getClient.test.ts +41 -0
  13. package/src/client/getClient.ts +13 -0
  14. package/src/client/getSubscribableClient.test.ts +71 -0
  15. package/src/client/getSubscribableClient.ts +17 -0
  16. package/src/client/store/actions/getClientEvents.test.ts +95 -0
  17. package/src/client/store/actions/getClientEvents.ts +33 -0
  18. package/src/client/store/actions/getOrCreateClient.test.ts +56 -0
  19. package/src/client/store/actions/getOrCreateClient.ts +40 -0
  20. package/src/client/store/actions/receiveToken.test.ts +18 -0
  21. package/src/client/store/actions/receiveToken.ts +31 -0
  22. package/src/client/store/clientStore.test.ts +152 -0
  23. package/src/client/store/clientStore.ts +98 -0
  24. package/src/documentList/documentListStore.test.ts +575 -0
  25. package/src/documentList/documentListStore.ts +269 -0
  26. package/src/documents/.keep +0 -0
  27. package/src/instance/identity.test.ts +46 -0
  28. package/src/instance/identity.ts +28 -0
  29. package/src/instance/sanityInstance.test.ts +66 -0
  30. package/src/instance/sanityInstance.ts +64 -0
  31. package/src/instance/types.d.ts +29 -0
  32. package/src/schema/schemaStore.test.ts +30 -0
  33. package/src/schema/schemaStore.ts +32 -0
  34. package/src/store/createStore.test.ts +108 -0
  35. package/src/store/createStore.ts +106 -0
  36. package/src/tsdoc.json +39 -0
@@ -0,0 +1,334 @@
1
+ import type {CurrentUser} from '@sanity/types'
2
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
3
+
4
+ import {getInternalAuthStore} from './getInternalAuthStore'
5
+ import {createInternalAuthStore} from './internalAuthStore'
6
+
7
+ // Define mockClient before vi.mock
8
+ const mockClient = {
9
+ request: vi.fn().mockResolvedValue({}),
10
+ }
11
+
12
+ // Move mock before any usage
13
+ vi.mock('@sanity/client', () => ({
14
+ createClient: () => mockClient,
15
+ }))
16
+
17
+ const instance = {
18
+ identity: {
19
+ id: '123abcStore',
20
+ projectId: 'test-project',
21
+ dataset: 'test-dataset',
22
+ },
23
+ config: {},
24
+ } as const
25
+
26
+ const mockStorage = {
27
+ getItem: vi.fn(),
28
+ setItem: vi.fn(),
29
+ removeItem: vi.fn(),
30
+ length: 0,
31
+ clear: vi.fn(),
32
+ key: vi.fn(),
33
+ }
34
+
35
+ // Add this mock setup at the top with other mocks
36
+ const mockWindow = {
37
+ localStorage: mockStorage,
38
+ addEventListener: vi.fn(),
39
+ removeEventListener: vi.fn(),
40
+ dispatchEvent: vi.fn(),
41
+ }
42
+
43
+ class MockStorageEvent {
44
+ key: string | null
45
+ newValue: string | null
46
+ storageArea: Storage | null
47
+
48
+ constructor(_type: string, eventInit?: StorageEventInit) {
49
+ this.key = eventInit?.key ?? null
50
+ this.newValue = eventInit?.newValue ?? null
51
+ this.storageArea = eventInit?.storageArea ?? null
52
+ }
53
+ }
54
+
55
+ // Mock StorageEvent globally
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ global.StorageEvent = MockStorageEvent as any
58
+
59
+ beforeEach(() => {
60
+ vi.clearAllMocks()
61
+ mockStorage.getItem.mockReturnValue(null)
62
+ // Set up window mock for each test
63
+ global.window = mockWindow as unknown as Window & typeof globalThis
64
+ })
65
+
66
+ afterEach(() => {
67
+ // Clean up window mock after each test
68
+ global.window = undefined as unknown as Window & typeof globalThis
69
+ })
70
+
71
+ describe('createInternalAuthStore', () => {
72
+ it('initializes with logged-out state when no token is present', () => {
73
+ const store = createInternalAuthStore(instance, {storageArea: mockStorage})
74
+ expect(store.getState().authState).toEqual({type: 'logged-out', isDestroyingSession: false})
75
+ })
76
+
77
+ it('initializes with logged-in state when token is provided', () => {
78
+ const store = createInternalAuthStore(instance, {
79
+ token: 'static-token',
80
+ storageArea: mockStorage,
81
+ })
82
+ expect(store.getState().authState).toEqual({
83
+ type: 'logged-in',
84
+ token: 'static-token',
85
+ currentUser: null,
86
+ })
87
+ })
88
+
89
+ it('initializes with logged-in state and defaults to local storage', () => {
90
+ global.window = mockWindow as unknown as typeof window
91
+
92
+ const store = createInternalAuthStore(instance, {
93
+ token: 'static-token',
94
+ })
95
+ expect(store.getState().authState).toEqual({
96
+ type: 'logged-in',
97
+ token: 'static-token',
98
+ currentUser: null,
99
+ })
100
+
101
+ // Cleanup
102
+ global.window = undefined as unknown as Window & typeof globalThis
103
+ })
104
+
105
+ it('handles successful login callback', async () => {
106
+ mockClient.request.mockResolvedValueOnce({token: 'new-token'})
107
+
108
+ const store = createInternalAuthStore(instance, {
109
+ storageArea: mockStorage,
110
+ initialLocationHref: 'http://localhost/#sid=callback-code',
111
+ })
112
+
113
+ const result = await store
114
+ .getState()
115
+ .handleCallback(new URL('http://localhost/#sid=callback-code').href)
116
+ expect(result).toBe('http://localhost/')
117
+ expect(mockStorage.setItem).toHaveBeenCalledWith(
118
+ '__sanity_auth_token_test-project_test-dataset',
119
+ JSON.stringify({token: 'new-token'}),
120
+ )
121
+ expect(store.getState().authState).toEqual({
122
+ type: 'logged-in',
123
+ token: 'new-token',
124
+ currentUser: {},
125
+ })
126
+ })
127
+
128
+ it('fetches login URLs', async () => {
129
+ const mockProviders = [
130
+ {name: 'google', title: 'Google', url: 'http://api.sanity.io/auth/google'},
131
+ ]
132
+ mockClient.request.mockResolvedValueOnce({providers: mockProviders})
133
+
134
+ const store = createInternalAuthStore(instance, {storageArea: mockStorage})
135
+ const providers = await store.getState().getLoginUrls()
136
+
137
+ expect(providers[0]).toMatchObject({
138
+ name: 'google',
139
+ title: 'Google',
140
+ url: expect.stringContaining('http://api.sanity.io/auth/google'),
141
+ })
142
+ })
143
+
144
+ it('handles logout', async () => {
145
+ mockStorage.getItem.mockReturnValueOnce(JSON.stringify({token: 'stored-token'}))
146
+ const store = createInternalAuthStore(instance, {storageArea: mockStorage})
147
+
148
+ await store.getState().logout()
149
+
150
+ expect(mockClient.request).toHaveBeenCalledWith({
151
+ uri: '/auth/logout',
152
+ method: 'POST',
153
+ })
154
+ expect(mockStorage.removeItem).toHaveBeenCalledWith(
155
+ '__sanity_auth_token_test-project_test-dataset',
156
+ )
157
+ expect(store.getState().authState).toEqual({
158
+ type: 'logged-out',
159
+ isDestroyingSession: false,
160
+ })
161
+ })
162
+
163
+ it('fetches current user after login', async () => {
164
+ const mockUser: CurrentUser = {
165
+ id: 'user-123',
166
+ name: 'Test User',
167
+ email: 'test@example.com',
168
+ role: 'developer',
169
+ roles: [{name: 'developer', title: 'Developer'}],
170
+ }
171
+
172
+ mockClient.request.mockResolvedValueOnce({token: 'new-token'}).mockResolvedValueOnce(mockUser)
173
+
174
+ const store = createInternalAuthStore(instance, {
175
+ storageArea: mockStorage,
176
+ initialLocationHref: 'http://localhost/#sid=callback-code',
177
+ })
178
+
179
+ await store.getState().handleCallback(new URL('http://localhost/#sid=callback-code').href)
180
+
181
+ expect(mockClient.request).toHaveBeenCalledWith({
182
+ uri: '/users/me',
183
+ method: 'GET',
184
+ })
185
+
186
+ expect(store.getState().authState).toEqual({
187
+ type: 'logged-in',
188
+ token: 'new-token',
189
+ currentUser: mockUser,
190
+ })
191
+ })
192
+
193
+ it('handles error during token exchange', async () => {
194
+ mockClient.request.mockRejectedValueOnce(new Error('Token exchange failed'))
195
+
196
+ const store = createInternalAuthStore(instance, {
197
+ storageArea: mockStorage,
198
+ initialLocationHref: 'http://localhost/#sid=callback-code',
199
+ })
200
+
201
+ const result = await store
202
+ .getState()
203
+ .handleCallback(new URL('http://localhost/#sid=callback-code').href)
204
+
205
+ expect(result).toBe(false)
206
+ expect(store.getState().authState).toEqual({
207
+ type: 'error',
208
+ error: new Error('Token exchange failed'),
209
+ })
210
+ })
211
+
212
+ it('handles error during user fetch', async () => {
213
+ mockClient.request
214
+ .mockResolvedValueOnce({token: 'new-token'})
215
+ .mockRejectedValueOnce(new Error('User fetch failed'))
216
+
217
+ const store = createInternalAuthStore(instance, {
218
+ storageArea: mockStorage,
219
+ initialLocationHref: 'http://localhost/#sid=callback-code',
220
+ })
221
+
222
+ await store.getState().handleCallback(new URL('http://localhost/#sid=callback-code').href)
223
+
224
+ // Add a small delay to allow the async user fetch to complete
225
+ await new Promise((resolve) => setTimeout(resolve, 0))
226
+
227
+ expect(store.getState().authState).toEqual({
228
+ type: 'error',
229
+ token: 'new-token',
230
+ currentUser: null,
231
+ error: new Error('User fetch failed'),
232
+ })
233
+ })
234
+
235
+ it('handles custom providers function', async () => {
236
+ const customProvider = {
237
+ name: 'custom',
238
+ title: 'Custom',
239
+ url: 'http://custom.auth',
240
+ }
241
+
242
+ mockClient.request.mockResolvedValueOnce({
243
+ providers: [{name: 'google', title: 'Google', url: 'http://api.sanity.io/auth/google'}],
244
+ })
245
+
246
+ const store = createInternalAuthStore(instance, {
247
+ storageArea: mockStorage,
248
+ providers: (defaultProviders) => [...defaultProviders, customProvider],
249
+ })
250
+
251
+ const providers = await store.getState().getLoginUrls()
252
+
253
+ expect(providers).toHaveLength(2)
254
+ expect(providers[1]).toMatchObject({
255
+ name: 'custom',
256
+ title: 'Custom',
257
+ url: expect.stringContaining('http://custom.auth'),
258
+ })
259
+ })
260
+
261
+ it('handles logout during in-flight logout', async () => {
262
+ mockStorage.getItem.mockReturnValueOnce(JSON.stringify({token: 'stored-token'}))
263
+ const store = createInternalAuthStore(instance, {storageArea: mockStorage})
264
+
265
+ // Start first logout
266
+ const firstLogout = store.getState().logout()
267
+ // Attempt second logout before first completes
268
+ const secondLogout = store.getState().logout()
269
+
270
+ await Promise.all([firstLogout, secondLogout])
271
+
272
+ // Should only make one logout request
273
+ expect(mockClient.request).toHaveBeenCalledWith({
274
+ uri: '/auth/logout',
275
+ method: 'POST',
276
+ })
277
+ expect(
278
+ mockClient.request.mock.calls.filter((call) => call[0].uri === '/auth/logout'),
279
+ ).toHaveLength(1)
280
+ })
281
+
282
+ it('handles storage events', () => {
283
+ const store = createInternalAuthStore(instance, {
284
+ storageArea: mockStorage,
285
+ })
286
+
287
+ // Mock the storage to return the new token when checked
288
+ mockStorage.getItem.mockReturnValue(JSON.stringify({token: 'new-token'}))
289
+
290
+ // Simulate storage event
291
+ const storageEvent = new StorageEvent('storage', {
292
+ key: '__sanity_auth_token_test-project_test-dataset',
293
+ newValue: JSON.stringify({token: 'new-token'}),
294
+ storageArea: mockStorage,
295
+ })
296
+
297
+ // Get the storage event handler
298
+ const [[eventName, handler]] = mockWindow.addEventListener.mock.calls
299
+ expect(eventName).toBe('storage')
300
+
301
+ // Manually call the handler
302
+ handler(storageEvent)
303
+
304
+ expect(store.getState().authState).toEqual({
305
+ type: 'logged-in',
306
+ token: 'new-token',
307
+ currentUser: null,
308
+ })
309
+ })
310
+ })
311
+
312
+ describe('getInternalAuthStore', () => {
313
+ it('returns the same store instance for the same sanity instance', () => {
314
+ const store1 = getInternalAuthStore(instance)
315
+ const store2 = getInternalAuthStore(instance)
316
+
317
+ expect(store1).toBe(store2)
318
+ })
319
+
320
+ it('creates different stores for different instances', () => {
321
+ const instance2 = {
322
+ ...instance,
323
+ identity: {
324
+ ...instance.identity,
325
+ id: 'different-id',
326
+ },
327
+ }
328
+
329
+ const store1 = getInternalAuthStore(instance)
330
+ const store2 = getInternalAuthStore(instance2)
331
+
332
+ expect(store1).not.toBe(store2)
333
+ })
334
+ })