@sanity/client 4.0.0-alpha.esm.1 → 4.0.0-alpha.esm.2
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/node/sanityClient.js +1 -1
- package/package.json +2 -1
- package/src/assets/assetsClient.js +132 -0
- package/src/auth/authClient.js +13 -0
- package/src/config.js +93 -0
- package/src/data/dataMethods.js +182 -0
- package/src/data/encodeQueryString.js +18 -0
- package/src/data/listen.js +159 -0
- package/src/data/patch.js +119 -0
- package/src/data/transaction.js +103 -0
- package/src/datasets/datasetsClient.js +28 -0
- package/src/http/browserMiddleware.js +1 -0
- package/src/http/errors.js +53 -0
- package/src/http/nodeMiddleware.js +11 -0
- package/src/http/queryString.js +10 -0
- package/src/http/request.js +50 -0
- package/src/http/requestOptions.js +29 -0
- package/src/projects/projectsClient.js +13 -0
- package/src/sanityClient.js +124 -0
- package/src/users/usersClient.js +9 -0
- package/src/util/defaults.js +9 -0
- package/src/util/getSelection.js +17 -0
- package/src/util/observable.js +6 -0
- package/src/util/once.js +12 -0
- package/src/util/pick.js +9 -0
- package/src/validators.js +76 -0
- package/src/warnings.js +25 -0
|
@@ -721,7 +721,7 @@ import headers from "get-it/lib-node/middleware/headers";
|
|
|
721
721
|
|
|
722
722
|
// package.json
|
|
723
723
|
var name = "@sanity/client";
|
|
724
|
-
var version = "4.0.0-alpha.esm.
|
|
724
|
+
var version = "4.0.0-alpha.esm.2";
|
|
725
725
|
|
|
726
726
|
// src/http/nodeMiddleware.js
|
|
727
727
|
var middleware = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/client",
|
|
3
|
-
"version": "4.0.0-alpha.esm.
|
|
3
|
+
"version": "4.0.0-alpha.esm.2",
|
|
4
4
|
"description": "Client for retrieving, creating and patching data from Sanity.io",
|
|
5
5
|
"main": "lib/sanityClient.js",
|
|
6
6
|
"umd": "umd/sanityClient.min.js",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"module": "./dist/sanityClient.js",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
|
+
"source": "./src/sanityClient.js",
|
|
12
13
|
"browser": "./dist/browser/sanityClient.js",
|
|
13
14
|
"default": "./dist/node/sanityClient.js"
|
|
14
15
|
},
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {map, filter} from '../util/observable'
|
|
2
|
+
import {queryString} from '../http/queryString'
|
|
3
|
+
import * as validators from '../validators'
|
|
4
|
+
|
|
5
|
+
export class AssetsClient {
|
|
6
|
+
constructor(client) {
|
|
7
|
+
this.client = client
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Upload an asset
|
|
12
|
+
*
|
|
13
|
+
* @param {String} assetType `image` or `file`
|
|
14
|
+
* @param {File|Blob|Buffer|ReadableStream} body File to upload
|
|
15
|
+
* @param {Object} opts Options for the upload
|
|
16
|
+
* @param {Boolean} opts.preserveFilename Whether or not to preserve the original filename (default: true)
|
|
17
|
+
* @param {String} opts.filename Filename for this file (optional)
|
|
18
|
+
* @param {Number} opts.timeout Milliseconds to wait before timing the request out (default: 0)
|
|
19
|
+
* @param {String} opts.contentType Mime type of the file
|
|
20
|
+
* @param {Array} opts.extract Array of metadata parts to extract from image.
|
|
21
|
+
* Possible values: `location`, `exif`, `image`, `palette`
|
|
22
|
+
* @param {String} opts.label Label
|
|
23
|
+
* @param {String} opts.title Title
|
|
24
|
+
* @param {String} opts.description Description
|
|
25
|
+
* @param {String} opts.creditLine The credit to person(s) and/or organization(s) required by the supplier of the image to be used when published
|
|
26
|
+
* @param {Object} opts.source Source data (when the asset is from an external service)
|
|
27
|
+
* @param {String} opts.source.id The (u)id of the asset within the source, i.e. 'i-f323r1E'
|
|
28
|
+
* Required if source is defined
|
|
29
|
+
* @param {String} opts.source.name The name of the source, i.e. 'unsplash'
|
|
30
|
+
* Required if source is defined
|
|
31
|
+
* @param {String} opts.source.url A url to where to find the asset, or get more info about it in the source
|
|
32
|
+
* Optional
|
|
33
|
+
* @return {Promise} Resolves with the created asset document
|
|
34
|
+
*/
|
|
35
|
+
upload(assetType, body, opts = {}) {
|
|
36
|
+
validators.validateAssetType(assetType)
|
|
37
|
+
|
|
38
|
+
// If an empty array is given, explicitly set `none` to override API defaults
|
|
39
|
+
let meta = opts.extract || undefined
|
|
40
|
+
if (meta && !meta.length) {
|
|
41
|
+
meta = ['none']
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const dataset = validators.hasDataset(this.client.clientConfig)
|
|
45
|
+
const assetEndpoint = assetType === 'image' ? 'images' : 'files'
|
|
46
|
+
const options = optionsFromFile(opts, body)
|
|
47
|
+
const {tag, label, title, description, creditLine, filename, source} = options
|
|
48
|
+
const query = {
|
|
49
|
+
label,
|
|
50
|
+
title,
|
|
51
|
+
description,
|
|
52
|
+
filename,
|
|
53
|
+
meta,
|
|
54
|
+
creditLine,
|
|
55
|
+
}
|
|
56
|
+
if (source) {
|
|
57
|
+
query.sourceId = source.id
|
|
58
|
+
query.sourceName = source.name
|
|
59
|
+
query.sourceUrl = source.url
|
|
60
|
+
}
|
|
61
|
+
const observable = this.client._requestObservable({
|
|
62
|
+
tag,
|
|
63
|
+
method: 'POST',
|
|
64
|
+
timeout: options.timeout || 0,
|
|
65
|
+
uri: `/assets/${assetEndpoint}/${dataset}`,
|
|
66
|
+
headers: options.contentType ? {'Content-Type': options.contentType} : {},
|
|
67
|
+
query,
|
|
68
|
+
body,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
return this.client.isPromiseAPI()
|
|
72
|
+
? observable
|
|
73
|
+
.pipe(
|
|
74
|
+
filter((event) => event.type === 'response'),
|
|
75
|
+
map((event) => event.body.document)
|
|
76
|
+
)
|
|
77
|
+
.toPromise()
|
|
78
|
+
: observable
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
delete(type, id) {
|
|
82
|
+
// eslint-disable-next-line no-console
|
|
83
|
+
console.warn('client.assets.delete() is deprecated, please use client.delete(<document-id>)')
|
|
84
|
+
|
|
85
|
+
let docId = id || ''
|
|
86
|
+
if (!/^(image|file)-/.test(docId)) {
|
|
87
|
+
docId = `${type}-${docId}`
|
|
88
|
+
} else if (type._id) {
|
|
89
|
+
// We could be passing an entire asset document instead of an ID
|
|
90
|
+
docId = type._id
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
validators.hasDataset(this.client.clientConfig)
|
|
94
|
+
return this.client.delete(docId)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getImageUrl(ref, query) {
|
|
98
|
+
const id = ref._ref || ref
|
|
99
|
+
if (typeof id !== 'string') {
|
|
100
|
+
throw new Error(
|
|
101
|
+
'getImageUrl() needs either an object with a _ref, or a string with an asset document ID'
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!/^image-[A-Za-z0-9_]+-\d+x\d+-[a-z]{1,5}$/.test(id)) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Unsupported asset ID "${id}". URL generation only works for auto-generated IDs.`
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const [, assetId, size, format] = id.split('-')
|
|
112
|
+
|
|
113
|
+
validators.hasDataset(this.client.clientConfig)
|
|
114
|
+
const {projectId, dataset} = this.client.clientConfig
|
|
115
|
+
const qs = query ? queryString(query) : ''
|
|
116
|
+
return `https://cdn.sanity.io/images/${projectId}/${dataset}/${assetId}-${size}.${format}${qs}`
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function optionsFromFile(opts, file) {
|
|
121
|
+
if (typeof window === 'undefined' || !(file instanceof window.File)) {
|
|
122
|
+
return opts
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return Object.assign(
|
|
126
|
+
{
|
|
127
|
+
filename: opts.preserveFilename === false ? undefined : file.name,
|
|
128
|
+
contentType: file.type,
|
|
129
|
+
},
|
|
130
|
+
opts
|
|
131
|
+
)
|
|
132
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class AuthClient {
|
|
2
|
+
constructor(client) {
|
|
3
|
+
this.client = client
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
getLoginProviders() {
|
|
7
|
+
return this.client.request({uri: '/auth/providers'})
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
logout() {
|
|
11
|
+
return this.client.request({uri: '/auth/logout', method: 'POST'})
|
|
12
|
+
}
|
|
13
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {generateHelpUrl} from '@sanity/generate-help-url'
|
|
2
|
+
import * as validate from './validators'
|
|
3
|
+
import * as warnings from './warnings'
|
|
4
|
+
|
|
5
|
+
const defaultCdnHost = 'apicdn.sanity.io'
|
|
6
|
+
export const defaultConfig = {
|
|
7
|
+
apiHost: 'https://api.sanity.io',
|
|
8
|
+
apiVersion: '1',
|
|
9
|
+
useProjectHostname: true,
|
|
10
|
+
isPromiseAPI: true,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const LOCALHOSTS = ['localhost', '127.0.0.1', '0.0.0.0']
|
|
14
|
+
const isLocal = (host) => LOCALHOSTS.indexOf(host) !== -1
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line complexity
|
|
17
|
+
export const initConfig = (config, prevConfig) => {
|
|
18
|
+
const specifiedConfig = Object.assign({}, prevConfig, config)
|
|
19
|
+
if (!specifiedConfig.apiVersion) {
|
|
20
|
+
warnings.printNoApiVersionSpecifiedWarning()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const newConfig = Object.assign({}, defaultConfig, specifiedConfig)
|
|
24
|
+
const projectBased = newConfig.useProjectHostname
|
|
25
|
+
|
|
26
|
+
if (typeof Promise === 'undefined') {
|
|
27
|
+
const helpUrl = generateHelpUrl('js-client-promise-polyfill')
|
|
28
|
+
throw new Error(`No native Promise-implementation found, polyfill needed - see ${helpUrl}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (projectBased && !newConfig.projectId) {
|
|
32
|
+
throw new Error('Configuration must contain `projectId`')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const isBrowser = typeof window !== 'undefined' && window.location && window.location.hostname
|
|
36
|
+
const isLocalhost = isBrowser && isLocal(window.location.hostname)
|
|
37
|
+
|
|
38
|
+
if (isBrowser && isLocalhost && newConfig.token && newConfig.ignoreBrowserTokenWarning !== true) {
|
|
39
|
+
warnings.printBrowserTokenWarning()
|
|
40
|
+
} else if (typeof newConfig.useCdn === 'undefined') {
|
|
41
|
+
warnings.printCdnWarning()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (projectBased) {
|
|
45
|
+
validate.projectId(newConfig.projectId)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (newConfig.dataset) {
|
|
49
|
+
validate.dataset(newConfig.dataset)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if ('requestTagPrefix' in newConfig) {
|
|
53
|
+
// Allow setting and unsetting request tag prefix
|
|
54
|
+
newConfig.requestTagPrefix = newConfig.requestTagPrefix
|
|
55
|
+
? validate.requestTag(newConfig.requestTagPrefix).replace(/\.+$/, '')
|
|
56
|
+
: undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
newConfig.apiVersion = `${newConfig.apiVersion}`.replace(/^v/, '')
|
|
60
|
+
newConfig.isDefaultApi = newConfig.apiHost === defaultConfig.apiHost
|
|
61
|
+
newConfig.useCdn = Boolean(newConfig.useCdn) && !newConfig.withCredentials
|
|
62
|
+
|
|
63
|
+
validateApiVersion(newConfig.apiVersion)
|
|
64
|
+
|
|
65
|
+
const hostParts = newConfig.apiHost.split('://', 2)
|
|
66
|
+
const protocol = hostParts[0]
|
|
67
|
+
const host = hostParts[1]
|
|
68
|
+
const cdnHost = newConfig.isDefaultApi ? defaultCdnHost : host
|
|
69
|
+
|
|
70
|
+
if (newConfig.useProjectHostname) {
|
|
71
|
+
newConfig.url = `${protocol}://${newConfig.projectId}.${host}/v${newConfig.apiVersion}`
|
|
72
|
+
newConfig.cdnUrl = `${protocol}://${newConfig.projectId}.${cdnHost}/v${newConfig.apiVersion}`
|
|
73
|
+
} else {
|
|
74
|
+
newConfig.url = `${newConfig.apiHost}/v${newConfig.apiVersion}`
|
|
75
|
+
newConfig.cdnUrl = newConfig.url
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return newConfig
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function validateApiVersion(apiVersion) {
|
|
82
|
+
if (apiVersion === '1' || apiVersion === 'X') {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const apiDate = new Date(apiVersion)
|
|
87
|
+
const apiVersionValid =
|
|
88
|
+
/^\d{4}-\d{2}-\d{2}$/.test(apiVersion) && apiDate instanceof Date && apiDate.getTime() > 0
|
|
89
|
+
|
|
90
|
+
if (!apiVersionValid) {
|
|
91
|
+
throw new Error('Invalid API version string, expected `1` or date in format `YYYY-MM-DD`')
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import {map, filter} from '../util/observable'
|
|
2
|
+
import * as validators from '../validators'
|
|
3
|
+
import {getSelection} from '../util/getSelection'
|
|
4
|
+
import {encodeQueryString} from './encodeQueryString'
|
|
5
|
+
import {Transaction} from './transaction'
|
|
6
|
+
import {Patch} from './patch'
|
|
7
|
+
import {listen} from './listen'
|
|
8
|
+
|
|
9
|
+
const excludeFalsey = (param, defValue) => {
|
|
10
|
+
const value = typeof param === 'undefined' ? defValue : param
|
|
11
|
+
return param === false ? undefined : value
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const getMutationQuery = (options = {}) => {
|
|
15
|
+
return {
|
|
16
|
+
dryRun: options.dryRun,
|
|
17
|
+
returnIds: true,
|
|
18
|
+
returnDocuments: excludeFalsey(options.returnDocuments, true),
|
|
19
|
+
visibility: options.visibility || 'sync',
|
|
20
|
+
autoGenerateArrayKeys: options.autoGenerateArrayKeys,
|
|
21
|
+
skipCrossDatasetReferenceValidation: options.skipCrossDatasetReferenceValidation,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const isResponse = (event) => event.type === 'response'
|
|
26
|
+
const getBody = (event) => event.body
|
|
27
|
+
|
|
28
|
+
const indexBy = (docs, attr) =>
|
|
29
|
+
docs.reduce((indexed, doc) => {
|
|
30
|
+
indexed[attr(doc)] = doc
|
|
31
|
+
return indexed
|
|
32
|
+
}, Object.create(null))
|
|
33
|
+
|
|
34
|
+
const toPromise = (observable) => observable.toPromise()
|
|
35
|
+
|
|
36
|
+
const getQuerySizeLimit = 11264
|
|
37
|
+
|
|
38
|
+
export const dataMethods = {
|
|
39
|
+
listen: listen,
|
|
40
|
+
|
|
41
|
+
getDataUrl(operation, path) {
|
|
42
|
+
const config = this.clientConfig
|
|
43
|
+
const catalog = validators.hasDataset(config)
|
|
44
|
+
const baseUri = `/${operation}/${catalog}`
|
|
45
|
+
const uri = path ? `${baseUri}/${path}` : baseUri
|
|
46
|
+
return `/data${uri}`.replace(/\/($|\?)/, '$1')
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
fetch(query, params, options = {}) {
|
|
50
|
+
const mapResponse = options.filterResponse === false ? (res) => res : (res) => res.result
|
|
51
|
+
|
|
52
|
+
const observable = this._dataRequest('query', {query, params}, options).pipe(map(mapResponse))
|
|
53
|
+
return this.isPromiseAPI() ? toPromise(observable) : observable
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
getDocument(id, opts = {}) {
|
|
57
|
+
const options = {uri: this.getDataUrl('doc', id), json: true, tag: opts.tag}
|
|
58
|
+
const observable = this._requestObservable(options).pipe(
|
|
59
|
+
filter(isResponse),
|
|
60
|
+
map((event) => event.body.documents && event.body.documents[0])
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return this.isPromiseAPI() ? toPromise(observable) : observable
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
getDocuments(ids, opts = {}) {
|
|
67
|
+
const options = {uri: this.getDataUrl('doc', ids.join(',')), json: true, tag: opts.tag}
|
|
68
|
+
const observable = this._requestObservable(options).pipe(
|
|
69
|
+
filter(isResponse),
|
|
70
|
+
map((event) => {
|
|
71
|
+
const indexed = indexBy(event.body.documents || [], (doc) => doc._id)
|
|
72
|
+
return ids.map((id) => indexed[id] || null)
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return this.isPromiseAPI() ? toPromise(observable) : observable
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
create(doc, options) {
|
|
80
|
+
return this._create(doc, 'create', options)
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
createIfNotExists(doc, options) {
|
|
84
|
+
validators.requireDocumentId('createIfNotExists', doc)
|
|
85
|
+
return this._create(doc, 'createIfNotExists', options)
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
createOrReplace(doc, options) {
|
|
89
|
+
validators.requireDocumentId('createOrReplace', doc)
|
|
90
|
+
return this._create(doc, 'createOrReplace', options)
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
patch(selector, operations) {
|
|
94
|
+
return new Patch(selector, operations, this)
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
delete(selection, options) {
|
|
98
|
+
return this.dataRequest('mutate', {mutations: [{delete: getSelection(selection)}]}, options)
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
mutate(mutations, options) {
|
|
102
|
+
const mut =
|
|
103
|
+
mutations instanceof Patch || mutations instanceof Transaction
|
|
104
|
+
? mutations.serialize()
|
|
105
|
+
: mutations
|
|
106
|
+
|
|
107
|
+
const muts = Array.isArray(mut) ? mut : [mut]
|
|
108
|
+
const transactionId = options && options.transactionId
|
|
109
|
+
return this.dataRequest('mutate', {mutations: muts, transactionId}, options)
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
transaction(operations) {
|
|
113
|
+
return new Transaction(operations, this)
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
dataRequest(endpoint, body, options = {}) {
|
|
117
|
+
const request = this._dataRequest(endpoint, body, options)
|
|
118
|
+
|
|
119
|
+
return this.isPromiseAPI() ? toPromise(request) : request
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
_dataRequest(endpoint, body, options = {}) {
|
|
123
|
+
const isMutation = endpoint === 'mutate'
|
|
124
|
+
const isQuery = endpoint === 'query'
|
|
125
|
+
|
|
126
|
+
// Check if the query string is within a configured threshold,
|
|
127
|
+
// in which case we can use GET. Otherwise, use POST.
|
|
128
|
+
const strQuery = !isMutation && encodeQueryString(body)
|
|
129
|
+
const useGet = !isMutation && strQuery.length < getQuerySizeLimit
|
|
130
|
+
const stringQuery = useGet ? strQuery : ''
|
|
131
|
+
const returnFirst = options.returnFirst
|
|
132
|
+
const {timeout, token, tag, headers} = options
|
|
133
|
+
|
|
134
|
+
const uri = this.getDataUrl(endpoint, stringQuery)
|
|
135
|
+
|
|
136
|
+
const reqOptions = {
|
|
137
|
+
method: useGet ? 'GET' : 'POST',
|
|
138
|
+
uri: uri,
|
|
139
|
+
json: true,
|
|
140
|
+
body: useGet ? undefined : body,
|
|
141
|
+
query: isMutation && getMutationQuery(options),
|
|
142
|
+
timeout,
|
|
143
|
+
headers,
|
|
144
|
+
token,
|
|
145
|
+
tag,
|
|
146
|
+
canUseCdn: isQuery,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return this._requestObservable(reqOptions).pipe(
|
|
150
|
+
filter(isResponse),
|
|
151
|
+
map(getBody),
|
|
152
|
+
map((res) => {
|
|
153
|
+
if (!isMutation) {
|
|
154
|
+
return res
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Should we return documents?
|
|
158
|
+
const results = res.results || []
|
|
159
|
+
if (options.returnDocuments) {
|
|
160
|
+
return returnFirst
|
|
161
|
+
? results[0] && results[0].document
|
|
162
|
+
: results.map((mut) => mut.document)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Return a reduced subset
|
|
166
|
+
const key = returnFirst ? 'documentId' : 'documentIds'
|
|
167
|
+
const ids = returnFirst ? results[0] && results[0].id : results.map((mut) => mut.id)
|
|
168
|
+
return {
|
|
169
|
+
transactionId: res.transactionId,
|
|
170
|
+
results: results,
|
|
171
|
+
[key]: ids,
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
)
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
_create(doc, op, options = {}) {
|
|
178
|
+
const mutation = {[op]: doc}
|
|
179
|
+
const opts = Object.assign({returnFirst: true, returnDocuments: true}, options)
|
|
180
|
+
return this.dataRequest('mutate', {mutations: [mutation]}, opts)
|
|
181
|
+
},
|
|
182
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const enc = encodeURIComponent
|
|
2
|
+
|
|
3
|
+
export const encodeQueryString = ({query, params = {}, options = {}}) => {
|
|
4
|
+
// We generally want tag at the start of the query string
|
|
5
|
+
const {tag, ...opts} = options
|
|
6
|
+
const q = `query=${enc(query)}`
|
|
7
|
+
const base = tag ? `?tag=${enc(tag)}&${q}` : `?${q}`
|
|
8
|
+
|
|
9
|
+
const qString = Object.keys(params).reduce(
|
|
10
|
+
(qs, param) => `${qs}&${enc(`$${param}`)}=${enc(JSON.stringify(params[param]))}`,
|
|
11
|
+
base
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
return Object.keys(opts).reduce((qs, option) => {
|
|
15
|
+
// Only include the option if it is truthy
|
|
16
|
+
return options[option] ? `${qs}&${enc(option)}=${enc(options[option])}` : qs
|
|
17
|
+
}, qString)
|
|
18
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {Observable} from '../util/observable'
|
|
2
|
+
import polyfilledEventSource from '@sanity/eventsource'
|
|
3
|
+
import {pick} from '../util/pick'
|
|
4
|
+
import {defaults} from '../util/defaults'
|
|
5
|
+
import {encodeQueryString} from './encodeQueryString'
|
|
6
|
+
|
|
7
|
+
// Limit is 16K for a _request_, eg including headers. Have to account for an
|
|
8
|
+
// unknown range of headers, but an average EventSource request from Chrome seems
|
|
9
|
+
// to have around 700 bytes of cruft, so let us account for 1.2K to be "safe"
|
|
10
|
+
const MAX_URL_LENGTH = 16000 - 1200
|
|
11
|
+
const EventSource = polyfilledEventSource
|
|
12
|
+
|
|
13
|
+
const possibleOptions = [
|
|
14
|
+
'includePreviousRevision',
|
|
15
|
+
'includeResult',
|
|
16
|
+
'visibility',
|
|
17
|
+
'effectFormat',
|
|
18
|
+
'tag',
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
const defaultOptions = {
|
|
22
|
+
includeResult: true,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function listen(query, params, opts = {}) {
|
|
26
|
+
const {url, token, withCredentials, requestTagPrefix} = this.clientConfig
|
|
27
|
+
const tag = opts.tag && requestTagPrefix ? [requestTagPrefix, opts.tag].join('.') : opts.tag
|
|
28
|
+
const options = {...defaults(opts, defaultOptions), tag}
|
|
29
|
+
const listenOpts = pick(options, possibleOptions)
|
|
30
|
+
const qs = encodeQueryString({query, params, options: listenOpts, tag})
|
|
31
|
+
|
|
32
|
+
const uri = `${url}${this.getDataUrl('listen', qs)}`
|
|
33
|
+
if (uri.length > MAX_URL_LENGTH) {
|
|
34
|
+
return new Observable((observer) => observer.error(new Error('Query too large for listener')))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const listenFor = options.events ? options.events : ['mutation']
|
|
38
|
+
const shouldEmitReconnect = listenFor.indexOf('reconnect') !== -1
|
|
39
|
+
|
|
40
|
+
const esOptions = {}
|
|
41
|
+
if (token || withCredentials) {
|
|
42
|
+
esOptions.withCredentials = true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (token) {
|
|
46
|
+
esOptions.headers = {
|
|
47
|
+
Authorization: `Bearer ${token}`,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return new Observable((observer) => {
|
|
52
|
+
let es = getEventSource()
|
|
53
|
+
let reconnectTimer
|
|
54
|
+
let stopped = false
|
|
55
|
+
|
|
56
|
+
function onError() {
|
|
57
|
+
if (stopped) {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
emitReconnect()
|
|
62
|
+
|
|
63
|
+
// Allow event handlers of `emitReconnect` to cancel/close the reconnect attempt
|
|
64
|
+
if (stopped) {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Unless we've explicitly stopped the ES (in which case `stopped` should be true),
|
|
69
|
+
// we should never be in a disconnected state. By default, EventSource will reconnect
|
|
70
|
+
// automatically, in which case it sets readyState to `CONNECTING`, but in some cases
|
|
71
|
+
// (like when a laptop lid is closed), it closes the connection. In these cases we need
|
|
72
|
+
// to explicitly reconnect.
|
|
73
|
+
if (es.readyState === EventSource.CLOSED) {
|
|
74
|
+
unsubscribe()
|
|
75
|
+
clearTimeout(reconnectTimer)
|
|
76
|
+
reconnectTimer = setTimeout(open, 100)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function onChannelError(err) {
|
|
81
|
+
observer.error(cooerceError(err))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function onMessage(evt) {
|
|
85
|
+
const event = parseEvent(evt)
|
|
86
|
+
return event instanceof Error ? observer.error(event) : observer.next(event)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function onDisconnect(evt) {
|
|
90
|
+
stopped = true
|
|
91
|
+
unsubscribe()
|
|
92
|
+
observer.complete()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function unsubscribe() {
|
|
96
|
+
es.removeEventListener('error', onError, false)
|
|
97
|
+
es.removeEventListener('channelError', onChannelError, false)
|
|
98
|
+
es.removeEventListener('disconnect', onDisconnect, false)
|
|
99
|
+
listenFor.forEach((type) => es.removeEventListener(type, onMessage, false))
|
|
100
|
+
es.close()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function emitReconnect() {
|
|
104
|
+
if (shouldEmitReconnect) {
|
|
105
|
+
observer.next({type: 'reconnect'})
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getEventSource() {
|
|
110
|
+
const evs = new EventSource(uri, esOptions)
|
|
111
|
+
evs.addEventListener('error', onError, false)
|
|
112
|
+
evs.addEventListener('channelError', onChannelError, false)
|
|
113
|
+
evs.addEventListener('disconnect', onDisconnect, false)
|
|
114
|
+
listenFor.forEach((type) => evs.addEventListener(type, onMessage, false))
|
|
115
|
+
return evs
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function open() {
|
|
119
|
+
es = getEventSource()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function stop() {
|
|
123
|
+
stopped = true
|
|
124
|
+
unsubscribe()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return stop
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseEvent(event) {
|
|
132
|
+
try {
|
|
133
|
+
const data = (event.data && JSON.parse(event.data)) || {}
|
|
134
|
+
return Object.assign({type: event.type}, data)
|
|
135
|
+
} catch (err) {
|
|
136
|
+
return err
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function cooerceError(err) {
|
|
141
|
+
if (err instanceof Error) {
|
|
142
|
+
return err
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const evt = parseEvent(err)
|
|
146
|
+
return evt instanceof Error ? evt : new Error(extractErrorMessage(evt))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function extractErrorMessage(err) {
|
|
150
|
+
if (!err.error) {
|
|
151
|
+
return err.message || 'Unknown listener error'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (err.error.description) {
|
|
155
|
+
return err.error.description
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return typeof err.error === 'string' ? err.error : JSON.stringify(err.error, null, 2)
|
|
159
|
+
}
|