@sanity/sdk 3.8.0 → 3.9.0-next.20261007114705
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/_exports/_internal.d.ts +8 -2
- package/dist/_exports/_internal.d.ts.map +1 -1
- package/dist/_exports/_internal.js +8 -8
- package/dist/_exports/agent.js +1 -1
- package/dist/_exports/dashboard.d.ts +2 -2
- package/dist/_exports/dashboard.js +2 -2
- package/dist/{applications-CaitEfgp.d.ts → applications-BioeDdGe.d.ts} +46 -11
- package/dist/{applications-CaitEfgp.d.ts.map → applications-BioeDdGe.d.ts.map} +1 -1
- package/dist/{authStore-DRX3qU3e.js → authStore-CwZPW3jK.js} +10 -2
- package/dist/{authStore-DRX3qU3e.js.map → authStore-CwZPW3jK.js.map} +1 -1
- package/dist/{bus-BjkKneLg.d.ts → bus-BULkpPgi.d.ts} +2 -2
- package/dist/{bus-BjkKneLg.d.ts.map → bus-BULkpPgi.d.ts.map} +1 -1
- package/dist/{clientStore-mFP_nOqf.js → clientStore-iv9ooGew.js} +2 -2
- package/dist/{clientStore-mFP_nOqf.js.map → clientStore-iv9ooGew.js.map} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -4
- package/dist/{topicStore-D5RKaWQk.js → topicStore-CjKrnfeL.js} +2 -2
- package/dist/{topicStore-D5RKaWQk.js.map → topicStore-CjKrnfeL.js.map} +1 -1
- package/dist/{version-asrkIujR.js → version-D68H4Kcd.js} +3 -3
- package/dist/{version-asrkIujR.js.map → version-D68H4Kcd.js.map} +1 -1
- package/package.json +2 -2
- package/src/_exports/_internal.ts +1 -0
- package/src/auth/utils.test.ts +25 -0
- package/src/auth/utils.ts +10 -1
- package/src/config/__tests__/handles.test-d.ts +24 -0
- package/src/typegen/resolve.test-d.ts +105 -0
- package/src/typegen/resolve.ts +61 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0-next.20261007114705",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK",
|
|
6
6
|
"keywords": [
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"@types/node": "^24.19.1",
|
|
74
74
|
"@vitest/coverage-v8": "^5.0.3",
|
|
75
75
|
"eslint": "^10.12.0",
|
|
76
|
-
"oxfmt": "^0.
|
|
76
|
+
"oxfmt": "^0.72.0",
|
|
77
77
|
"rolldown": "^1.2.12",
|
|
78
78
|
"typescript": "^6.0.3",
|
|
79
79
|
"vite": "^8.3.2",
|
package/src/auth/utils.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {ClientError} from '@sanity/client'
|
|
1
2
|
import {EMPTY, NEVER} from 'rxjs'
|
|
2
3
|
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
4
|
|
|
@@ -5,6 +6,7 @@ import {AUTH_CODE_PARAM, DEFAULT_BASE} from './authConstants'
|
|
|
5
6
|
import {
|
|
6
7
|
getAuthCode,
|
|
7
8
|
getCleanedUrl,
|
|
9
|
+
getClientErrorApiProjectId,
|
|
8
10
|
getDefaultLocation,
|
|
9
11
|
getDefaultStorage,
|
|
10
12
|
getStorageEvents,
|
|
@@ -264,3 +266,26 @@ describe('getCleanedUrl', () => {
|
|
|
264
266
|
expect(cleaned).toBe('http://example.com/page#section')
|
|
265
267
|
})
|
|
266
268
|
})
|
|
269
|
+
|
|
270
|
+
describe('getClientErrorApiProjectId', () => {
|
|
271
|
+
const clientError = (body: unknown) =>
|
|
272
|
+
new ClientError({
|
|
273
|
+
statusCode: 401,
|
|
274
|
+
headers: {},
|
|
275
|
+
body,
|
|
276
|
+
url: 'https://exx11uqh.api.sanity.work/v2025-05-06/data/query/production',
|
|
277
|
+
method: 'GET',
|
|
278
|
+
} as ConstructorParameters<typeof ClientError>[0])
|
|
279
|
+
|
|
280
|
+
it('reads the project from the error body', () => {
|
|
281
|
+
expect(getClientErrorApiProjectId(clientError({error: {projectID: 'exx11uqh'}}))).toBe(
|
|
282
|
+
'exx11uqh',
|
|
283
|
+
)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('ignores a missing, empty or non-string project', () => {
|
|
287
|
+
expect(getClientErrorApiProjectId(clientError({error: {}}))).toBeUndefined()
|
|
288
|
+
expect(getClientErrorApiProjectId(clientError({error: {projectID: 42}}))).toBeUndefined()
|
|
289
|
+
expect(getClientErrorApiProjectId(clientError({error: {projectID: ''}}))).toBeUndefined()
|
|
290
|
+
})
|
|
291
|
+
})
|
package/src/auth/utils.ts
CHANGED
|
@@ -175,7 +175,7 @@ export function getCleanedUrl(locationUrl: string): string {
|
|
|
175
175
|
|
|
176
176
|
/** @internal */
|
|
177
177
|
export type ApiErrorBody = {
|
|
178
|
-
error?: {type?: string; description?: string}
|
|
178
|
+
error?: {type?: string; description?: string; projectID?: string}
|
|
179
179
|
type?: string
|
|
180
180
|
description?: string
|
|
181
181
|
message?: string
|
|
@@ -199,6 +199,15 @@ export function getClientErrorApiDescription(error: ClientError): string | undef
|
|
|
199
199
|
return body?.error?.description ?? body?.description
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Returns the project an API error refers to, if available.
|
|
204
|
+
* @internal
|
|
205
|
+
*/
|
|
206
|
+
export function getClientErrorApiProjectId(error: ClientError): string | undefined {
|
|
207
|
+
const projectId = getClientErrorApiBody(error)?.error?.projectID
|
|
208
|
+
return typeof projectId === 'string' && projectId ? projectId : undefined
|
|
209
|
+
}
|
|
210
|
+
|
|
202
211
|
/** @internal True if the error represents a projectUserNotFoundError. */
|
|
203
212
|
export function isProjectUserNotFoundClientError(error: ClientError): boolean {
|
|
204
213
|
return getClientErrorApiType(error) === 'projectUserNotFoundError'
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {expectTypeOf, test} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {createDocumentHandle} from '../handles'
|
|
4
|
+
|
|
5
|
+
// `createDocumentHandle` exists to capture the literal types of its input. Its runtime
|
|
6
|
+
// behavior, returning the input unchanged, is covered in `handles.test.ts` and is the
|
|
7
|
+
// uninteresting half: a signature change that dropped `TDataset` or `TProjectId` would
|
|
8
|
+
// pass every runtime test while silently removing inference for every consumer.
|
|
9
|
+
//
|
|
10
|
+
// Typegen looks results up by a `projectId.dataset` key, so a handle carrying `string`
|
|
11
|
+
// instead of a literal matches nothing.
|
|
12
|
+
|
|
13
|
+
test('createDocumentHandle preserves the literal types of its input', () => {
|
|
14
|
+
const handle = createDocumentHandle({
|
|
15
|
+
projectId: 'ppsg7ml5',
|
|
16
|
+
dataset: 'test',
|
|
17
|
+
documentId: 'some-id',
|
|
18
|
+
documentType: 'author',
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
expectTypeOf(handle.projectId).toEqualTypeOf<'ppsg7ml5' | undefined>()
|
|
22
|
+
expectTypeOf(handle.dataset).toEqualTypeOf<'test' | undefined>()
|
|
23
|
+
expectTypeOf(handle.documentType).toEqualTypeOf<'author'>()
|
|
24
|
+
})
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {expectTypeOf, test} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {type ActionsResult} from '../document/applyDocumentActions'
|
|
4
|
+
import {
|
|
5
|
+
type ResolveDocument,
|
|
6
|
+
type ResolveProjectionResult,
|
|
7
|
+
type ResolveQueryResult,
|
|
8
|
+
} from './resolve'
|
|
9
|
+
|
|
10
|
+
// Stands in for what a multi-resource `sanity typegen` run emits. Two resources whose
|
|
11
|
+
// schemas disagree, plus an object type that must not resolve as a document.
|
|
12
|
+
type TestPost = {
|
|
13
|
+
_id: string
|
|
14
|
+
_type: 'post'
|
|
15
|
+
_createdAt: string
|
|
16
|
+
_updatedAt: string
|
|
17
|
+
_rev: string
|
|
18
|
+
title: string
|
|
19
|
+
}
|
|
20
|
+
interface ProductionPost {
|
|
21
|
+
_id: string
|
|
22
|
+
_type: 'post'
|
|
23
|
+
_createdAt: string
|
|
24
|
+
_updatedAt: string
|
|
25
|
+
_rev: string
|
|
26
|
+
title: number
|
|
27
|
+
}
|
|
28
|
+
type Slug = {_type: 'slug'; current: string}
|
|
29
|
+
|
|
30
|
+
type TestPostsResult = {_id: string; title: string}[]
|
|
31
|
+
type ProductionPostsResult = {_id: string; title: number}[]
|
|
32
|
+
type TestTitleProjection = {title: string}
|
|
33
|
+
|
|
34
|
+
// Registered the way generated files do: as globals, which the interfaces `@sanity/client`
|
|
35
|
+
// exports extend. Every file in this package's type-check program sees these registrations, not
|
|
36
|
+
// only this test, so their keys must stay unique across the package's tests.
|
|
37
|
+
declare global {
|
|
38
|
+
interface SanitySchemasByResource {
|
|
39
|
+
'resolve1.test': TestPost | Slug
|
|
40
|
+
'resolve2.production': ProductionPost
|
|
41
|
+
}
|
|
42
|
+
interface SanityQueriesByResource {
|
|
43
|
+
'resolve1.test': {'*[_type == "post"]': TestPostsResult}
|
|
44
|
+
'resolve2.production': {'*[_type == "post"]': ProductionPostsResult}
|
|
45
|
+
}
|
|
46
|
+
interface SanityProjectionsByResource {
|
|
47
|
+
'resolve1.test': {post: {'{title}': TestTitleProjection}}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test('registered resources resolve documents declared as aliases or interfaces', () => {
|
|
52
|
+
expectTypeOf<ResolveDocument<'post', 'resolve1.test'>>().toEqualTypeOf<TestPost>()
|
|
53
|
+
expectTypeOf<ResolveDocument<'post', 'resolve2.production'>>().toEqualTypeOf<ProductionPost>()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('interface registrations remain usable in document action results', () => {
|
|
57
|
+
type Result = ActionsResult<ResolveDocument<'post', 'resolve2.production'>>
|
|
58
|
+
expectTypeOf<NonNullable<Result['documents'][string]>>().toEqualTypeOf<ProductionPost>()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('a document type absent from a registered resource is never', () => {
|
|
62
|
+
// Not the legacy union. Falling through would describe the wrong dataset while looking
|
|
63
|
+
// precise.
|
|
64
|
+
expectTypeOf<ResolveDocument<'movie', 'resolve1.test'>>().toEqualTypeOf<never>()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('an object type in the schema union is not a document', () => {
|
|
68
|
+
expectTypeOf<ResolveDocument<'slug', 'resolve1.test'>>().toEqualTypeOf<never>()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('an unconstrained document type yields the resource documents only', () => {
|
|
72
|
+
expectTypeOf<ResolveDocument<string, 'resolve1.test'>>().toEqualTypeOf<TestPost>()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('an unregistered resource falls back to the legacy lookup', () => {
|
|
76
|
+
// The legacy path answers with the base document shape when nothing is registered, which
|
|
77
|
+
// is what keeps saved experimental output working.
|
|
78
|
+
expectTypeOf<ResolveDocument<string, 'unregistered.dataset'>['_id']>().toEqualTypeOf<string>()
|
|
79
|
+
expectTypeOf<ResolveDocument<'post', 'unregistered.dataset'>>().toEqualTypeOf<never>()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('the same query text resolves to a different type per resource', () => {
|
|
83
|
+
expectTypeOf<
|
|
84
|
+
ResolveQueryResult<'*[_type == "post"]', 'resolve1.test'>
|
|
85
|
+
>().toEqualTypeOf<TestPostsResult>()
|
|
86
|
+
expectTypeOf<
|
|
87
|
+
ResolveQueryResult<'*[_type == "post"]', 'resolve2.production'>
|
|
88
|
+
>().toEqualTypeOf<ProductionPostsResult>()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('an unregistered query on a registered resource is never', () => {
|
|
92
|
+
expectTypeOf<ResolveQueryResult<'*[_type == "movie"]', 'resolve1.test'>>().toEqualTypeOf<never>()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('a projection resolves by resource and document type', () => {
|
|
96
|
+
expectTypeOf<
|
|
97
|
+
ResolveProjectionResult<'{title}', 'post', 'resolve1.test'>
|
|
98
|
+
>().toEqualTypeOf<TestTitleProjection>()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('a projection on a document type with no registrations is never', () => {
|
|
102
|
+
expectTypeOf<
|
|
103
|
+
ResolveProjectionResult<'{title}', 'movie', 'resolve1.test'>
|
|
104
|
+
>().toEqualTypeOf<never>()
|
|
105
|
+
})
|
package/src/typegen/resolve.ts
CHANGED
|
@@ -1,32 +1,79 @@
|
|
|
1
1
|
import './groqCompat'
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
type SanityProjectionsByResource,
|
|
5
|
+
type SanityQueriesByResource,
|
|
6
|
+
type SanitySchemasByResource,
|
|
7
|
+
} from '@sanity/client'
|
|
3
8
|
import {type SanityDocument, type SanityProjectionResult, type SanityQueryResult} from 'groq'
|
|
4
9
|
|
|
5
10
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
11
|
+
* Indexes `T` by `K`, or `never` when `K` is not a key of `T`.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately exact. The legacy `groq` lookup falls back to the union of every registered
|
|
14
|
+
* value on a miss, which lets a query resolve to another dataset's result type.
|
|
15
|
+
*/
|
|
16
|
+
type At<T, K> = K extends keyof T ? T[K] : never
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fields Sanity adds to every document. A resource's schema union also carries object types
|
|
20
|
+
* such as `slug`, which lack these, so this separates documents from the rest.
|
|
21
|
+
* No index signature is required, so both type aliases and interfaces are supported.
|
|
22
|
+
*/
|
|
23
|
+
interface DocumentFields {
|
|
24
|
+
_id: string
|
|
25
|
+
_type: string
|
|
26
|
+
_createdAt: string
|
|
27
|
+
_updatedAt: string
|
|
28
|
+
_rev: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Copies document fields into a type alias so interface registrations satisfy the
|
|
33
|
+
* record constraints used by document actions without adding an index signature.
|
|
34
|
+
*/
|
|
35
|
+
type DocumentRecord<T> = {[K in keyof T]: T[K]}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolves a document type for one resource.
|
|
39
|
+
*
|
|
40
|
+
* A resource registered in `SanitySchemasByResource` is answered from there and only there:
|
|
41
|
+
* a document type absent from a registered resource is `never`, never the legacy union,
|
|
42
|
+
* because an unrelated union looks precise while describing the wrong dataset. A resource
|
|
43
|
+
* with no registration falls back to the experimental Typegen declarations, which is what
|
|
44
|
+
* keeps saved generated files working.
|
|
8
45
|
*
|
|
9
46
|
* @beta
|
|
10
47
|
*/
|
|
11
48
|
export type ResolveDocument<
|
|
12
49
|
TDocumentType extends string = string,
|
|
13
50
|
TSchemaId extends string = string,
|
|
14
|
-
> =
|
|
51
|
+
> = TSchemaId extends keyof SanitySchemasByResource
|
|
52
|
+
? DocumentRecord<
|
|
53
|
+
Extract<Extract<SanitySchemasByResource[TSchemaId], DocumentFields>, {_type: TDocumentType}>
|
|
54
|
+
>
|
|
55
|
+
: SanityDocument<TDocumentType, TSchemaId>
|
|
15
56
|
|
|
16
57
|
/**
|
|
17
|
-
* Resolves a query
|
|
18
|
-
*
|
|
58
|
+
* Resolves a query result for one resource, selected by the exact query text.
|
|
59
|
+
*
|
|
60
|
+
* Falls back to the experimental Typegen declarations for an unregistered resource. See
|
|
61
|
+
* {@link ResolveDocument} for why a registered resource does not fall through.
|
|
19
62
|
*
|
|
20
63
|
* @beta
|
|
21
64
|
*/
|
|
22
65
|
export type ResolveQueryResult<
|
|
23
66
|
TQuery extends string = string,
|
|
24
67
|
TSchemaId extends string = string,
|
|
25
|
-
> =
|
|
68
|
+
> = TSchemaId extends keyof SanityQueriesByResource
|
|
69
|
+
? At<SanityQueriesByResource[TSchemaId], TQuery>
|
|
70
|
+
: SanityQueryResult<TQuery, TSchemaId>
|
|
26
71
|
|
|
27
72
|
/**
|
|
28
|
-
* Resolves a projection
|
|
29
|
-
*
|
|
73
|
+
* Resolves a projection result for one resource and document type.
|
|
74
|
+
*
|
|
75
|
+
* A projection runs against the document its handle names, so the same projection text
|
|
76
|
+
* resolves differently per document type as well as per resource.
|
|
30
77
|
*
|
|
31
78
|
* @beta
|
|
32
79
|
*/
|
|
@@ -34,4 +81,9 @@ export type ResolveProjectionResult<
|
|
|
34
81
|
TProjection extends string = string,
|
|
35
82
|
TDocumentType extends string = string,
|
|
36
83
|
TSchemaId extends string = string,
|
|
37
|
-
> =
|
|
84
|
+
> = TSchemaId extends keyof SanityProjectionsByResource
|
|
85
|
+
? // `Extract<..., object>` keeps this assignable to the `object` bound that
|
|
86
|
+
// `ProjectionValuePending` places on a projection result. A projection always selects
|
|
87
|
+
// fields into an object, so nothing is lost.
|
|
88
|
+
Extract<At<At<SanityProjectionsByResource[TSchemaId], TDocumentType>, TProjection>, object>
|
|
89
|
+
: SanityProjectionResult<TProjection, TDocumentType, TSchemaId>
|