@sanity/client 8.0.0 → 8.2.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.
Files changed (58) hide show
  1. package/README.md +265 -0
  2. package/dist/{browserUpload-CQgx9YYo.js → browserUpload-2tz6Sdqp.js} +4 -3
  3. package/dist/browserUpload-2tz6Sdqp.js.map +1 -0
  4. package/dist/{browserUpload-icWlVP15.js → browserUpload-CwpNx7Vl.js} +4 -3
  5. package/dist/browserUpload-CwpNx7Vl.js.map +1 -0
  6. package/dist/{config-a8VajuEY.js → config-3wiPP-sZ.js} +2 -2
  7. package/dist/config-3wiPP-sZ.js.map +1 -0
  8. package/dist/csm.js +2 -2
  9. package/dist/csm.js.map +1 -1
  10. package/dist/index.d.ts +17 -11
  11. package/dist/index.js +610 -136
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.node.d.ts +918 -26
  14. package/dist/index.node.js +559 -66
  15. package/dist/index.node.js.map +1 -1
  16. package/dist/media-library.d.ts +1 -1
  17. package/dist/{request-CJxcN16k.js → request-BhMuKj0D.js} +10 -9
  18. package/dist/request-BhMuKj0D.js.map +1 -0
  19. package/dist/{request-k7VS_NnC.js → request-SnMg7nUX.js} +10 -9
  20. package/dist/request-SnMg7nUX.js.map +1 -0
  21. package/dist/{resolveEditInfo-sq7yF78q.js → resolveEditInfo-Cz-smq3a.js} +17 -3
  22. package/dist/resolveEditInfo-Cz-smq3a.js.map +1 -0
  23. package/dist/stega.js +1 -1
  24. package/dist/{stegaEncodeSourceMap-DkoIlutY.js → stegaEncodeSourceMap-DbM2fTN4.js} +8 -2
  25. package/dist/stegaEncodeSourceMap-DbM2fTN4.js.map +1 -0
  26. package/dist/{stegaEncodeSourceMap-B2fGArSf.js → stegaEncodeSourceMap-YR3NQ3iz.js} +2 -2
  27. package/dist/{stegaEncodeSourceMap-B2fGArSf.js.map → stegaEncodeSourceMap-YR3NQ3iz.js.map} +1 -1
  28. package/dist/{types-CUxZSgB2.d.ts → types-nJhm5Nyq.d.ts} +910 -24
  29. package/package.json +26 -11
  30. package/src/SanityClient.ts +39 -20
  31. package/src/assets/AssetsClient.ts +54 -5
  32. package/src/collaboration/CollaborationCommentsClient.ts +387 -0
  33. package/src/collaboration/comments.ts +313 -0
  34. package/src/collaboration/types.ts +252 -0
  35. package/src/csm/applySourceDocuments.ts +2 -4
  36. package/src/csm/draftUtils.ts +23 -4
  37. package/src/data/dataMethods.ts +9 -20
  38. package/src/data/eventsource.ts +71 -41
  39. package/src/data/listen.ts +20 -5
  40. package/src/data/live.ts +17 -9
  41. package/src/data/resolveEventSourceFetch.ts +9 -1
  42. package/src/defineCreateClient.ts +5 -1
  43. package/src/functions/FunctionsClient.ts +66 -0
  44. package/src/functions/invoke.ts +176 -0
  45. package/src/http/browserUpload.ts +1 -0
  46. package/src/http/errors.ts +2 -1
  47. package/src/http/request.ts +8 -14
  48. package/src/mediaLibrary/MediaLibraryVideoClient.ts +1 -1
  49. package/src/types.ts +420 -4
  50. package/src/validators.ts +1 -1
  51. package/src/warnings.ts +7 -1
  52. package/dist/browserUpload-CQgx9YYo.js.map +0 -1
  53. package/dist/browserUpload-icWlVP15.js.map +0 -1
  54. package/dist/config-a8VajuEY.js.map +0 -1
  55. package/dist/request-CJxcN16k.js.map +0 -1
  56. package/dist/request-k7VS_NnC.js.map +0 -1
  57. package/dist/resolveEditInfo-sq7yF78q.js.map +0 -1
  58. package/dist/stegaEncodeSourceMap-DkoIlutY.js.map +0 -1
