@sanity/client 0.0.0-dev.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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1225 -0
  3. package/dist/index.browser.cjs +1760 -0
  4. package/dist/index.browser.cjs.map +1 -0
  5. package/dist/index.browser.js +1737 -0
  6. package/dist/index.browser.js.map +1 -0
  7. package/dist/index.cjs +1769 -0
  8. package/dist/index.cjs.js +16 -0
  9. package/dist/index.cjs.map +1 -0
  10. package/dist/index.d.ts +2234 -0
  11. package/dist/index.js +1746 -0
  12. package/dist/index.js.map +1 -0
  13. package/package.json +127 -0
  14. package/src/SanityClient.ts +1253 -0
  15. package/src/assets/AssetsClient.ts +191 -0
  16. package/src/config.ts +96 -0
  17. package/src/data/dataMethods.ts +416 -0
  18. package/src/data/encodeQueryString.ts +29 -0
  19. package/src/data/listen.ts +196 -0
  20. package/src/data/patch.ts +345 -0
  21. package/src/data/transaction.ts +358 -0
  22. package/src/datasets/DatasetsClient.ts +115 -0
  23. package/src/generateHelpUrl.ts +5 -0
  24. package/src/http/browserMiddleware.ts +1 -0
  25. package/src/http/errors.ts +68 -0
  26. package/src/http/nodeMiddleware.ts +11 -0
  27. package/src/http/request.ts +48 -0
  28. package/src/http/requestOptions.ts +33 -0
  29. package/src/index.browser.ts +28 -0
  30. package/src/index.ts +28 -0
  31. package/src/projects/ProjectsClient.ts +61 -0
  32. package/src/types.ts +575 -0
  33. package/src/users/UsersClient.ts +55 -0
  34. package/src/util/defaults.ts +10 -0
  35. package/src/util/getSelection.ts +21 -0
  36. package/src/util/once.ts +14 -0
  37. package/src/util/pick.ts +11 -0
  38. package/src/validators.ts +78 -0
  39. package/src/warnings.ts +30 -0
  40. package/umd/.gitkeep +1 -0
  41. package/umd/sanityClient.js +4840 -0
  42. package/umd/sanityClient.min.js +14 -0
