@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.
@@ -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.1";
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.1",
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
+ }