package/src/data/live.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import {EventSource} from 'eventsource'
2
+ import type {FetchFunction} from 'get-it'
2
3
  import {catchError, mergeMap, Observable, of, throwError} from 'rxjs'
3
4
  import {finalize, map} from 'rxjs/operators'
4
5
 
@@ -14,11 +15,12 @@ import type {
14
15
  LiveEventWelcome,
15
16
  SyncTag,
16
17
  } from '../types'
18
+ import {isRecord} from '../util/isRecord'
17
19
  import {shareReplayLatest} from '../util/shareReplayLatest'
18
20
  import {_getDataUrl} from './dataMethods'
19
21
  import {connectEventSource} from './eventsource'
20
22
  import {reconnectOnConnectionFailure} from './reconnectOnConnectionFailure'
21
- import {resolveEventSourceFetch} from './resolveEventSourceFetch'
23
+ import {pickBaseFetch, resolveEventSourceFetch} from './resolveEventSourceFetch'
22
24
 
23
25
  const requiredApiVersion = '2021-03-25'
24
26
 
@@ -132,6 +134,7 @@ export class LiveClient {
132
134
  new URL(this.#client.getUrl('/check/cors', false)),
133
135
  projectId,
134
136
  eventSourceWithCredentials,
137
+ pickBaseFetch(config),
135
138
  )
136
139
 
137
140
  const observable = events
@@ -221,35 +224,40 @@ function checkCorsObservable(
221
224
  url: URL,
222
225
  projectId: string | undefined,
223
226
  requireCredentials: boolean,
227
+ fetcher: FetchFunction,
224
228
  ): Observable<void> {
225
229
  return new Observable<void>((observer) => {
226
230
  const controller = new AbortController()
227
231
  const {signal} = controller
228
- fetch(url, {method: 'GET', mode: 'cors', credentials: 'omit', signal})
232
+ fetcher(url.href, {method: 'GET', credentials: 'omit', signal})
229
233
  .then((response) => {
230
234
  // Aborted or non-2xx: not a confirmed CORS rejection. Fall through with
231
235
  // an undefined body so the next step takes the silent-completion path.
232
- if (signal.aborted || !response.ok) return
233
- return response.json() as Promise<{
234
- result?: {allowed?: boolean; withCredentials?: boolean}
235
- }>
236
+ if (signal.aborted || !response.ok) return undefined
237
+ return response.text()
236
238
  })
237
- .then((body) => {
239
+ .then((text) => {
238
240
  if (signal.aborted) return
241
+ // `get-it`'s `FetchResponse` only guarantees `.text()`/`.arrayBuffer()`,
242
+ // not `.json()`, so the body is parsed by hand here. An empty/aborted
243
+ // fall-through (`text === undefined`) and malformed JSON both leave
244
+ // `result` unresolved, taking the same "no signal either way" path.
245
+ const parsed: unknown = text === undefined ? undefined : JSON.parse(text)
246
+ const result = isRecord(parsed) ? parsed.result : undefined
239
247
  // Check the credentialed case first: if the EventSource was about to
240
248
  // send credentials but the project's CORS config doesn't permit them,
241
249
  // the credentialed request would fail with a missing
242
250
  // `access-control-allow-credentials` header. Surface this as a CORS
243
251
  // rejection with `credentials: true` so the deep-link pre-selects the
244
252
  // "Allow credentials" toggle.
245
- if (requireCredentials && body?.result?.withCredentials === false) {
253
+ if (requireCredentials && isRecord(result) && result.withCredentials === false) {
246
254
  observer.error(new CorsOriginError({projectId, credentials: true}))
247
255
  return
248
256
  }
249
257
  // Generic case: the server actively rejected this origin. Propagate
250
258
  // `credentials: requireCredentials` so the deep-link still pre-selects
251
259
  // credentials when the caller needed them.
252
- if (body?.result?.allowed === false) {
260
+ if (isRecord(result) && result.allowed === false) {
253
261
  observer.error(new CorsOriginError({projectId, credentials: requireCredentials}))
254
262
  return
255
263
  }
@@ -88,7 +88,15 @@ export type EventSourceFetch = (
88
88
  init?: EventSourceFetchInit,
89
89
  ) => Promise<FetchLikeResponse>
90
90
 
91
- function pickBaseFetch(config: InitializedClientConfig): FetchFunction {
91
+ /**
92
+ * The fetch the client's own transport resolves for this config: the
93
+ * configured `resolveFetch` (honouring an explicit `proxy`) if present,
94
+ * otherwise the global fetch. Shared by the EventSource connection and the
95
+ * `/check/cors` probe so both resolve identically.
96
+ *
97
+ * @internal
98
+ */
99
+ export function pickBaseFetch(config: InitializedClientConfig): FetchFunction {
92
100
  if (config.resolveFetch) {
93
101
  return config.resolveFetch(typeof config.proxy === 'string' ? config.proxy : undefined)
94
102
  }
@@ -1,6 +1,8 @@
1
1
  import {defineRequester, type EnvironmentOptions} from './http/request'
2
2
  import type {ClientConfig, HttpRequest} from './types'
3
3
 
4
+ export {isTimeoutError, type TimeoutErrorLike} from 'get-it'
5
+
4
6
  export {validateApiPerspective} from './config'
5
7
  export {
6
8
  ChannelError,
@@ -55,10 +57,12 @@ export default function defineCreateClientExports<
55
57
  // this in `new Observable(...)` (see `_observe` in dataMethods).
56
58
  // Redirects are surfaced rather than followed unless a request opts in
57
59
  // (via the public `maxRedirects` option, translated in `requestOptions`).
58
- const httpRequest: HttpRequest = async (options) => {
60
+ const performRequest = async (options: Parameters<HttpRequest>[0]): Promise<unknown> => {
59
61
  const event = await clientRequesterPromise({redirect: 'manual', ...options})
60
62
  return event.body
61
63
  }
64
+ const httpRequest: HttpRequest = (options, requestHandler) =>
65
+ requestHandler ? requestHandler(options, performRequest) : performRequest(options)
62
66
  // Populate `requester` on the initialized config so internal paths
63
67
  // (e.g. the asset upload event stream) can reach the underlying transport.
64
68
  // `resolveFetch` is threaded onto the config so request building and
@@ -0,0 +1,66 @@
1
+ import {lastValueFrom, type Observable} from 'rxjs'
2
+
3
+ import type {ObservableSanityClient, SanityClient} from '../SanityClient'
4
+ import type {HttpRequest} from '../types'
5
+ import {_invoke, type InvokeFunctionRequest} from './invoke'
6
+
7
+ /** @public */
8
+ export class ObservableFunctionsClient {
9
+ #client: ObservableSanityClient
10
+ #httpRequest: HttpRequest
11
+ constructor(client: ObservableSanityClient, httpRequest: HttpRequest) {
12
+ this.#client = client
13
+ this.#httpRequest = httpRequest
14
+ }
15
+
16
+ /**
17
+ * Invoke a deployed function by its blueprint name.
18
+ *
19
+ * The name is resolved within the stack given by `stackId` on the request or
20
+ * the client config. Passes the function's return value once it finishes.
21
+ *
22
+ * @param functionName - name of the function, as declared in the blueprint
23
+ * @param request - payload and request options
24
+ */
25
+ invoke<R = unknown>(
26
+ functionName: string,
27
+ request?: InvokeFunctionRequest,
28
+ ): Observable<R | undefined> {
29
+ return _invoke<R>(this.#client, this.#httpRequest, functionName, request)
30
+ }
31
+ }
32
+
33
+ /** @public */
34
+ export class FunctionsClient {
35
+ #client: SanityClient
36
+ #httpRequest: HttpRequest
37
+ constructor(client: SanityClient, httpRequest: HttpRequest) {
38
+ this.#client = client
39
+ this.#httpRequest = httpRequest
40
+ }
41
+
42
+ /**
43
+ * Invoke a deployed function by its blueprint name.
44
+ *
45
+ * The name is resolved within the stack given by `stackId` on the request or
46
+ * the client config, which costs one extra request per call. Rejects if the
47
+ * stack has no function by that name, or if the name resolves to anything
48
+ * other than a `sanity.function.pubsub` function.
49
+ *
50
+ * The lookup is scoped to `projectId`, or to `organizationId` when one is set
51
+ * for a stack deployed at organization scope.
52
+ *
53
+ * The request stays open until the function finishes, and resolves with its
54
+ * return value, or `undefined` if it returns nothing. Long-running functions
55
+ * may need an explicit `timeout`.
56
+ *
57
+ * @param functionName - name of the function, as declared in the blueprint
58
+ * @param request - payload and request options
59
+ */
60
+ invoke<R = unknown>(
61
+ functionName: string,
62
+ request?: InvokeFunctionRequest,
63
+ ): Promise<R | undefined> {
64
+ return lastValueFrom(_invoke<R>(this.#client, this.#httpRequest, functionName, request))
65
+ }
66
+ }
@@ -0,0 +1,176 @@
1
+ import {defer, map, mergeMap, type Observable} from 'rxjs'
2
+
3
+ import {_requestObservable} from '../data/dataMethods'
4
+ import type {ObservableSanityClient, SanityClient} from '../SanityClient'
5
+ import type {HttpRequest, InitializedClientConfig} from '../types'
6
+
7
+ /** Function resource types in a blueprint are namespaced under this prefix. */
8
+ const FUNCTION_RESOURCE_PREFIX = 'sanity.function.'
9
+ const INVOKABLE_FUNCTION_TYPE = 'sanity.function.pubsub'
10
+
11
+ /** @public */
12
+ export interface InvokeFunctionEvent {
13
+ /**
14
+ * Payload handed to the function.
15
+ * The function receives it as `event.data`.
16
+ */
17
+ data?: unknown
18
+ }
19
+
20
+ /** @public */
21
+ export interface InvokeFunctionRequest {
22
+ event?: InvokeFunctionEvent
23
+ /**
24
+ * Stack to resolve the function name against.
25
+ * Overrides `stackId` from the client config.
26
+ */
27
+ stackId?: string
28
+ /**
29
+ * Organization owning the stack.
30
+ */
31
+ organizationId?: string
32
+ /**
33
+ * Milliseconds to wait for the function to return.
34
+ */
35
+ timeout?: number
36
+ /** Abort the invocation. */
37
+ signal?: AbortSignal
38
+ }
39
+
40
+ /**
41
+ * Subset of a stack resource the client needs in order to resolve a name.
42
+ *
43
+ * @internal
44
+ */
45
+ interface StackResource {
46
+ name: string
47
+ type: string
48
+ /** Provider-side id. For a function resource, the function id. */
49
+ externalId?: string
50
+ }
51
+
52
+ type Client = SanityClient | ObservableSanityClient
53
+
54
+ const scopeHeaders = (
55
+ config: InitializedClientConfig,
56
+ request: InvokeFunctionRequest | undefined,
57
+ ): Record<string, string> => {
58
+ const organizationId = request?.organizationId || config.organizationId
59
+ if (organizationId) {
60
+ return {
61
+ 'X-Sanity-Scope-Type': 'organization',
62
+ 'X-Sanity-Scope-Id': organizationId,
63
+ }
64
+ }
65
+
66
+ const {projectId} = config
67
+ if (!projectId) {
68
+ throw new Error(
69
+ '`functions.invoke()` requires a `projectId` to be set in the client config, or an ' +
70
+ '`organizationId` for a stack deployed at organization scope',
71
+ )
72
+ }
73
+
74
+ return {
75
+ 'X-Sanity-Scope-Type': 'project',
76
+ 'X-Sanity-Scope-Id': projectId,
77
+ }
78
+ }
79
+
80
+ /**
81
+ * A per-call `stackId` overrides the one in the client config.
82
+ */
83
+ const resolveStackId = (
84
+ config: InitializedClientConfig,
85
+ request: InvokeFunctionRequest | undefined,
86
+ ): string => {
87
+ const stackId = request?.stackId || config.stackId
88
+ if (!stackId) {
89
+ throw new Error(
90
+ '`functions.invoke()` requires a `stackId`, either in the client config or on the request. ' +
91
+ 'Function names are only unique within a stack.',
92
+ )
93
+ }
94
+
95
+ return stackId
96
+ }
97
+
98
+ /**
99
+ * The invoke route is keyed by function id, but callers know functions by the
100
+ * name declared in the blueprint. Names are unique within a stack, so the stack
101
+ * both makes the name resolvable and confines the call to its own functions.
102
+ *
103
+ * @internal
104
+ */
105
+ function _resolveFunctionId(
106
+ client: Client,
107
+ httpRequest: HttpRequest,
108
+ functionName: string,
109
+ stackId: string,
110
+ headers: Record<string, string>,
111
+ request: InvokeFunctionRequest | undefined,
112
+ ): Observable<string> {
113
+ return _requestObservable<{resources?: StackResource[]}>(client, httpRequest, {
114
+ method: 'GET',
115
+ url: `/blueprints/stacks/${stackId}`,
116
+ headers,
117
+ signal: request?.signal,
118
+ }).pipe(
119
+ map((stack) => {
120
+ const match = (stack?.resources || []).find(
121
+ (resource) =>
122
+ resource.type?.startsWith(FUNCTION_RESOURCE_PREFIX) && resource.name === functionName,
123
+ )
124
+
125
+ if (!match) {
126
+ throw new Error(`Function "${functionName}" not found in stack "${stackId}"`)
127
+ }
128
+
129
+ if (!match.externalId) {
130
+ throw new Error(
131
+ `Function "${functionName}" is declared in stack "${stackId}" but is not deployed`,
132
+ )
133
+ }
134
+
135
+ if (match.type !== INVOKABLE_FUNCTION_TYPE) {
136
+ throw new Error(`Function invocation is not supported for ${match.type}`)
137
+ }
138
+
139
+ return match.externalId
140
+ }),
141
+ )
142
+ }
143
+
144
+ /** @internal */
145
+ export function _invoke<R = unknown>(
146
+ client: Client,
147
+ httpRequest: HttpRequest,
148
+ functionName: string,
149
+ request?: InvokeFunctionRequest,
150
+ ): Observable<R | undefined> {
151
+ // Deferred so a bad config surfaces as an error on the returned observable
152
+ // (and so a rejected promise) rather than throwing at the call site.
153
+ return defer(() => {
154
+ const config = client.config()
155
+ const headers = scopeHeaders(config, request)
156
+ const stackId = resolveStackId(config, request)
157
+
158
+ return _resolveFunctionId(client, httpRequest, functionName, stackId, headers, request).pipe(
159
+ // A function that returns nothing answers 204, which the transport parses
160
+ // to an `undefined` body. The status code is not observable from here —
161
+ // the `HttpRequest` boundary resolves to the body alone — so an empty
162
+ // response and a function that returned nothing both surface as
163
+ // `undefined`.
164
+ mergeMap((functionId) =>
165
+ _requestObservable<R | undefined>(client, httpRequest, {
166
+ method: 'POST',
167
+ url: `/functions/${functionId}/invoke`,
168
+ headers,
169
+ body: {event: {data: request?.event?.data ?? {}}},
170
+ timeout: request?.timeout,
171
+ signal: request?.signal,
172
+ }),
173
+ ),
174
+ )
175
+ })
176
+ }
@@ -76,6 +76,7 @@ export function uploadWithProgress<T>(options: BrowserUploadOptions): Observable
76
76
  statusText: xhr.statusText,
77
77
  headers: errorHeaders,
78
78
  body: parseJsonText(xhr.responseText, errorHeaders),
79
+ url: xhr.responseURL,
79
80
  },
80
81
  url,
81
82
  method,
@@ -39,6 +39,7 @@ export function httpResponseFromFetch(
39
39
  statusText: string
40
40
  headers: Headers
41
41
  body: unknown
42
+ url: string
42
43
  },
43
44
  reqUrl: string,
44
45
  reqMethod: string,
@@ -48,7 +49,7 @@ export function httpResponseFromFetch(
48
49
  statusMessage: res.statusText || null,
49
50
  headers: headersToRecord(res.headers),
50
51
  body: res.body,
51
- url: reqUrl,
52
+ url: res.url || reqUrl,
52
53
  method: reqMethod,
53
54
  }
54
55
  }
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createRequester,
3
3
  type FetchFunction,
4
- HttpError as GetItHttpError,
4
+ isHttpError as isGetItHttpError,
5
5
  type RequestOptions as FetchRequestOptions,
6
6
  type TransformMiddleware,
7
7
  type WrappingMiddleware,
@@ -40,8 +40,9 @@ export interface ResponseEvent {
40
40
  }
41
41
 
42
42
  /**
43
- * Legacy "requester" type the result of `defineHttpRequest`. Returns a
44
- * single-event Observable for compatibility with the rest of the codebase.
43
+ * Legacy "requester" type - the observable half of `defineRequester`'s
44
+ * result. Returns a single-event Observable for compatibility with the rest
45
+ * of the codebase.
45
46
  *
46
47
  * @internal
47
48
  */
@@ -163,14 +164,6 @@ export function defineRequester(
163
164
  return {promise, observable}
164
165
  }
165
166
 
166
- /** @internal */
167
- export function defineHttpRequest(
168
- envOptions: EnvironmentOptions,
169
- config: HttpRequestConfig = {},
170
- ): LegacyRequester {
171
- return defineRequester(envOptions, config).observable
172
- }
173
-
174
167
  /**
175
168
  * Options describing the environment-specific defaults (Node vs. browser).
176
169
  *
@@ -208,7 +201,7 @@ async function executeRequest(
208
201
  try {
209
202
  response = await requester(fetchOptions)
210
203
  } catch (err) {
211
- if (err instanceof GetItHttpError) {
204
+ if (isGetItHttpError(err)) {
212
205
  // `err.body` is the response body as a string (get-it v9 stores the
213
206
  // already-decoded text), regardless of which response variant
214
207
  // `err.response` is.
@@ -220,6 +213,7 @@ async function executeRequest(
220
213
  statusText: err.statusText,
221
214
  headers: err.headers,
222
215
  body: errBody,
216
+ url: err.response.url ?? err.url,
223
217
  },
224
218
  url,
225
219
  method,
@@ -240,7 +234,7 @@ async function executeRequest(
240
234
  statusCode: response.status,
241
235
  statusMessage: response.statusText || null,
242
236
  headers: headersToRecord(response.headers),
243
- url,
237
+ url: response.url || url,
244
238
  method,
245
239
  }
246
240
  }
@@ -291,7 +285,7 @@ function headersToRecord(headers: Headers): Record<string, string> {
291
285
  function shouldRetryRequest(err: unknown, attempt: number, options: FetchRequestOptions): boolean {
292
286
  // HTTP errors aren't usually retryable, but Content Lake gives us a few
293
287
  // status codes where retrying *is* the right move.
294
- if (err instanceof GetItHttpError) {
288
+ if (isGetItHttpError(err)) {
295
289
  const isSafe = (options.method ?? 'GET') === 'GET' || options.method === 'HEAD'
296
290
  const isQuery = (options.url ?? '').includes('/data/query')
297
291
  const status = err.status
@@ -111,7 +111,7 @@ function isSanityReference(
111
111
  * @param assetIdentifier - The asset identifier - either a asset instance id or a Media Library GDR
112
112
  * @returns The asset instance id and library id
113
113
  */
114
- export function parseAssetInstanceId(assetIdentifier: MediaLibraryAssetInstanceIdentifier): {
114
+ function parseAssetInstanceId(assetIdentifier: MediaLibraryAssetInstanceIdentifier): {
115
115
  instanceId: string
116
116
  libraryId?: string
117
117
  } {