@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,95 @@
1
+ import {createClient, type SanityClient} from '@sanity/client'
2
+ import {describe, expect, it, vi} from 'vitest'
3
+
4
+ import {config} from '../../../../test/fixtures'
5
+ import {createSanityInstance} from '../../../instance/sanityInstance'
6
+ import {createClientStore} from '../clientStore'
7
+
8
+ describe('getClientEvents', () => {
9
+ const API_VERSION = '2024-12-05'
10
+ let defaultClient: SanityClient
11
+ let store: ReturnType<typeof createClientStore>
12
+ let instance: ReturnType<typeof createSanityInstance>
13
+
14
+ beforeEach(() => {
15
+ instance = createSanityInstance(config)
16
+ defaultClient = createClient({...config, apiVersion: API_VERSION, useCdn: false})
17
+ store = createClientStore(instance, defaultClient)
18
+ })
19
+
20
+ it('immediately emits initial client', () => {
21
+ const events = store.getClientEvents({apiVersion: '2024-01-01'})
22
+ const mockNext = vi.fn()
23
+
24
+ events.subscribe({next: mockNext})
25
+
26
+ expect(mockNext).toHaveBeenCalledTimes(1)
27
+ const emittedClient = mockNext.mock.calls[0][0]
28
+ expect(emittedClient.config().apiVersion).toBe('2024-01-01')
29
+ })
30
+
31
+ it('emits new client when store updates token', () => {
32
+ const events = store.getClientEvents({apiVersion: '2024-01-01'})
33
+ const mockNext = vi.fn()
34
+
35
+ events.subscribe({next: mockNext})
36
+ store.receiveToken('new-token')
37
+
38
+ expect(mockNext).toHaveBeenCalledTimes(2) // Initial + update
39
+ const latestClient = mockNext.mock.calls[1][0]
40
+ expect(latestClient.config().token).toBe('new-token')
41
+ expect(latestClient.config().apiVersion).toBe('2024-01-01')
42
+ })
43
+
44
+ it('unsubscribes properly', () => {
45
+ const events = store.getClientEvents({apiVersion: '2024-01-01'})
46
+ const mockNext = vi.fn()
47
+
48
+ const subscription = events.subscribe({next: mockNext})
49
+ subscription.unsubscribe()
50
+
51
+ store.receiveToken('new-token')
52
+
53
+ // Should only have the initial emission
54
+ expect(mockNext).toHaveBeenCalledTimes(1)
55
+ })
56
+
57
+ it('throws error when apiVersion is missing', () => {
58
+ expect(() => store.getClientEvents({})).toThrow('Missing required `apiVersion` option')
59
+ })
60
+
61
+ it('maintains apiVersion through updates', () => {
62
+ const events = store.getClientEvents({apiVersion: '2024-01-01'})
63
+ const mockNext = vi.fn()
64
+
65
+ events.subscribe({next: mockNext})
66
+ store.receiveToken('token1')
67
+ store.receiveToken('token2')
68
+
69
+ expect(mockNext).toHaveBeenCalledTimes(3) // Initial + 2 updates
70
+ mockNext.mock.calls.forEach(([client]) => {
71
+ expect(client.config().apiVersion).toBe('2024-01-01')
72
+ })
73
+ })
74
+
75
+ it('handles multiple subscribers independently', () => {
76
+ const events1 = store.getClientEvents({apiVersion: '2024-01-01'})
77
+ const events2 = store.getClientEvents({apiVersion: '2024-02-01'})
78
+ const mockNext1 = vi.fn()
79
+ const mockNext2 = vi.fn()
80
+
81
+ events1.subscribe({next: mockNext1})
82
+ events2.subscribe({next: mockNext2})
83
+ store.receiveToken('new-token')
84
+
85
+ expect(mockNext1).toHaveBeenCalledTimes(2) // Initial + update
86
+ expect(mockNext2).toHaveBeenCalledTimes(2) // Initial + update
87
+
88
+ const latestClient1 = mockNext1.mock.calls[1][0]
89
+ const latestClient2 = mockNext2.mock.calls[1][0]
90
+ expect(latestClient1.config().apiVersion).toBe('2024-01-01')
91
+ expect(latestClient2.config().apiVersion).toBe('2024-02-01')
92
+ expect(latestClient1.config().token).toBe('new-token')
93
+ expect(latestClient2.config().token).toBe('new-token')
94
+ })
95
+ })
@@ -0,0 +1,33 @@
1
+ import type {SanityClient} from '@sanity/client'
2
+ import {distinctUntilChanged, map, Observable, startWith, type Subscribable} from 'rxjs'
3
+
4
+ import type {StoreActionContext} from '../../../store/createStore'
5
+ import type {ClientOptions, ClientState} from '../clientStore'
6
+ import {getOrCreateClient} from './getOrCreateClient'
7
+
8
+ /**
9
+ * Provides a stream of clients, based on the current state of the store.
10
+ * (For example, when a user logs in, this will emit an authorized client.)
11
+ * @internal
12
+ */
13
+ export const getClientEvents = (
14
+ context: StoreActionContext<ClientState>,
15
+ options: ClientOptions = {},
16
+ ): Subscribable<SanityClient> => {
17
+ const {store} = context
18
+
19
+ const initialClient = getOrCreateClient(context, options)
20
+ const clientStore$ = new Observable<void>((subscriber) =>
21
+ store.subscribe(() => subscriber.next()),
22
+ )
23
+
24
+ const client$ = clientStore$.pipe(
25
+ map(() => getOrCreateClient(context, options)),
26
+ startWith(initialClient),
27
+ distinctUntilChanged((prev, curr) => prev.config().token === curr.config().token),
28
+ )
29
+
30
+ return {
31
+ subscribe: client$.subscribe.bind(client$),
32
+ }
33
+ }
@@ -0,0 +1,56 @@
1
+ import {createClient, type SanityClient} from '@sanity/client'
2
+
3
+ import {config} from '../../../../test/fixtures'
4
+ import {createSanityInstance} from '../../../instance/sanityInstance'
5
+ import {createClientStore} from '../clientStore'
6
+
7
+ describe('getOrCreateClient', () => {
8
+ const API_VERSION = '2024-12-05'
9
+ let defaultClient: SanityClient
10
+ let store: ReturnType<typeof createClientStore>
11
+ let instance: ReturnType<typeof createSanityInstance>
12
+
13
+ beforeEach(() => {
14
+ instance = createSanityInstance(config)
15
+ defaultClient = createClient({...config, apiVersion: API_VERSION, useCdn: false})
16
+ store = createClientStore(instance, defaultClient)
17
+ })
18
+
19
+ it('throws error when apiVersion is missing', () => {
20
+ expect(() => store.getOrCreateClient({})).toThrow('Missing required `apiVersion` option')
21
+ })
22
+
23
+ it('creates new client with correct apiVersion', () => {
24
+ const apiVersion = '2024-01-01'
25
+ const result = store.getOrCreateClient({apiVersion})
26
+ expect(result.config().apiVersion).toBe(apiVersion)
27
+ })
28
+
29
+ it('reuses existing client for same apiVersion', () => {
30
+ const apiVersion = '2024-01-01'
31
+ const result1 = store.getOrCreateClient({apiVersion})
32
+ const result2 = store.getOrCreateClient({apiVersion})
33
+
34
+ expect(result1).toBe(result2)
35
+ })
36
+
37
+ it('preserves client identity after token update', () => {
38
+ const apiVersion = '2024-01-01'
39
+ const client1 = store.getOrCreateClient({apiVersion})
40
+
41
+ // Update token
42
+ store.receiveToken('new-token')
43
+
44
+ const client2 = store.getOrCreateClient({apiVersion})
45
+
46
+ // Verify the new token was applied
47
+ expect(client2.config().token).toBe('new-token')
48
+ expect(client2.config().apiVersion).toBe(apiVersion)
49
+
50
+ // Verify we got a new client instance (since token changed)
51
+ expect(client2).not.toBe(client1)
52
+
53
+ // Verify first client keeps its original config
54
+ expect(client1.config().token).toBeUndefined()
55
+ })
56
+ })
@@ -0,0 +1,40 @@
1
+ import {type SanityClient} from '@sanity/client'
2
+
3
+ import type {StoreActionContext} from '../../../store/createStore'
4
+ import type {ClientOptions, ClientState} from '../clientStore'
5
+
6
+ /**
7
+ * Retrieves a memoized client based on the API version,
8
+ * or creates a new one if it doesn't exist.
9
+ * @internal
10
+ */
11
+ export const getOrCreateClient = (
12
+ {store}: StoreActionContext<ClientState>,
13
+ options: ClientOptions = {},
14
+ ): SanityClient => {
15
+ const state = store.getState()
16
+ const {apiVersion} = options
17
+
18
+ if (!apiVersion) {
19
+ throw new Error('Missing required `apiVersion` option')
20
+ }
21
+
22
+ const cached = state.clients.get(apiVersion)
23
+ if (cached) {
24
+ return cached
25
+ }
26
+
27
+ // Create new client with specified API version
28
+ const client = state.defaultClient.withConfig(options)
29
+
30
+ // Update state with new client
31
+ store.setState((prevState) => {
32
+ const newMap = new Map(prevState.clients)
33
+ newMap.set(apiVersion, client)
34
+ return {
35
+ clients: newMap,
36
+ }
37
+ })
38
+
39
+ return client
40
+ }
@@ -0,0 +1,18 @@
1
+ import {config} from '../../../../test/fixtures'
2
+ import {createSanityInstance} from '../../../instance/sanityInstance'
3
+ import {getClientStore} from '../clientStore'
4
+
5
+ describe('receiveToken', () => {
6
+ const sanityInstance = createSanityInstance(config)
7
+ it('updates client tokens when auth state changes', () => {
8
+ const store = getClientStore(sanityInstance)
9
+
10
+ const client = store.getOrCreateClient({apiVersion: 'v2023-01-01'})
11
+ expect(client.config().token).toBeUndefined()
12
+
13
+ store.receiveToken('new-token')
14
+
15
+ const updatedClient = store.getOrCreateClient({apiVersion: 'v2023-01-01'})
16
+ expect(updatedClient.config().token).toBe('new-token')
17
+ })
18
+ })
@@ -0,0 +1,31 @@
1
+ import type {StoreActionContext} from '../../../store/createStore'
2
+ import type {ClientState} from '../clientStore'
3
+
4
+ /**
5
+ * Updates the client store state when a token is received.
6
+ * @internal
7
+ */
8
+ export const receiveToken = (
9
+ {store}: StoreActionContext<ClientState>,
10
+ token: string | undefined,
11
+ ): void => {
12
+ // Update the default client
13
+ const newDefaultClient = store.getState().defaultClient.withConfig({
14
+ token,
15
+ })
16
+
17
+ // Update existing clients while preserving the map structure
18
+ store.setState((prevState) => {
19
+ const updatedClients = new Map(
20
+ Array.from(prevState.clients.entries()).map(([version, client]) => [
21
+ version,
22
+ client.withConfig({token}),
23
+ ]),
24
+ )
25
+
26
+ return {
27
+ defaultClient: newDefaultClient,
28
+ clients: updatedClients,
29
+ }
30
+ })
31
+ }
@@ -0,0 +1,152 @@
1
+ import {describe, expect, it, vi} from 'vitest'
2
+
3
+ import {config} from '../../../test/fixtures'
4
+ import {getInternalAuthStore} from '../../auth/getInternalAuthStore'
5
+ import {createSanityInstance} from '../../instance/sanityInstance'
6
+ import {getClientStore} from './clientStore'
7
+
8
+ // Mock at module level but don't provide implementation yet
9
+ vi.mock('../../auth/getAuthStore')
10
+
11
+ describe.skip('clientStore', () => {
12
+ beforeEach(() => {
13
+ vi.resetModules()
14
+ vi.clearAllMocks()
15
+ // Reset to default mock implementation
16
+ vi.mocked(getInternalAuthStore).mockImplementation(() => ({
17
+ setState: vi.fn(),
18
+ getState: () => ({
19
+ authState: {type: 'logged-out', isDestroyingSession: false},
20
+ providers: undefined,
21
+ setAuthState: vi.fn(),
22
+ setProviders: vi.fn(),
23
+ handleCallback: vi.fn(),
24
+ getLoginUrls: vi.fn(),
25
+ logout: vi.fn(),
26
+ dispose: vi.fn(),
27
+ }),
28
+ getInitialState: () => ({
29
+ authState: {type: 'logged-out', isDestroyingSession: false},
30
+ providers: undefined,
31
+ setAuthState: vi.fn(),
32
+ setProviders: vi.fn(),
33
+ handleCallback: vi.fn(),
34
+ getLoginUrls: vi.fn(),
35
+ logout: vi.fn(),
36
+ dispose: vi.fn(),
37
+ }),
38
+ subscribe: () => () => {}, // Default no-op implementation
39
+ }))
40
+ })
41
+
42
+ it('creates a store with the expected interface', () => {
43
+ const sanityInstance = createSanityInstance(config)
44
+ const store = getClientStore(sanityInstance)
45
+ expect(store).toHaveProperty('getClientEvents')
46
+ expect(store).toHaveProperty('getOrCreateClient')
47
+ expect(store).toHaveProperty('receiveToken')
48
+ })
49
+
50
+ it('provides clients with correct configuration', () => {
51
+ const sanityInstance = createSanityInstance(config)
52
+ const store = getClientStore(sanityInstance)
53
+ const client = store.getOrCreateClient({apiVersion: 'v2024-11-12'})
54
+ const clientConfig = client.config()
55
+ expect(clientConfig).toMatchObject({
56
+ ...config,
57
+ useCdn: false,
58
+ })
59
+ })
60
+
61
+ it('creates clients with config auth token', () => {
62
+ const instanceWithToken = createSanityInstance({
63
+ ...config,
64
+ auth: {
65
+ token: 'initial-auth-token',
66
+ },
67
+ })
68
+
69
+ const store = getClientStore(instanceWithToken)
70
+ const client = store.getOrCreateClient({apiVersion: 'v2024-11-12'})
71
+
72
+ expect(client.config().token).toBe('initial-auth-token')
73
+ })
74
+
75
+ it('handles logged-in auth state changes', async () => {
76
+ const sanityInstance = createSanityInstance(config)
77
+
78
+ // Override mock implementation just for this test
79
+ vi.mocked(getInternalAuthStore).mockImplementation(() => ({
80
+ setState: vi.fn(),
81
+ getState: () => ({
82
+ authState: {type: 'logged-in', token: 'test-token', currentUser: null},
83
+ providers: undefined,
84
+ setAuthState: vi.fn(),
85
+ setProviders: vi.fn(),
86
+ handleCallback: vi.fn(),
87
+ getLoginUrls: vi.fn(),
88
+ logout: vi.fn(),
89
+ dispose: vi.fn(),
90
+ }),
91
+ getInitialState: () => ({
92
+ authState: {type: 'logged-in', token: 'test-token', currentUser: null},
93
+ providers: undefined,
94
+ setAuthState: vi.fn(),
95
+ setProviders: vi.fn(),
96
+ handleCallback: vi.fn(),
97
+ getLoginUrls: vi.fn(),
98
+ logout: vi.fn(),
99
+ dispose: vi.fn(),
100
+ }),
101
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
102
+ subscribe: (observer: any) => {
103
+ observer.next({type: 'logged-in', token: 'test-token', currentUser: null})
104
+ return () => {}
105
+ },
106
+ }))
107
+
108
+ const store = getClientStore(sanityInstance)
109
+
110
+ await new Promise((resolve) => setTimeout(resolve, 0))
111
+
112
+ const client = store.getOrCreateClient({apiVersion: 'v2023-01-01'})
113
+ expect(client.config().token).toBe('test-token')
114
+ })
115
+
116
+ it('properly cleans up auth subscription when cleanup is called', () => {
117
+ const unsubscribeSpy = vi.fn()
118
+
119
+ // Mock the auth store with a spy on the unsubscribe function
120
+ vi.mocked(getInternalAuthStore).mockImplementation(() => ({
121
+ setState: vi.fn(),
122
+ getState: () => ({
123
+ authState: {type: 'logged-in', token: 'test-token', currentUser: null},
124
+ providers: undefined,
125
+ setAuthState: vi.fn(),
126
+ setProviders: vi.fn(),
127
+ handleCallback: vi.fn(),
128
+ getLoginUrls: vi.fn(),
129
+ logout: vi.fn(),
130
+ dispose: vi.fn(),
131
+ }),
132
+ getInitialState: () => ({
133
+ authState: {type: 'logged-in', token: 'test-token', currentUser: null},
134
+ providers: undefined,
135
+ setAuthState: vi.fn(),
136
+ setProviders: vi.fn(),
137
+ handleCallback: vi.fn(),
138
+ getLoginUrls: vi.fn(),
139
+ logout: vi.fn(),
140
+ dispose: vi.fn(),
141
+ }),
142
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
143
+ subscribe: (observer: any) => {
144
+ observer.next({type: 'logged-in', token: 'test-token', currentUser: null})
145
+ return unsubscribeSpy
146
+ },
147
+ }))
148
+
149
+ // Verify that the unsubscribe function was called
150
+ expect(unsubscribeSpy).toHaveBeenCalledTimes(1)
151
+ })
152
+ })
@@ -0,0 +1,98 @@
1
+ import {createClient, type SanityClient} from '@sanity/client'
2
+ import type {Subscribable} from 'rxjs'
3
+
4
+ import {getInternalAuthStore} from '../../auth/getInternalAuthStore'
5
+ import {getOrCreateResource} from '../../instance/sanityInstance'
6
+ import type {SanityInstance} from '../../instance/types'
7
+ import {createStore} from '../../store/createStore'
8
+ import {getClientEvents} from './actions/getClientEvents'
9
+ import {getOrCreateClient} from './actions/getOrCreateClient'
10
+ import {receiveToken} from './actions/receiveToken'
11
+
12
+ export const DEFAULT_API_VERSION = 'v2024-11-12'
13
+
14
+ /**
15
+ * Options used when retrieving a client via getOrCreateClient.
16
+ * @public
17
+ */
18
+ export interface ClientOptions {
19
+ apiVersion?: string
20
+ }
21
+
22
+ /**
23
+ * Internal state of the client store.
24
+ * @internal
25
+ */
26
+ export interface ClientState {
27
+ // default client shouldn't be exposed, but is used in creation of new clients
28
+ defaultClient: SanityClient
29
+ clients: Map<string, SanityClient>
30
+ }
31
+
32
+ /**
33
+ * Collection of actions to retrieve or create clients.
34
+ * @internal
35
+ */
36
+ export interface ClientStore {
37
+ getOrCreateClient: (options: ClientOptions) => SanityClient
38
+ receiveToken: (token: string | undefined) => void
39
+ getClientEvents: (options: ClientOptions) => Subscribable<SanityClient>
40
+ }
41
+
42
+ const createInitialState = (defaultClient: SanityClient): ClientState => {
43
+ const clients = new Map<string, SanityClient>()
44
+ clients.set(defaultClient.config().apiVersion, defaultClient)
45
+ return {defaultClient, clients}
46
+ }
47
+
48
+ const clientStoreActions = {
49
+ getOrCreateClient,
50
+ receiveToken,
51
+ getClientEvents,
52
+ }
53
+
54
+ /**
55
+ * Construction method for creating a client store, including subscribing to auth store.
56
+ * @internal
57
+ */
58
+ export const createClientStore = (
59
+ instance: SanityInstance,
60
+ defaultClient: SanityClient,
61
+ ): ClientStore => {
62
+ const internalAuthStore = getInternalAuthStore(instance)
63
+
64
+ const store = createStore(createInitialState(defaultClient), clientStoreActions, {
65
+ name: 'clientStore',
66
+ instance,
67
+ })
68
+
69
+ internalAuthStore.subscribe((state, prevState) => {
70
+ if (state.authState.type === 'logged-in' && prevState.authState.type !== 'logged-in') {
71
+ store.receiveToken(state.authState.token)
72
+ }
73
+ if (prevState.authState.type === 'logged-in' && state.authState.type !== 'logged-in') {
74
+ store.receiveToken(undefined)
75
+ }
76
+ })
77
+
78
+ return store
79
+ }
80
+
81
+ /**
82
+ * This is an internal function that retrieves or creates a client store.
83
+ * @internal
84
+ */
85
+ export const getClientStore = (instance: SanityInstance): ClientStore => {
86
+ return getOrCreateResource(instance, 'clientStore', () => {
87
+ const {config, identity} = instance
88
+
89
+ const client = createClient({
90
+ projectId: identity.projectId,
91
+ dataset: identity.dataset,
92
+ token: config?.auth?.token,
93
+ useCdn: false,
94
+ apiVersion: DEFAULT_API_VERSION,
95
+ })
96
+ return createClientStore(instance, client)
97
+ })
98
+ }