@@ -0,0 +1,191 @@
1
+ import {lastValueFrom, type Observable} from 'rxjs'
2
+ import {filter, map} from 'rxjs/operators'
3
+
4
+ import {_requestObservable} from '../data/dataMethods'
5
+ import type {ObservableSanityClient, SanityClient} from '../SanityClient'
6
+ import type {
7
+ Any,
8
+ HttpRequest,
9
+ HttpRequestEvent,
10
+ ResponseEvent,
11
+ SanityAssetDocument,
12
+ SanityImageAssetDocument,
13
+ UploadClientConfig,
14
+ } from '../types'
15
+ import * as validators from '../validators'
16
+
17
+ /** @internal */
18
+ export class ObservableAssetsClient {
19
+ #client: ObservableSanityClient
20
+ #httpRequest: HttpRequest
21
+ constructor(client: ObservableSanityClient, httpRequest: HttpRequest) {
22
+ this.#client = client
23
+ this.#httpRequest = httpRequest
24
+ }
25
+
26
+ /**
27
+ * Uploads a file asset to the configured dataset
28
+ *
29
+ * @param assetType - Asset type (file)
30
+ * @param body - Asset content - can be a browser File instance, a Blob, a Node.js Buffer instance or a Node.js ReadableStream.
31
+ * @param options - Options to use for the upload
32
+ */
33
+ upload(
34
+ assetType: 'file',
35
+ body: File | Blob | Buffer | NodeJS.ReadableStream,
36
+ options?: UploadClientConfig
37
+ ): Observable<HttpRequestEvent<{document: SanityAssetDocument}>>
38
+
39
+ /**
40
+ * Uploads an image asset to the configured dataset
41
+ *
42
+ * @param assetType - Asset type (image)
43
+ * @param body - Asset content - can be a browser File instance, a Blob, a Node.js Buffer instance or a Node.js ReadableStream.
44
+ * @param options - Options to use for the upload
45
+ */
46
+ upload(
47
+ assetType: 'image',
48
+ body: File | Blob | Buffer | NodeJS.ReadableStream,
49
+ options?: UploadClientConfig
50
+ ): Observable<HttpRequestEvent<{document: SanityImageAssetDocument}>>
51
+ /**
52
+ * Uploads a file or an image asset to the configured dataset
53
+ *
54
+ * @param assetType - Asset type (file/image)
55
+ * @param body - Asset content - can be a browser File instance, a Blob, a Node.js Buffer instance or a Node.js ReadableStream.
56
+ * @param options - Options to use for the upload
57
+ */
58
+ upload(
59
+ assetType: 'file' | 'image',
60
+ body: File | Blob | Buffer | NodeJS.ReadableStream,
61
+ options?: UploadClientConfig
62
+ ): Observable<HttpRequestEvent<{document: SanityAssetDocument | SanityImageAssetDocument}>>
63
+ upload(
64
+ assetType: 'file' | 'image',
65
+ body: File | Blob | Buffer | NodeJS.ReadableStream,
66
+ options?: UploadClientConfig
67
+ ): Observable<HttpRequestEvent<{document: SanityAssetDocument | SanityImageAssetDocument}>> {
68
+ return _upload(this.#client, this.#httpRequest, assetType, body, options)
69
+ }
70
+ }
71
+
72
+ /** @internal */
73
+ export class AssetsClient {
74
+ #client: SanityClient
75
+ #httpRequest: HttpRequest
76
+ constructor(client: SanityClient, httpRequest: HttpRequest) {
77
+ this.#client = client
78
+ this.#httpRequest = httpRequest
79
+ }
80
+
81
+ /**
82
+ * Uploads a file asset to the configured dataset
83
+ *
84
+ * @param assetType - Asset type (file)
85
+ * @param body - Asset content - can be a browser File instance, a Blob, a Node.js Buffer instance or a Node.js ReadableStream.
86
+ * @param options - Options to use for the upload
87
+ */
88
+ upload(
89
+ assetType: 'file',
90
+ body: File | Blob | Buffer | NodeJS.ReadableStream,
91
+ options?: UploadClientConfig
92
+ ): Promise<SanityAssetDocument>
93
+ /**
94
+ * Uploads an image asset to the configured dataset
95
+ *
96
+ * @param assetType - Asset type (image)
97
+ * @param body - Asset content - can be a browser File instance, a Blob, a Node.js Buffer instance or a Node.js ReadableStream.
98
+ * @param options - Options to use for the upload
99
+ */
100
+ upload(
101
+ assetType: 'image',
102
+ body: File | Blob | Buffer | NodeJS.ReadableStream,
103
+ options?: UploadClientConfig
104
+ ): Promise<SanityImageAssetDocument>
105
+ /**
106
+ * Uploads a file or an image asset to the configured dataset
107
+ *
108
+ * @param assetType - Asset type (file/image)
109
+ * @param body - Asset content - can be a browser File instance, a Blob, a Node.js Buffer instance or a Node.js ReadableStream.
110
+ * @param options - Options to use for the upload
111
+ */
112
+ upload(
113
+ assetType: 'file' | 'image',
114
+ body: File | Blob | Buffer | NodeJS.ReadableStream,
115
+ options?: UploadClientConfig
116
+ ): Promise<SanityAssetDocument | SanityImageAssetDocument>
117
+ upload(
118
+ assetType: 'file' | 'image',
119
+ body: File | Blob | Buffer | NodeJS.ReadableStream,
120
+ options?: UploadClientConfig
121
+ ): Promise<SanityAssetDocument | SanityImageAssetDocument> {
122
+ const observable = _upload(this.#client, this.#httpRequest, assetType, body, options)
123
+ return lastValueFrom(
124
+ observable.pipe(
125
+ filter((event: Any) => event.type === 'response'),
126
+ map(
127
+ (event) =>
128
+ (event as ResponseEvent<{document: SanityAssetDocument | SanityImageAssetDocument}>)
129
+ .body.document
130
+ )
131
+ )
132
+ )
133
+ }
134
+ }
135
+
136
+ function _upload(
137
+ client: SanityClient | ObservableSanityClient,
138
+ httpRequest: HttpRequest,
139
+ assetType: 'image' | 'file',
140
+ body: File | Blob | Buffer | NodeJS.ReadableStream,
141
+ opts: UploadClientConfig = {}
142
+ ): Observable<HttpRequestEvent<{document: SanityAssetDocument | SanityImageAssetDocument}>> {
143
+ validators.validateAssetType(assetType)
144
+
145
+ // If an empty array is given, explicitly set `none` to override API defaults
146
+ let meta = opts.extract || undefined
147
+ if (meta && !meta.length) {
148
+ meta = ['none']
149
+ }
150
+
151
+ const dataset = validators.hasDataset(client.config())
152
+ const assetEndpoint = assetType === 'image' ? 'images' : 'files'
153
+ const options = optionsFromFile(opts, body)
154
+ const {tag, label, title, description, creditLine, filename, source} = options
155
+ const query: Any = {
156
+ label,
157
+ title,
158
+ description,
159
+ filename,
160
+ meta,
161
+ creditLine,
162
+ }
163
+ if (source) {
164
+ query.sourceId = source.id
165
+ query.sourceName = source.name
166
+ query.sourceUrl = source.url
167
+ }
168
+ return _requestObservable(client, httpRequest, {
169
+ tag,
170
+ method: 'POST',
171
+ timeout: options.timeout || 0,
172
+ uri: `/assets/${assetEndpoint}/${dataset}`,
173
+ headers: options.contentType ? {'Content-Type': options.contentType} : {},
174
+ query,
175
+ body,
176
+ })
177
+ }
178
+
179
+ function optionsFromFile(opts: Record<string, Any>, file: Any) {
180
+ if (typeof window === 'undefined' || !(file instanceof window.File)) {
181
+ return opts
182
+ }
183
+
184
+ return Object.assign(
185
+ {
186
+ filename: opts.preserveFilename === false ? undefined : file.name,
187
+ contentType: file.type,
188
+ },
189
+ opts
190
+ )
191
+ }
package/src/config.ts ADDED
@@ -0,0 +1,96 @@
1
+ import {generateHelpUrl} from './generateHelpUrl'
2
+ import type {ClientConfig, InitializedClientConfig} from './types'
3
+ import * as validate from './validators'
4
+ import * as warnings from './warnings'
5
+
6
+ const defaultCdnHost = 'apicdn.sanity.io'
7
+ export const defaultConfig = {
8
+ apiHost: 'https://api.sanity.io',
9
+ apiVersion: '1',
10
+ useProjectHostname: true,
11
+ } satisfies ClientConfig
12
+
13
+ const LOCALHOSTS = ['localhost', '127.0.0.1', '0.0.0.0']
14
+ const isLocal = (host: string) => LOCALHOSTS.indexOf(host) !== -1
15
+
16
+ export const validateApiVersion = function validateApiVersion(apiVersion: string) {
17
+ if (apiVersion === '1' || apiVersion === 'X') {
18
+ return
19
+ }
20
+
21
+ const apiDate = new Date(apiVersion)
22
+ const apiVersionValid =
23
+ /^\d{4}-\d{2}-\d{2}$/.test(apiVersion) && apiDate instanceof Date && apiDate.getTime() > 0
24
+
25
+ if (!apiVersionValid) {
26
+ throw new Error('Invalid API version string, expected `1` or date in format `YYYY-MM-DD`')
27
+ }
28
+ }
29
+
30
+ export const initConfig = (
31
+ config: Partial<ClientConfig>,
32
+ prevConfig: Partial<ClientConfig>
33
+ ): InitializedClientConfig => {
34
+ const specifiedConfig = Object.assign({}, prevConfig, config)
35
+ if (!specifiedConfig.apiVersion) {
36
+ warnings.printNoApiVersionSpecifiedWarning()
37
+ }
38
+
39
+ const newConfig = Object.assign({} as InitializedClientConfig, defaultConfig, specifiedConfig)
40
+ const projectBased = newConfig.useProjectHostname
41
+
42
+ if (typeof Promise === 'undefined') {
43
+ const helpUrl = generateHelpUrl('js-client-promise-polyfill')
44
+ throw new Error(`No native Promise-implementation found, polyfill needed - see ${helpUrl}`)
45
+ }
46
+
47
+ if (projectBased && !newConfig.projectId) {
48
+ throw new Error('Configuration must contain `projectId`')
49
+ }
50
+
51
+ const isBrowser = typeof window !== 'undefined' && window.location && window.location.hostname
52
+ const isLocalhost = isBrowser && isLocal(window.location.hostname)
53
+
54
+ if (isBrowser && isLocalhost && newConfig.token && newConfig.ignoreBrowserTokenWarning !== true) {
55
+ warnings.printBrowserTokenWarning()
56
+ } else if (typeof newConfig.useCdn === 'undefined') {
57
+ warnings.printCdnWarning()
58
+ }
59
+
60
+ if (projectBased) {
61
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- the nullability here is wrong, as line 48 throws an error if it's undefined
62
+ validate.projectId(newConfig.projectId!)
63
+ }
64
+
65
+ if (newConfig.dataset) {
66
+ validate.dataset(newConfig.dataset)
67
+ }
68
+
69
+ if ('requestTagPrefix' in newConfig) {
70
+ // Allow setting and unsetting request tag prefix
71
+ newConfig.requestTagPrefix = newConfig.requestTagPrefix
72
+ ? validate.requestTag(newConfig.requestTagPrefix).replace(/\.+$/, '')
73
+ : undefined
74
+ }
75
+
76
+ newConfig.apiVersion = `${newConfig.apiVersion}`.replace(/^v/, '')
77
+ newConfig.isDefaultApi = newConfig.apiHost === defaultConfig.apiHost
78
+ newConfig.useCdn = Boolean(newConfig.useCdn) && !newConfig.withCredentials
79
+
80
+ validateApiVersion(newConfig.apiVersion)
81
+
82
+ const hostParts = newConfig.apiHost.split('://', 2)
83
+ const protocol = hostParts[0]
84
+ const host = hostParts[1]
85
+ const cdnHost = newConfig.isDefaultApi ? defaultCdnHost : host
86
+
87
+ if (newConfig.useProjectHostname) {
88
+ newConfig.url = `${protocol}://${newConfig.projectId}.${host}/v${newConfig.apiVersion}`
89
+ newConfig.cdnUrl = `${protocol}://${newConfig.projectId}.${cdnHost}/v${newConfig.apiVersion}`
90
+ } else {
91
+ newConfig.url = `${newConfig.apiHost}/v${newConfig.apiVersion}`
92
+ newConfig.cdnUrl = newConfig.url
93
+ }
94
+
95
+ return newConfig
96
+ }
@@ -0,0 +1,416 @@
1
+ import {type MonoTypeOperatorFunction, Observable} from 'rxjs'
2
+ import {filter, map} from 'rxjs/operators'
3
+
4
+ import {requestOptions} from '../http/requestOptions'
5
+ import type {ObservableSanityClient, SanityClient} from '../SanityClient'
6
+ import type {
7
+ AllDocumentIdsMutationOptions,
8
+ AllDocumentsMutationOptions,
9
+ Any,
10
+ BaseMutationOptions,
11
+ FilteredResponseQueryOptions,
12
+ FirstDocumentIdMutationOptions,
13
+ FirstDocumentMutationOptions,
14
+ HttpRequest,
15
+ HttpRequestEvent,
16
+ IdentifiedSanityDocumentStub,
17
+ MultipleMutationResult,
18
+ Mutation,
19
+ MutationSelection,
20
+ QueryParams,
21
+ RawQueryResponse,
22
+ RequestObservableOptions,
23
+ RequestOptions,
24
+ SanityDocument,
25
+ SingleMutationResult,
26
+ UnfilteredResponseQueryOptions,
27
+ } from '../types'
28
+ import {getSelection} from '../util/getSelection'
29
+ import * as validate from '../validators'
30
+ import * as validators from '../validators'
31
+ import {encodeQueryString} from './encodeQueryString'
32
+ import {ObservablePatch, Patch} from './patch'
33
+ import {ObservableTransaction, Transaction} from './transaction'
34
+
35
+ const excludeFalsey = (param: Any, defValue: Any) => {
36
+ const value = typeof param === 'undefined' ? defValue : param
37
+ return param === false ? undefined : value
38
+ }
39
+
40
+ const getMutationQuery = (options: BaseMutationOptions = {}) => {
41
+ return {
42
+ dryRun: options.dryRun,
43
+ returnIds: true,
44
+ returnDocuments: excludeFalsey(options.returnDocuments, true),
45
+ visibility: options.visibility || 'sync',
46
+ autoGenerateArrayKeys: options.autoGenerateArrayKeys,
47
+ skipCrossDatasetReferenceValidation: options.skipCrossDatasetReferenceValidation,
48
+ }
49
+ }
50
+
51
+ const isResponse = (event: Any) => event.type === 'response'
52
+ const getBody = (event: Any) => event.body
53
+
54
+ const indexBy = (docs: Any[], attr: Any) =>
55
+ docs.reduce((indexed, doc) => {
56
+ indexed[attr(doc)] = doc
57
+ return indexed
58
+ }, Object.create(null))
59
+
60
+ const getQuerySizeLimit = 11264
61
+
62
+ /** @internal */
63
+ export function _fetch<R, Q extends QueryParams>(
64
+ client: ObservableSanityClient | SanityClient,
65
+ httpRequest: HttpRequest,
66
+ query: string,
67
+ params?: Q,
68
+ options: FilteredResponseQueryOptions | UnfilteredResponseQueryOptions = {}
69
+ ): Observable<RawQueryResponse<R> | R> {
70
+ const mapResponse =
71
+ options.filterResponse === false ? (res: Any) => res : (res: Any) => res.result
72
+
73
+ return _dataRequest(client, httpRequest, 'query', {query, params}, options).pipe(map(mapResponse))
74
+ }
75
+
76
+ /** @internal */
77
+ export function _getDocument<R extends Record<string, Any>>(
78
+ client: ObservableSanityClient | SanityClient,
79
+ httpRequest: HttpRequest,
80
+ id: string,
81
+ opts: {tag?: string} = {}
82
+ ): Observable<SanityDocument<R> | undefined> {
83
+ const options = {uri: _getDataUrl(client, 'doc', id), json: true, tag: opts.tag}
84
+ return _requestObservable<SanityDocument<R> | undefined>(client, httpRequest, options).pipe(
85
+ filter(isResponse),
86
+ map((event) => event.body.documents && event.body.documents[0])
87
+ )
88
+ }
89
+
90
+ /** @internal */
91
+ export function _getDocuments<R extends Record<string, Any>>(
92
+ client: ObservableSanityClient | SanityClient,
93
+ httpRequest: HttpRequest,
94
+ ids: string[],
95
+ opts: {tag?: string} = {}
96
+ ): Observable<(SanityDocument<R> | null)[]> {
97
+ const options = {uri: _getDataUrl(client, 'doc', ids.join(',')), json: true, tag: opts.tag}
98
+ return _requestObservable<(SanityDocument<R> | null)[]>(client, httpRequest, options).pipe(
99
+ filter(isResponse),
100
+ map((event: Any) => {
101
+ const indexed = indexBy(event.body.documents || [], (doc: Any) => doc._id)
102
+ return ids.map((id) => indexed[id] || null)
103
+ })
104
+ )
105
+ }
106
+
107
+ /** @internal */
108
+ export function _createIfNotExists<R extends Record<string, Any>>(
109
+ client: ObservableSanityClient | SanityClient,
110
+ httpRequest: HttpRequest,
111
+ doc: IdentifiedSanityDocumentStub<R>,
112
+ options?:
113
+ | AllDocumentIdsMutationOptions
114
+ | AllDocumentsMutationOptions
115
+ | BaseMutationOptions
116
+ | FirstDocumentIdMutationOptions
117
+ | FirstDocumentMutationOptions
118
+ ): Observable<
119
+ SanityDocument<R> | SanityDocument<R>[] | SingleMutationResult | MultipleMutationResult
120
+ > {
121
+ validators.requireDocumentId('createIfNotExists', doc)
122
+ return _create<R>(client, httpRequest, doc, 'createIfNotExists', options)
123
+ }
124
+
125
+ /** @internal */
126
+ export function _createOrReplace<R extends Record<string, Any>>(
127
+ client: ObservableSanityClient | SanityClient,
128
+ httpRequest: HttpRequest,
129
+ doc: IdentifiedSanityDocumentStub<R>,
130
+ options?:
131
+ | AllDocumentIdsMutationOptions
132
+ | AllDocumentsMutationOptions
133
+ | BaseMutationOptions
134
+ | FirstDocumentIdMutationOptions
135
+ | FirstDocumentMutationOptions
136
+ ): Observable<
137
+ SanityDocument<R> | SanityDocument<R>[] | SingleMutationResult | MultipleMutationResult
138
+ > {
139
+ validators.requireDocumentId('createOrReplace', doc)
140
+ return _create<R>(client, httpRequest, doc, 'createOrReplace', options)
141
+ }
142
+
143
+ /** @internal */
144
+ export function _delete<R extends Record<string, Any>>(
145
+ client: ObservableSanityClient | SanityClient,
146
+ httpRequest: HttpRequest,
147
+ selection: string | MutationSelection,
148
+ options?:
149
+ | AllDocumentIdsMutationOptions
150
+ | AllDocumentsMutationOptions
151
+ | BaseMutationOptions
152
+ | FirstDocumentIdMutationOptions
153
+ | FirstDocumentMutationOptions
154
+ ): Observable<
155
+ SanityDocument<R> | SanityDocument<R>[] | SingleMutationResult | MultipleMutationResult
156
+ > {
157
+ return _dataRequest(
158
+ client,
159
+ httpRequest,
160
+ 'mutate',
161
+ {mutations: [{delete: getSelection(selection)}]},
162
+ options
163
+ )
164
+ }
165
+
166
+ /** @internal */
167
+ export function _mutate<R extends Record<string, Any>>(
168
+ client: SanityClient | ObservableSanityClient,
169
+ httpRequest: HttpRequest,
170
+ mutations: Mutation<R>[] | Patch | ObservablePatch | Transaction | ObservableTransaction,
171
+ options?:
172
+ | AllDocumentIdsMutationOptions
173
+ | AllDocumentsMutationOptions
174
+ | BaseMutationOptions
175
+ | FirstDocumentIdMutationOptions
176
+ | FirstDocumentMutationOptions
177
+ ): Observable<
178
+ SanityDocument<R> | SanityDocument<R>[] | SingleMutationResult | MultipleMutationResult
179
+ > {
180
+ const mut =
181
+ mutations instanceof Patch ||
182
+ mutations instanceof ObservablePatch ||
183
+ mutations instanceof Transaction ||
184
+ mutations instanceof ObservableTransaction
185
+ ? mutations.serialize()
186
+ : mutations
187
+
188
+ const muts = Array.isArray(mut) ? mut : [mut]
189
+ const transactionId = options && (options as Any).transactionId
190
+ return _dataRequest(client, httpRequest, 'mutate', {mutations: muts, transactionId}, options)
191
+ }
192
+
193
+ /**
194
+ * @internal
195
+ */
196
+ export function _dataRequest(
197
+ client: SanityClient | ObservableSanityClient,
198
+ httpRequest: HttpRequest,
199
+ endpoint: string,
200
+ body: Any,
201
+ options: Any = {}
202
+ ): Any {
203
+ const isMutation = endpoint === 'mutate'
204
+ const isQuery = endpoint === 'query'
205
+
206
+ // Check if the query string is within a configured threshold,
207
+ // in which case we can use GET. Otherwise, use POST.
208
+ const strQuery = isMutation ? '' : encodeQueryString(body)
209
+ const useGet = !isMutation && strQuery.length < getQuerySizeLimit
210
+ const stringQuery = useGet ? strQuery : ''
211
+ const returnFirst = options.returnFirst
212
+ const {timeout, token, tag, headers} = options
213
+
214
+ const uri = _getDataUrl(client, endpoint, stringQuery)
215
+
216
+ const reqOptions = {
217
+ method: useGet ? 'GET' : 'POST',
218
+ uri: uri,
219
+ json: true,
220
+ body: useGet ? undefined : body,
221
+ query: isMutation && getMutationQuery(options),
222
+ timeout,
223
+ headers,
224
+ token,
225
+ tag,
226
+ canUseCdn: isQuery,
227
+ signal: options.signal,
228
+ }
229
+
230
+ return _requestObservable(client, httpRequest, reqOptions).pipe(
231
+ filter(isResponse),
232
+ map(getBody),
233
+ map((res) => {
234
+ if (!isMutation) {
235
+ return res
236
+ }
237
+
238
+ // Should we return documents?
239
+ const results = res.results || []
240
+ if (options.returnDocuments) {
241
+ return returnFirst
242
+ ? results[0] && results[0].document
243
+ : results.map((mut: Any) => mut.document)
244
+ }
245
+
246
+ // Return a reduced subset
247
+ const key = returnFirst ? 'documentId' : 'documentIds'
248
+ const ids = returnFirst ? results[0] && results[0].id : results.map((mut: Any) => mut.id)
249
+ return {
250
+ transactionId: res.transactionId,
251
+ results: results,
252
+ [key]: ids,
253
+ }
254
+ })
255
+ )
256
+ }
257
+
258
+ /**
259
+ * @internal
260
+ */
261
+ export function _create<R extends Record<string, Any>>(
262
+ client: SanityClient | ObservableSanityClient,
263
+ httpRequest: HttpRequest,
264
+ doc: Any,
265
+ op: Any,
266
+ options: Any = {}
267
+ ): Observable<
268
+ SanityDocument<R> | SanityDocument<R>[] | SingleMutationResult | MultipleMutationResult
269
+ > {
270
+ const mutation = {[op]: doc}
271
+ const opts = Object.assign({returnFirst: true, returnDocuments: true}, options)
272
+ return _dataRequest(client, httpRequest, 'mutate', {mutations: [mutation]}, opts)
273
+ }
274
+
275
+ /**
276
+ * @internal
277
+ */
278
+ export function _requestObservable<R>(
279
+ client: SanityClient | ObservableSanityClient,
280
+ httpRequest: HttpRequest,
281
+ options: RequestObservableOptions
282
+ ): Observable<HttpRequestEvent<R>> {
283
+ const uri = options.url || (options.uri as string)
284
+ const config = client.config()
285
+
286
+ // If the `canUseCdn`-option is not set we detect it automatically based on the method + URL.
287
+ // Only the /data endpoint is currently available through API-CDN.
288
+ const canUseCdn =
289
+ typeof options.canUseCdn === 'undefined'
290
+ ? ['GET', 'HEAD'].indexOf(options.method || 'GET') >= 0 && uri.indexOf('/data/') === 0
291
+ : options.canUseCdn
292
+
293
+ const useCdn = config.useCdn && canUseCdn
294
+
295
+ const tag =
296
+ options.tag && config.requestTagPrefix
297
+ ? [config.requestTagPrefix, options.tag].join('.')
298
+ : options.tag || config.requestTagPrefix
299
+
300
+ if (tag) {
301
+ options.query = {tag: validate.requestTag(tag), ...options.query}
302
+ }
303
+
304
+ if (config.unstable_overlayDrafts) {
305
+ if (config.apiVersion !== 'X') {
306
+ // eslint-disable-next-line no-console
307
+ console.error('You need to set `apiVersion` to `X` to use `unstable_overlayDrafts')
308
+ }
309
+ options.query = {overlayDrafts: true, ...options.query}
310
+ }
311
+
312
+ const reqOptions = requestOptions(
313
+ config,
314
+ Object.assign({}, options, {
315
+ url: _getUrl(client, uri, useCdn),
316
+ })
317
+ ) as RequestOptions
318
+
319
+ const request = new Observable<HttpRequestEvent<R>>((subscriber) =>
320
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- the typings thinks it's optional because it's not required to specify it when calling createClient, but it's always defined in practice since SanityClient provides a default
321
+ httpRequest(reqOptions, config.requester!).subscribe(subscriber)
322
+ )
323
+
324
+ return options.signal ? request.pipe(_withAbortSignal(options.signal)) : request
325
+ }
326
+
327
+ /**
328
+ * @internal
329
+ */
330
+ export function _request<R>(
331
+ client: SanityClient | ObservableSanityClient,
332
+ httpRequest: HttpRequest,
333
+ options: Any
334
+ ): Observable<R> {
335
+ const observable = _requestObservable<R>(client, httpRequest, options).pipe(
336
+ filter((event: Any) => event.type === 'response'),
337
+ map((event: Any) => event.body)
338
+ )
339
+
340
+ return observable
341
+ }
342
+
343
+ /**
344
+ * @internal
345
+ */
346
+ export function _getDataUrl(
347
+ client: SanityClient | ObservableSanityClient,
348
+ operation: string,
349
+ path?: string
350
+ ): string {
351
+ const config = client.config()
352
+ const catalog = validators.hasDataset(config)
353
+ const baseUri = `/${operation}/${catalog}`
354
+ const uri = path ? `${baseUri}/${path}` : baseUri
355
+ return `/data${uri}`.replace(/\/($|\?)/, '$1')
356
+ }
357
+
358
+ /**
359
+ * @internal
360
+ */
361
+ export function _getUrl(
362
+ client: SanityClient | ObservableSanityClient,
363
+ uri: string,
364
+ canUseCdn = false
365
+ ): string {
366
+ const {url, cdnUrl} = client.config()
367
+ const base = canUseCdn ? cdnUrl : url
368
+ return `${base}/${uri.replace(/^\//, '')}`
369
+ }
370
+
371
+ /**
372
+ * @internal
373
+ */
374
+ function _withAbortSignal<T>(signal: AbortSignal): MonoTypeOperatorFunction<T> {
375
+ return (input) => {
376
+ return new Observable((observer) => {
377
+ const abort = () => observer.error(_createAbortError(signal))
378
+
379
+ if (signal && signal.aborted) {
380
+ abort()
381
+ return
382
+ }
383
+ const subscription = input.subscribe(observer)
384
+ signal.addEventListener('abort', abort)
385
+ return () => {
386
+ signal.removeEventListener('abort', abort)
387
+ subscription.unsubscribe()
388
+ }
389
+ })
390
+ }
391
+ }
392
+ // DOMException is supported on most modern browsers and Node.js 18+.
393
+ // @see https://developer.mozilla.org/en-US/docs/Web/API/DOMException#browser_compatibility
394
+ const isDomExceptionSupported = Boolean(globalThis.DOMException)
395
+
396
+ /**
397
+ * @internal
398
+ * @param signal
399
+ * Original source copied from https://github.com/sindresorhus/ky/blob/740732c78aad97e9aec199e9871bdbf0ae29b805/source/errors/DOMException.ts
400
+ * TODO: When targeting Node.js 18, use `signal.throwIfAborted()` (https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/throwIfAborted)
401
+ */
402
+ function _createAbortError(signal?: AbortSignal) {
403
+ /*
404
+ NOTE: Use DomException with AbortError name as specified in MDN docs (https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort)
405
+ > When abort() is called, the fetch() promise rejects with an Error of type DOMException, with name AbortError.
406
+ */
407
+ if (isDomExceptionSupported) {
408
+ return new DOMException(signal?.reason ?? 'The operation was aborted.', 'AbortError')
409
+ }
410
+
411
+ // DOMException not supported. Fall back to use of error and override name.
412
+ const error = new Error(signal?.reason ?? 'The operation was aborted.')
413
+ error.name = 'AbortError'
414
+
415
+ return error
416
+ }