@sanity/sdk 2.3.1 → 2.4.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.
- package/dist/index.d.ts +140 -17
- package/dist/index.js +131 -25
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/_exports/index.ts +30 -0
- package/src/agent/agentActions.test.ts +81 -0
- package/src/agent/agentActions.ts +139 -0
- package/src/auth/utils.ts +36 -0
- package/src/client/clientStore.test.ts +151 -0
- package/src/client/clientStore.ts +39 -1
- package/src/config/sanityConfig.ts +41 -0
- package/src/document/actions.test.ts +34 -0
- package/src/document/actions.ts +20 -0
- package/src/document/documentStore.test.ts +28 -0
- package/src/document/processActions.test.ts +97 -0
- package/src/document/processActions.ts +12 -2
- package/src/query/queryStore.ts +7 -4
- package/src/store/createActionBinder.ts +27 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK",
|
|
6
6
|
"keywords": [
|
|
@@ -61,20 +61,20 @@
|
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@sanity/browserslist-config": "^1.0.5",
|
|
63
63
|
"@sanity/pkg-utils": "^7.9.6",
|
|
64
|
-
"@sanity/prettier-config": "^1.0.
|
|
64
|
+
"@sanity/prettier-config": "^1.0.6",
|
|
65
65
|
"@types/lodash-es": "^4.17.12",
|
|
66
|
-
"@vitest/coverage-v8": "3.
|
|
66
|
+
"@vitest/coverage-v8": "3.2.4",
|
|
67
67
|
"eslint": "^9.22.0",
|
|
68
|
-
"prettier": "^3.
|
|
68
|
+
"prettier": "^3.7.3",
|
|
69
69
|
"rollup-plugin-visualizer": "^5.14.0",
|
|
70
70
|
"typescript": "^5.8.3",
|
|
71
71
|
"vite": "^6.3.4",
|
|
72
|
-
"vitest": "^3.
|
|
73
|
-
"@repo/config-
|
|
72
|
+
"vitest": "^3.2.4",
|
|
73
|
+
"@repo/config-test": "0.0.1",
|
|
74
74
|
"@repo/package.bundle": "3.82.0",
|
|
75
|
-
"@repo/
|
|
75
|
+
"@repo/config-eslint": "0.0.0",
|
|
76
76
|
"@repo/tsconfig": "0.0.1",
|
|
77
|
-
"@repo/config
|
|
77
|
+
"@repo/package.config": "0.0.1"
|
|
78
78
|
},
|
|
79
79
|
"engines": {
|
|
80
80
|
"node": ">=20.0.0"
|
package/src/_exports/index.ts
CHANGED
|
@@ -5,6 +5,25 @@ import {type SanityProject as _SanityProject} from '@sanity/client'
|
|
|
5
5
|
*/
|
|
6
6
|
export type SanityProject = _SanityProject
|
|
7
7
|
|
|
8
|
+
export type {
|
|
9
|
+
AgentGenerateOptions,
|
|
10
|
+
AgentGenerateResult,
|
|
11
|
+
AgentPatchOptions,
|
|
12
|
+
AgentPatchResult,
|
|
13
|
+
AgentPromptOptions,
|
|
14
|
+
AgentPromptResult,
|
|
15
|
+
AgentTransformOptions,
|
|
16
|
+
AgentTransformResult,
|
|
17
|
+
AgentTranslateOptions,
|
|
18
|
+
AgentTranslateResult,
|
|
19
|
+
} from '../agent/agentActions'
|
|
20
|
+
export {
|
|
21
|
+
agentGenerate,
|
|
22
|
+
agentPatch,
|
|
23
|
+
agentPrompt,
|
|
24
|
+
agentTransform,
|
|
25
|
+
agentTranslate,
|
|
26
|
+
} from '../agent/agentActions'
|
|
8
27
|
export {AuthStateType} from '../auth/authStateType'
|
|
9
28
|
export {
|
|
10
29
|
type AuthState,
|
|
@@ -24,6 +43,13 @@ export {
|
|
|
24
43
|
export {observeOrganizationVerificationState} from '../auth/getOrganizationVerificationState'
|
|
25
44
|
export {handleAuthCallback} from '../auth/handleAuthCallback'
|
|
26
45
|
export {logout} from '../auth/logout'
|
|
46
|
+
export {
|
|
47
|
+
type ApiErrorBody,
|
|
48
|
+
getClientErrorApiBody,
|
|
49
|
+
getClientErrorApiDescription,
|
|
50
|
+
getClientErrorApiType,
|
|
51
|
+
isProjectUserNotFoundClientError,
|
|
52
|
+
} from '../auth/utils'
|
|
27
53
|
export type {ClientStoreState as ClientState} from '../client/clientStore'
|
|
28
54
|
export {type ClientOptions, getClient, getClientState} from '../client/clientStore'
|
|
29
55
|
export {
|
|
@@ -50,9 +76,13 @@ export {
|
|
|
50
76
|
createProjectHandle,
|
|
51
77
|
} from '../config/handles'
|
|
52
78
|
export {
|
|
79
|
+
canvasSource,
|
|
53
80
|
type DatasetHandle,
|
|
81
|
+
datasetSource,
|
|
54
82
|
type DocumentHandle,
|
|
83
|
+
type DocumentSource,
|
|
55
84
|
type DocumentTypeHandle,
|
|
85
|
+
mediaLibrarySource,
|
|
56
86
|
type PerspectiveHandle,
|
|
57
87
|
type ProjectHandle,
|
|
58
88
|
type ReleasePerspective,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import {firstValueFrom, of} from 'rxjs'
|
|
3
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
agentGenerate,
|
|
7
|
+
agentPatch,
|
|
8
|
+
agentPrompt,
|
|
9
|
+
agentTransform,
|
|
10
|
+
agentTranslate,
|
|
11
|
+
} from './agentActions'
|
|
12
|
+
|
|
13
|
+
let mockClient: any
|
|
14
|
+
|
|
15
|
+
vi.mock('../client/clientStore', () => {
|
|
16
|
+
return {
|
|
17
|
+
getClientState: () => ({observable: of(mockClient)}),
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe('agent actions', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
mockClient = {
|
|
24
|
+
observable: {
|
|
25
|
+
agent: {
|
|
26
|
+
action: {
|
|
27
|
+
generate: vi.fn(),
|
|
28
|
+
transform: vi.fn(),
|
|
29
|
+
translate: vi.fn(),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
agent: {
|
|
34
|
+
action: {
|
|
35
|
+
prompt: vi.fn(),
|
|
36
|
+
patch: vi.fn(),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('agentGenerate returns observable from client', async () => {
|
|
43
|
+
mockClient.observable.agent.action.generate.mockReturnValue(of('gen'))
|
|
44
|
+
const instance = {config: {projectId: 'p', dataset: 'd'}} as any
|
|
45
|
+
const value = await firstValueFrom(agentGenerate(instance, {foo: 'bar'} as any))
|
|
46
|
+
expect(value).toBe('gen')
|
|
47
|
+
expect(mockClient.observable.agent.action.generate).toHaveBeenCalledWith({foo: 'bar'})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('agentTransform returns observable from client', async () => {
|
|
51
|
+
mockClient.observable.agent.action.transform.mockReturnValue(of('xform'))
|
|
52
|
+
const instance = {config: {projectId: 'p', dataset: 'd'}} as any
|
|
53
|
+
const value = await firstValueFrom(agentTransform(instance, {a: 1} as any))
|
|
54
|
+
expect(value).toBe('xform')
|
|
55
|
+
expect(mockClient.observable.agent.action.transform).toHaveBeenCalledWith({a: 1})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('agentTranslate returns observable from client', async () => {
|
|
59
|
+
mockClient.observable.agent.action.translate.mockReturnValue(of('xlate'))
|
|
60
|
+
const instance = {config: {projectId: 'p', dataset: 'd'}} as any
|
|
61
|
+
const value = await firstValueFrom(agentTranslate(instance, {b: 2} as any))
|
|
62
|
+
expect(value).toBe('xlate')
|
|
63
|
+
expect(mockClient.observable.agent.action.translate).toHaveBeenCalledWith({b: 2})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('agentPrompt wraps promise into observable', async () => {
|
|
67
|
+
mockClient.agent.action.prompt.mockResolvedValue('prompted')
|
|
68
|
+
const instance = {config: {projectId: 'p', dataset: 'd'}} as any
|
|
69
|
+
const value = await firstValueFrom(agentPrompt(instance, {p: true} as any))
|
|
70
|
+
expect(value).toBe('prompted')
|
|
71
|
+
expect(mockClient.agent.action.prompt).toHaveBeenCalledWith({p: true})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('agentPatch wraps promise into observable', async () => {
|
|
75
|
+
mockClient.agent.action.patch.mockResolvedValue('patched')
|
|
76
|
+
const instance = {config: {projectId: 'p', dataset: 'd'}} as any
|
|
77
|
+
const value = await firstValueFrom(agentPatch(instance, {q: false} as any))
|
|
78
|
+
expect(value).toBe('patched')
|
|
79
|
+
expect(mockClient.agent.action.patch).toHaveBeenCalledWith({q: false})
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {type SanityClient} from '@sanity/client'
|
|
2
|
+
import {from, Observable, switchMap} from 'rxjs'
|
|
3
|
+
|
|
4
|
+
import {getClientState} from '../client/clientStore'
|
|
5
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
6
|
+
|
|
7
|
+
const API_VERSION = 'vX'
|
|
8
|
+
|
|
9
|
+
/** @alpha */
|
|
10
|
+
export type AgentGenerateOptions = Parameters<
|
|
11
|
+
SanityClient['observable']['agent']['action']['generate']
|
|
12
|
+
>[0]
|
|
13
|
+
|
|
14
|
+
/** @alpha */
|
|
15
|
+
export type AgentTransformOptions = Parameters<
|
|
16
|
+
SanityClient['observable']['agent']['action']['transform']
|
|
17
|
+
>[0]
|
|
18
|
+
|
|
19
|
+
/** @alpha */
|
|
20
|
+
export type AgentTranslateOptions = Parameters<
|
|
21
|
+
SanityClient['observable']['agent']['action']['translate']
|
|
22
|
+
>[0]
|
|
23
|
+
|
|
24
|
+
/** @alpha */
|
|
25
|
+
export type AgentPromptOptions = Parameters<SanityClient['agent']['action']['prompt']>[0]
|
|
26
|
+
|
|
27
|
+
/** @alpha */
|
|
28
|
+
export type AgentPatchOptions = Parameters<SanityClient['agent']['action']['patch']>[0]
|
|
29
|
+
|
|
30
|
+
/** @alpha */
|
|
31
|
+
export type AgentGenerateResult = Awaited<
|
|
32
|
+
ReturnType<SanityClient['observable']['agent']['action']['generate']>
|
|
33
|
+
>
|
|
34
|
+
|
|
35
|
+
/** @alpha */
|
|
36
|
+
export type AgentTransformResult = Awaited<
|
|
37
|
+
ReturnType<SanityClient['observable']['agent']['action']['transform']>
|
|
38
|
+
>
|
|
39
|
+
|
|
40
|
+
/** @alpha */
|
|
41
|
+
export type AgentTranslateResult = Awaited<
|
|
42
|
+
ReturnType<SanityClient['observable']['agent']['action']['translate']>
|
|
43
|
+
>
|
|
44
|
+
|
|
45
|
+
/** @alpha */
|
|
46
|
+
export type AgentPromptResult = Awaited<ReturnType<SanityClient['agent']['action']['prompt']>>
|
|
47
|
+
|
|
48
|
+
/** @alpha */
|
|
49
|
+
export type AgentPatchResult = Awaited<ReturnType<SanityClient['agent']['action']['patch']>>
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generates a new document using the agent.
|
|
53
|
+
* @param instance - The Sanity instance.
|
|
54
|
+
* @param options - The options for the agent generate action. See the [Agent Actions API](https://www.sanity.io/docs/agent-actions/introduction) for more details.
|
|
55
|
+
* @returns An Observable emitting the result of the agent generate action.
|
|
56
|
+
* @alpha
|
|
57
|
+
*/
|
|
58
|
+
export function agentGenerate(
|
|
59
|
+
instance: SanityInstance,
|
|
60
|
+
options: AgentGenerateOptions,
|
|
61
|
+
): AgentGenerateResult {
|
|
62
|
+
return getClientState(instance, {
|
|
63
|
+
apiVersion: API_VERSION,
|
|
64
|
+
projectId: instance.config.projectId,
|
|
65
|
+
dataset: instance.config.dataset,
|
|
66
|
+
}).observable.pipe(switchMap((client) => client.observable.agent.action.generate(options)))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Transforms a document using the agent.
|
|
71
|
+
* @param instance - The Sanity instance.
|
|
72
|
+
* @param options - The options for the agent transform action. See the [Agent Actions API](https://www.sanity.io/docs/agent-actions/introduction) for more details.
|
|
73
|
+
* @returns An Observable emitting the result of the agent transform action.
|
|
74
|
+
* @alpha
|
|
75
|
+
*/
|
|
76
|
+
export function agentTransform(
|
|
77
|
+
instance: SanityInstance,
|
|
78
|
+
options: AgentTransformOptions,
|
|
79
|
+
): AgentTransformResult {
|
|
80
|
+
return getClientState(instance, {
|
|
81
|
+
apiVersion: API_VERSION,
|
|
82
|
+
projectId: instance.config.projectId,
|
|
83
|
+
dataset: instance.config.dataset,
|
|
84
|
+
}).observable.pipe(switchMap((client) => client.observable.agent.action.transform(options)))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Translates a document using the agent.
|
|
89
|
+
* @param instance - The Sanity instance.
|
|
90
|
+
* @param options - The options for the agent translate action. See the [Agent Actions API](https://www.sanity.io/docs/agent-actions/introduction) for more details.
|
|
91
|
+
* @returns An Observable emitting the result of the agent translate action.
|
|
92
|
+
* @alpha
|
|
93
|
+
*/
|
|
94
|
+
export function agentTranslate(
|
|
95
|
+
instance: SanityInstance,
|
|
96
|
+
options: AgentTranslateOptions,
|
|
97
|
+
): AgentTranslateResult {
|
|
98
|
+
return getClientState(instance, {
|
|
99
|
+
apiVersion: API_VERSION,
|
|
100
|
+
projectId: instance.config.projectId,
|
|
101
|
+
dataset: instance.config.dataset,
|
|
102
|
+
}).observable.pipe(switchMap((client) => client.observable.agent.action.translate(options)))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Prompts the agent using the same instruction template format as the other actions, but returns text or json instead of acting on a document.
|
|
107
|
+
* @param instance - The Sanity instance.
|
|
108
|
+
* @param options - The options for the agent prompt action. See the [Agent Actions API](https://www.sanity.io/docs/agent-actions/introduction) for more details.
|
|
109
|
+
* @returns An Observable emitting the result of the agent prompt action.
|
|
110
|
+
* @alpha
|
|
111
|
+
*/
|
|
112
|
+
export function agentPrompt(
|
|
113
|
+
instance: SanityInstance,
|
|
114
|
+
options: AgentPromptOptions,
|
|
115
|
+
): Observable<AgentPromptResult> {
|
|
116
|
+
return getClientState(instance, {
|
|
117
|
+
apiVersion: API_VERSION,
|
|
118
|
+
projectId: instance.config.projectId,
|
|
119
|
+
dataset: instance.config.dataset,
|
|
120
|
+
}).observable.pipe(switchMap((client) => from(client.agent.action.prompt(options))))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Patches a document using the agent.
|
|
125
|
+
* @param instance - The Sanity instance.
|
|
126
|
+
* @param options - The options for the agent patch action. See the [Agent Actions API](https://www.sanity.io/docs/agent-actions/introduction) for more details.
|
|
127
|
+
* @returns An Observable emitting the result of the agent patch action.
|
|
128
|
+
* @alpha
|
|
129
|
+
*/
|
|
130
|
+
export function agentPatch(
|
|
131
|
+
instance: SanityInstance,
|
|
132
|
+
options: AgentPatchOptions,
|
|
133
|
+
): Observable<AgentPatchResult> {
|
|
134
|
+
return getClientState(instance, {
|
|
135
|
+
apiVersion: API_VERSION,
|
|
136
|
+
projectId: instance.config.projectId,
|
|
137
|
+
dataset: instance.config.dataset,
|
|
138
|
+
}).observable.pipe(switchMap((client) => from(client.agent.action.patch(options))))
|
|
139
|
+
}
|
package/src/auth/utils.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {type ClientError} from '@sanity/client'
|
|
1
2
|
import {EMPTY, fromEvent, Observable} from 'rxjs'
|
|
2
3
|
|
|
3
4
|
import {AUTH_CODE_PARAM, DEFAULT_BASE} from './authConstants'
|
|
@@ -134,3 +135,38 @@ export function getCleanedUrl(locationUrl: string): string {
|
|
|
134
135
|
loc.searchParams.delete('url')
|
|
135
136
|
return loc.toString()
|
|
136
137
|
}
|
|
138
|
+
|
|
139
|
+
// -----------------------------------------------------------------------------
|
|
140
|
+
// ClientError helpers (shared)
|
|
141
|
+
// -----------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
/** @internal */
|
|
144
|
+
export type ApiErrorBody = {
|
|
145
|
+
error?: {type?: string; description?: string}
|
|
146
|
+
type?: string
|
|
147
|
+
description?: string
|
|
148
|
+
message?: string
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** @internal Extracts the structured API error body from a ClientError, if present. */
|
|
152
|
+
export function getClientErrorApiBody(error: ClientError): ApiErrorBody | undefined {
|
|
153
|
+
const body: unknown = (error as ClientError).response?.body
|
|
154
|
+
return body && typeof body === 'object' ? (body as ApiErrorBody) : undefined
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** @internal Returns the error type string from an API error body, if available. */
|
|
158
|
+
export function getClientErrorApiType(error: ClientError): string | undefined {
|
|
159
|
+
const body = getClientErrorApiBody(error)
|
|
160
|
+
return body?.error?.type ?? body?.type
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** @internal Returns the error description string from an API error body, if available. */
|
|
164
|
+
export function getClientErrorApiDescription(error: ClientError): string | undefined {
|
|
165
|
+
const body = getClientErrorApiBody(error)
|
|
166
|
+
return body?.error?.description ?? body?.description
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** @internal True if the error represents a projectUserNotFoundError. */
|
|
170
|
+
export function isProjectUserNotFoundClientError(error: ClientError): boolean {
|
|
171
|
+
return getClientErrorApiType(error) === 'projectUserNotFoundError'
|
|
172
|
+
}
|
|
@@ -3,6 +3,7 @@ import {Subject} from 'rxjs'
|
|
|
3
3
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
4
|
|
|
5
5
|
import {getAuthMethodState, getTokenState} from '../auth/authStore'
|
|
6
|
+
import {canvasSource, datasetSource, mediaLibrarySource} from '../config/sanityConfig'
|
|
6
7
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
7
8
|
import {getClient, getClientState} from './clientStore'
|
|
8
9
|
|
|
@@ -75,6 +76,20 @@ describe('clientStore', () => {
|
|
|
75
76
|
).toThrowError(/unsupported properties: illegalKey/)
|
|
76
77
|
})
|
|
77
78
|
|
|
79
|
+
it('should throw a helpful error when called without options', () => {
|
|
80
|
+
expect(() =>
|
|
81
|
+
// @ts-expect-error Testing missing options
|
|
82
|
+
getClient(instance, undefined),
|
|
83
|
+
).toThrowError(/requires a configuration object with at least an "apiVersion" property/)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should throw a helpful error when called with null options', () => {
|
|
87
|
+
expect(() =>
|
|
88
|
+
// @ts-expect-error Testing null options
|
|
89
|
+
getClient(instance, null),
|
|
90
|
+
).toThrowError(/requires a configuration object with at least an "apiVersion" property/)
|
|
91
|
+
})
|
|
92
|
+
|
|
78
93
|
it('should reuse clients with identical configurations', () => {
|
|
79
94
|
const options = {apiVersion: '2024-11-12', useCdn: true}
|
|
80
95
|
const client1 = getClient(instance, options)
|
|
@@ -158,4 +173,140 @@ describe('clientStore', () => {
|
|
|
158
173
|
subscription.unsubscribe()
|
|
159
174
|
})
|
|
160
175
|
})
|
|
176
|
+
|
|
177
|
+
describe('source handling', () => {
|
|
178
|
+
it('should create client when source is provided', () => {
|
|
179
|
+
const source = datasetSource('source-project', 'source-dataset')
|
|
180
|
+
const client = getClient(instance, {apiVersion: '2024-11-12', source})
|
|
181
|
+
|
|
182
|
+
expect(vi.mocked(createClient)).toHaveBeenCalledWith(
|
|
183
|
+
expect.objectContaining({
|
|
184
|
+
apiVersion: '2024-11-12',
|
|
185
|
+
source: expect.objectContaining({
|
|
186
|
+
__sanity_internal_sourceId: {
|
|
187
|
+
projectId: 'source-project',
|
|
188
|
+
dataset: 'source-dataset',
|
|
189
|
+
},
|
|
190
|
+
}),
|
|
191
|
+
}),
|
|
192
|
+
)
|
|
193
|
+
// Client should be projectless - no projectId/dataset in config
|
|
194
|
+
expect(client.config()).not.toHaveProperty('projectId')
|
|
195
|
+
expect(client.config()).not.toHaveProperty('dataset')
|
|
196
|
+
expect(client.config()).toEqual(
|
|
197
|
+
expect.objectContaining({
|
|
198
|
+
source: expect.objectContaining({
|
|
199
|
+
__sanity_internal_sourceId: {
|
|
200
|
+
projectId: 'source-project',
|
|
201
|
+
dataset: 'source-dataset',
|
|
202
|
+
},
|
|
203
|
+
}),
|
|
204
|
+
}),
|
|
205
|
+
)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('should create resource when source has array sourceId and be projectless', () => {
|
|
209
|
+
const source = mediaLibrarySource('media-lib-123')
|
|
210
|
+
const client = getClient(instance, {apiVersion: '2024-11-12', source})
|
|
211
|
+
|
|
212
|
+
expect(vi.mocked(createClient)).toHaveBeenCalledWith(
|
|
213
|
+
expect.objectContaining({
|
|
214
|
+
'~experimental_resource': {type: 'media-library', id: 'media-lib-123'},
|
|
215
|
+
'apiVersion': '2024-11-12',
|
|
216
|
+
}),
|
|
217
|
+
)
|
|
218
|
+
// Client should be projectless - no projectId/dataset in config
|
|
219
|
+
expect(client.config()).not.toHaveProperty('projectId')
|
|
220
|
+
expect(client.config()).not.toHaveProperty('dataset')
|
|
221
|
+
expect(client.config()).toEqual(
|
|
222
|
+
expect.objectContaining({
|
|
223
|
+
'~experimental_resource': {type: 'media-library', id: 'media-lib-123'},
|
|
224
|
+
}),
|
|
225
|
+
)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('should create resource when canvas source is provided and be projectless', () => {
|
|
229
|
+
const source = canvasSource('canvas-123')
|
|
230
|
+
const client = getClient(instance, {apiVersion: '2024-11-12', source})
|
|
231
|
+
|
|
232
|
+
expect(vi.mocked(createClient)).toHaveBeenCalledWith(
|
|
233
|
+
expect.objectContaining({
|
|
234
|
+
'~experimental_resource': {type: 'canvas', id: 'canvas-123'},
|
|
235
|
+
'apiVersion': '2024-11-12',
|
|
236
|
+
}),
|
|
237
|
+
)
|
|
238
|
+
// Client should be projectless - no projectId/dataset in config
|
|
239
|
+
expect(client.config()).not.toHaveProperty('projectId')
|
|
240
|
+
expect(client.config()).not.toHaveProperty('dataset')
|
|
241
|
+
expect(client.config()).toEqual(
|
|
242
|
+
expect.objectContaining({
|
|
243
|
+
'~experimental_resource': {type: 'canvas', id: 'canvas-123'},
|
|
244
|
+
}),
|
|
245
|
+
)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('should create projectless client when source is provided, ignoring instance config', () => {
|
|
249
|
+
const source = datasetSource('source-project', 'source-dataset')
|
|
250
|
+
const client = getClient(instance, {apiVersion: '2024-11-12', source})
|
|
251
|
+
|
|
252
|
+
// Client should be projectless - source takes precedence, instance config is ignored
|
|
253
|
+
expect(client.config()).not.toHaveProperty('projectId')
|
|
254
|
+
expect(client.config()).not.toHaveProperty('dataset')
|
|
255
|
+
expect(client.config()).toEqual(
|
|
256
|
+
expect.objectContaining({
|
|
257
|
+
source: expect.objectContaining({
|
|
258
|
+
__sanity_internal_sourceId: {
|
|
259
|
+
projectId: 'source-project',
|
|
260
|
+
dataset: 'source-dataset',
|
|
261
|
+
},
|
|
262
|
+
}),
|
|
263
|
+
}),
|
|
264
|
+
)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('should warn when both source and explicit projectId/dataset are provided', () => {
|
|
268
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
269
|
+
const source = datasetSource('source-project', 'source-dataset')
|
|
270
|
+
const client = getClient(instance, {
|
|
271
|
+
apiVersion: '2024-11-12',
|
|
272
|
+
source,
|
|
273
|
+
projectId: 'explicit-project',
|
|
274
|
+
dataset: 'explicit-dataset',
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
278
|
+
'Both source and explicit projectId/dataset are provided. The source will be used and projectId/dataset will be ignored.',
|
|
279
|
+
)
|
|
280
|
+
// Client should still be projectless despite explicit projectId/dataset
|
|
281
|
+
expect(client.config()).not.toHaveProperty('projectId')
|
|
282
|
+
expect(client.config()).not.toHaveProperty('dataset')
|
|
283
|
+
consoleSpy.mockRestore()
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('should create different clients for different sources', () => {
|
|
287
|
+
const source1 = datasetSource('project-1', 'dataset-1')
|
|
288
|
+
const source2 = datasetSource('project-2', 'dataset-2')
|
|
289
|
+
const source3 = mediaLibrarySource('media-lib-1')
|
|
290
|
+
|
|
291
|
+
const client1 = getClient(instance, {apiVersion: '2024-11-12', source: source1})
|
|
292
|
+
const client2 = getClient(instance, {apiVersion: '2024-11-12', source: source2})
|
|
293
|
+
const client3 = getClient(instance, {apiVersion: '2024-11-12', source: source3})
|
|
294
|
+
|
|
295
|
+
expect(client1).not.toBe(client2)
|
|
296
|
+
expect(client2).not.toBe(client3)
|
|
297
|
+
expect(client1).not.toBe(client3)
|
|
298
|
+
expect(vi.mocked(createClient)).toHaveBeenCalledTimes(3)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('should reuse clients with identical source configurations', () => {
|
|
302
|
+
const source = datasetSource('same-project', 'same-dataset')
|
|
303
|
+
const options = {apiVersion: '2024-11-12', source}
|
|
304
|
+
|
|
305
|
+
const client1 = getClient(instance, options)
|
|
306
|
+
const client2 = getClient(instance, options)
|
|
307
|
+
|
|
308
|
+
expect(client1).toBe(client2)
|
|
309
|
+
expect(vi.mocked(createClient)).toHaveBeenCalledTimes(1)
|
|
310
|
+
})
|
|
311
|
+
})
|
|
161
312
|
})
|
|
@@ -2,6 +2,7 @@ import {type ClientConfig, createClient, type SanityClient} from '@sanity/client
|
|
|
2
2
|
import {pick} from 'lodash-es'
|
|
3
3
|
|
|
4
4
|
import {getAuthMethodState, getTokenState} from '../auth/authStore'
|
|
5
|
+
import {type DocumentSource, SOURCE_ID} from '../config/sanityConfig'
|
|
5
6
|
import {bindActionGlobally} from '../store/createActionBinder'
|
|
6
7
|
import {createStateSourceAction} from '../store/createStateSourceAction'
|
|
7
8
|
import {defineStore, type StoreContext} from '../store/defineStore'
|
|
@@ -39,6 +40,7 @@ const allowedKeys = Object.keys({
|
|
|
39
40
|
'requestTagPrefix': null,
|
|
40
41
|
'useProjectHostname': null,
|
|
41
42
|
'~experimental_resource': null,
|
|
43
|
+
'source': null,
|
|
42
44
|
} satisfies Record<keyof ClientOptions, null>) as (keyof ClientOptions)[]
|
|
43
45
|
|
|
44
46
|
const DEFAULT_CLIENT_CONFIG: ClientConfig = {
|
|
@@ -90,6 +92,11 @@ export interface ClientOptions extends Pick<ClientConfig, AllowedClientConfigKey
|
|
|
90
92
|
* @internal
|
|
91
93
|
*/
|
|
92
94
|
'~experimental_resource'?: ClientConfig['~experimental_resource']
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @internal
|
|
98
|
+
*/
|
|
99
|
+
'source'?: DocumentSource
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
const clientStore = defineStore<ClientStoreState>({
|
|
@@ -142,6 +149,13 @@ const getClientConfigKey = (options: ClientOptions) => JSON.stringify(pick(optio
|
|
|
142
149
|
export const getClient = bindActionGlobally(
|
|
143
150
|
clientStore,
|
|
144
151
|
({state, instance}, options: ClientOptions) => {
|
|
152
|
+
if (!options || typeof options !== 'object') {
|
|
153
|
+
throw new Error(
|
|
154
|
+
'getClient() requires a configuration object with at least an "apiVersion" property. ' +
|
|
155
|
+
'Example: getClient(instance, { apiVersion: "2024-11-12" })',
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
145
159
|
// Check for disallowed keys
|
|
146
160
|
const providedKeys = Object.keys(options) as (keyof ClientOptions)[]
|
|
147
161
|
const disallowedKeys = providedKeys.filter((key) => !allowedKeys.includes(key))
|
|
@@ -156,18 +170,42 @@ export const getClient = bindActionGlobally(
|
|
|
156
170
|
|
|
157
171
|
const tokenFromState = state.get().token
|
|
158
172
|
const {clients, authMethod} = state.get()
|
|
173
|
+
const hasSource = !!options.source
|
|
174
|
+
let sourceId = options.source?.[SOURCE_ID]
|
|
175
|
+
|
|
176
|
+
let resource
|
|
177
|
+
if (Array.isArray(sourceId)) {
|
|
178
|
+
resource = {type: sourceId[0], id: sourceId[1]}
|
|
179
|
+
sourceId = undefined
|
|
180
|
+
}
|
|
181
|
+
|
|
159
182
|
const projectId = options.projectId ?? instance.config.projectId
|
|
160
183
|
const dataset = options.dataset ?? instance.config.dataset
|
|
161
184
|
const apiHost = options.apiHost ?? instance.config.auth?.apiHost
|
|
162
185
|
|
|
163
186
|
const effectiveOptions: ClientOptions = {
|
|
164
187
|
...DEFAULT_CLIENT_CONFIG,
|
|
165
|
-
...((options.scope === 'global' || !projectId) && {useProjectHostname: false}),
|
|
188
|
+
...((options.scope === 'global' || !projectId || hasSource) && {useProjectHostname: false}),
|
|
166
189
|
token: authMethod === 'cookie' ? undefined : (tokenFromState ?? undefined),
|
|
167
190
|
...options,
|
|
168
191
|
...(projectId && {projectId}),
|
|
169
192
|
...(dataset && {dataset}),
|
|
170
193
|
...(apiHost && {apiHost}),
|
|
194
|
+
...(resource && {'~experimental_resource': resource}),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// When a source is provided, don't use projectId/dataset - the client should be "projectless"
|
|
198
|
+
// The client code itself will ignore the non-source config, so we do this to prevent confusing the user.
|
|
199
|
+
// (ref: https://github.com/sanity-io/client/blob/5c23f81f5ab93a53f5b22b39845c867988508d84/src/data/dataMethods.ts#L691)
|
|
200
|
+
if (hasSource) {
|
|
201
|
+
if (options.projectId || options.dataset) {
|
|
202
|
+
// eslint-disable-next-line no-console
|
|
203
|
+
console.warn(
|
|
204
|
+
'Both source and explicit projectId/dataset are provided. The source will be used and projectId/dataset will be ignored.',
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
delete effectiveOptions.projectId
|
|
208
|
+
delete effectiveOptions.dataset
|
|
171
209
|
}
|
|
172
210
|
|
|
173
211
|
if (effectiveOptions.token === null || typeof effectiveOptions.token === 'undefined') {
|
|
@@ -81,3 +81,44 @@ export interface SanityConfig extends DatasetHandle, PerspectiveHandle {
|
|
|
81
81
|
enabled: boolean
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
|
|
85
|
+
export const SOURCE_ID = '__sanity_internal_sourceId'
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A document source can be used for querying.
|
|
89
|
+
*
|
|
90
|
+
* @beta
|
|
91
|
+
* @see datasetSource Construct a document source for a given projectId and dataset.
|
|
92
|
+
* @see mediaLibrarySource Construct a document source for a mediaLibraryId.
|
|
93
|
+
* @see canvasSource Construct a document source for a canvasId.
|
|
94
|
+
*/
|
|
95
|
+
export type DocumentSource = {
|
|
96
|
+
[SOURCE_ID]: ['media-library', string] | ['canvas', string] | {projectId: string; dataset: string}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Returns a document source for a projectId and dataset.
|
|
101
|
+
*
|
|
102
|
+
* @beta
|
|
103
|
+
*/
|
|
104
|
+
export function datasetSource(projectId: string, dataset: string): DocumentSource {
|
|
105
|
+
return {[SOURCE_ID]: {projectId, dataset}}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Returns a document source for a Media Library.
|
|
110
|
+
*
|
|
111
|
+
* @beta
|
|
112
|
+
*/
|
|
113
|
+
export function mediaLibrarySource(id: string): DocumentSource {
|
|
114
|
+
return {[SOURCE_ID]: ['media-library', id]}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Returns a document source for a Canvas.
|
|
119
|
+
*
|
|
120
|
+
* @beta
|
|
121
|
+
*/
|
|
122
|
+
export function canvasSource(id: string): DocumentSource {
|
|
123
|
+
return {[SOURCE_ID]: ['canvas', id]}
|
|
124
|
+
}
|