@sanity/export 3.37.4 → 3.38.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/export",
3
- "version": "3.37.4",
3
+ "version": "3.38.1",
4
4
  "description": "Export Sanity documents and assets",
5
5
  "keywords": [
6
6
  "sanity",
@@ -27,9 +27,10 @@
27
27
  ],
28
28
  "scripts": {
29
29
  "lint": "eslint .",
30
- "test": "jest"
30
+ "test": "jest --verbose"
31
31
  },
32
32
  "dependencies": {
33
+ "@sanity/client": "^6.15.20",
33
34
  "@sanity/util": "3.37.2",
34
35
  "archiver": "^7.0.0",
35
36
  "debug": "^4.3.4",
@@ -38,7 +39,9 @@
38
39
  "mississippi": "^4.0.0",
39
40
  "p-queue": "^2.3.0",
40
41
  "rimraf": "^3.0.2",
41
- "split2": "^4.2.0"
42
+ "split2": "^4.2.0",
43
+ "tar": "^7.0.1",
44
+ "yaml": "^2.4.2"
42
45
  },
43
46
  "devDependencies": {
44
47
  "@jest/globals": "^29.7.0",
@@ -48,9 +51,11 @@
48
51
  "eslint-config-sanity": "^7.1.2",
49
52
  "eslint-plugin-prettier": "^5.1.3",
50
53
  "jest": "^29.7.0",
54
+ "nock": "^13.5.4",
51
55
  "prettier": "^3.2.5",
52
56
  "prettier-plugin-packagejson": "^2.5.0",
53
- "string-to-stream": "^1.1.0"
57
+ "string-to-stream": "^1.1.0",
58
+ "tar": "^7.0.1"
54
59
  },
55
60
  "engines": {
56
61
  "node": ">=18"
@@ -9,29 +9,11 @@ const pkg = require('../package.json')
9
9
  const debug = require('./debug')
10
10
  const requestStream = require('./requestStream')
11
11
  const rimraf = require('./util/rimraf')
12
+ const {ASSET_DOWNLOAD_MAX_RETRIES, ASSET_DOWNLOAD_CONCURRENCY} = require('./constants')
12
13
 
13
14
  const EXCLUDE_PROPS = ['_id', '_type', 'assetId', 'extension', 'mimeType', 'path', 'url']
14
15
  const ACTION_REMOVE = 'remove'
15
16
  const ACTION_REWRITE = 'rewrite'
16
- const ASSET_DOWNLOAD_CONCURRENCY = 8
17
-
18
- const retryHelper = (times, fn, onError) => {
19
- let attempt = 0
20
- const caller = (...args) => {
21
- return fn(...args).catch((err) => {
22
- if (onError) {
23
- onError(err, attempt)
24
- }
25
- if (attempt < times) {
26
- attempt++
27
- return caller(...args)
28
- }
29
-
30
- throw err
31
- })
32
- }
33
- return caller
34
- }
35
17
 
36
18
  class AssetHandler {
37
19
  constructor(options) {
@@ -47,6 +29,7 @@ class AssetHandler {
47
29
  this.assetMap = {}
48
30
  this.filesWritten = 0
49
31
  this.queueSize = 0
32
+ this.maxRetries = options.maxRetries || ASSET_DOWNLOAD_MAX_RETRIES
50
33
  this.queue = options.queue || new PQueue({concurrency})
51
34
 
52
35
  this.rejectedError = null
@@ -123,26 +106,42 @@ class AssetHandler {
123
106
  this.queueSize++
124
107
  this.downloading.push(assetDoc.url)
125
108
 
126
- const doDownload = retryHelper(
127
- 10, // try 10 times
128
- () => this.downloadAsset(assetDoc, dstPath),
129
- (err, attempt) => {
130
- debug(
131
- `Error downloading asset %s (destination: %s), attempt %d`,
132
- assetDoc._id,
133
- dstPath,
134
- attempt,
135
- err,
136
- )
137
- },
138
- )
139
- this.queue.add(() =>
140
- doDownload().catch((err) => {
141
- debug('Error downloading asset', err)
142
- this.queue.clear()
143
- this.reject(err)
144
- }),
145
- )
109
+ const doDownload = async () => {
110
+ let dlError
111
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
112
+ try {
113
+ return await this.downloadAsset(assetDoc, dstPath)
114
+ } catch (err) {
115
+ debug(
116
+ `Error downloading asset %s (destination: %s), attempt %d`,
117
+ assetDoc._id,
118
+ dstPath,
119
+ attempt,
120
+ err,
121
+ )
122
+
123
+ dlError = err
124
+
125
+ if ('statusCode' in err && err.statusCode >= 400 && err.statusCode < 500) {
126
+ // Don't retry on client errors
127
+ break
128
+ }
129
+ }
130
+ }
131
+ throw dlError
132
+ }
133
+
134
+ this.queue
135
+ .add(() =>
136
+ doDownload().catch((err) => {
137
+ debug('Failed to download the asset, aborting download', err)
138
+ this.queue.clear()
139
+ this.reject(err)
140
+ }),
141
+ )
142
+ .catch((error) => {
143
+ debug('Queued task failed', error)
144
+ })
146
145
  }
147
146
 
148
147
  maybeCreateAssetDirs() {
@@ -163,7 +162,13 @@ class AssetHandler {
163
162
  const isImage = assetDoc._type === 'sanity.imageAsset'
164
163
 
165
164
  const url = parseUrl(assetDoc.url, true)
166
- if (isImage && ['cdn.sanity.io', 'cdn.sanity.work'].includes(url.hostname) && token) {
165
+ if (
166
+ isImage &&
167
+ token &&
168
+ (['cdn.sanity.io', 'cdn.sanity.work'].includes(url.hostname) ||
169
+ // used in tests. use a very specific port to avoid conflicts
170
+ url.host === 'localhost:43216')
171
+ ) {
167
172
  headers.Authorization = `Bearer ${token}`
168
173
  url.query = {...(url.query || {}), dlRaw: 'true'}
169
174
  }
@@ -191,15 +196,13 @@ class AssetHandler {
191
196
  }
192
197
 
193
198
  if (stream.statusCode !== 200) {
194
- this.queue.clear()
199
+ let errMsg
195
200
  try {
196
201
  const err = await tryGetErrorFromStream(stream)
197
- let errMsg = `Referenced asset URL "${url}" returned HTTP ${stream.statusCode}`
202
+ errMsg = `Referenced asset URL "${url}" returned HTTP ${stream.statusCode}`
198
203
  if (err) {
199
- errMsg = `${errMsg}:\n\n${err}`
204
+ errMsg = `${errMsg}: ${err}`
200
205
  }
201
-
202
- throw new Error(errMsg)
203
206
  } catch (err) {
204
207
  const message = 'Failed to parse error response from asset stream'
205
208
  if (typeof err.message === 'string') {
@@ -210,6 +213,10 @@ class AssetHandler {
210
213
 
211
214
  throw new Error(message, {cause: err})
212
215
  }
216
+
217
+ const streamError = new Error(errMsg)
218
+ streamError.statusCode = stream.statusCode
219
+ throw streamError
213
220
  }
214
221
 
215
222
  this.maybeCreateAssetDirs()
@@ -385,9 +392,20 @@ function writeHashedStream(filePath, stream) {
385
392
 
386
393
  function tryGetErrorFromStream(stream) {
387
394
  return new Promise((resolve, reject) => {
388
- miss.pipe(stream, miss.concat(parse), (err) => (err ? reject(err) : noop))
395
+ let receivedData = false
396
+
397
+ miss.pipe(stream, miss.concat(parse), (err) => {
398
+ if (err) {
399
+ reject(err)
400
+ } else if (!receivedData) {
401
+ // Resolve with null if no data was received, to let the caller
402
+ // know we couldn't parse the error.
403
+ resolve(null)
404
+ }
405
+ })
389
406
 
390
407
  function parse(body) {
408
+ receivedData = true
391
409
  try {
392
410
  const parsed = JSON.parse(body.toString('utf8'))
393
411
  resolve(parsed.message || parsed.error || null)
@@ -0,0 +1,22 @@
1
+ /**
2
+ * How many retries to attempt when retrieving the document stream.
3
+ * User overridable as `options.maxRetries`.
4
+ *
5
+ * Note: Only for initial connection - if download fails while streaming, we cannot easily resume.
6
+ * @internal
7
+ */
8
+ exports.DOCUMENT_STREAM_MAX_RETRIES = 5
9
+
10
+ /**
11
+ * How many retries to attempt when downloading an asset.
12
+ * User overridable as `options.maxAssetRetries`.
13
+ * @internal
14
+ */
15
+ exports.ASSET_DOWNLOAD_MAX_RETRIES = 10
16
+
17
+ /**
18
+ * How many concurrent asset downloads to allow.
19
+ * User overridable as `options.assetConcurrency`.
20
+ * @internal
21
+ */
22
+ exports.ASSET_DOWNLOAD_CONCURRENCY = 8
package/src/export.js CHANGED
@@ -20,12 +20,16 @@ const validateOptions = require('./validateOptions')
20
20
 
21
21
  const noop = () => null
22
22
 
23
- function exportDataset(opts) {
23
+ async function exportDataset(opts) {
24
24
  const options = validateOptions(opts)
25
25
  const onProgress = options.onProgress || noop
26
26
  const archive = archiver('tar', {
27
27
  gzip: true,
28
- gzipOptions: {level: options.compress ? zlib.Z_DEFAULT_COMPRESSION : zlib.Z_NO_COMPRESSION},
28
+ gzipOptions: {
29
+ level: options.compress
30
+ ? zlib.constants.Z_DEFAULT_COMPRESSION
31
+ : zlib.constants.Z_NO_COMPRESSION,
32
+ },
29
33
  })
30
34
 
31
35
  const slugDate = new Date()
@@ -35,6 +39,9 @@ function exportDataset(opts) {
35
39
 
36
40
  const prefix = `${opts.dataset}-export-${slugDate}`
37
41
  const tmpDir = path.join(os.tmpdir(), prefix)
42
+ fs.mkdirSync(tmpDir, {recursive: true})
43
+ const dataPath = path.join(tmpDir, 'data.ndjson')
44
+
38
45
  const cleanup = () =>
39
46
  rimraf(tmpDir).catch((err) => {
40
47
  debug(`Error while cleaning up temporary files: ${err.message}`)
@@ -45,10 +52,11 @@ function exportDataset(opts) {
45
52
  tmpDir,
46
53
  prefix,
47
54
  concurrency: options.assetConcurrency,
55
+ maxRetries: options.maxAssetRetries,
48
56
  })
49
57
 
50
- debug('Outputting assets (temporarily) to %s', tmpDir)
51
- debug('Outputting to %s', options.outputPath === '-' ? 'stdout' : options.outputPath)
58
+ debug('Downloading assets (temporarily) to %s', tmpDir)
59
+ debug('Downloading to %s', options.outputPath === '-' ? 'stdout' : options.outputPath)
52
60
 
53
61
  let outputStream
54
62
  if (isWritableStream(options.outputPath)) {
@@ -63,144 +71,160 @@ function exportDataset(opts) {
63
71
  assetStreamHandler = options.assets ? assetHandler.rewriteAssets : assetHandler.stripAssets
64
72
  }
65
73
 
66
- // eslint-disable-next-line no-async-promise-executor
67
- return new Promise(async (resolve, reject) => {
68
- miss.finished(archive, async (archiveErr) => {
69
- if (archiveErr) {
70
- debug('Archiving errored! %s', archiveErr.stack)
71
- await cleanup()
72
- reject(archiveErr)
73
- return
74
- }
75
-
76
- debug('Archive finished!')
77
- })
78
-
79
- debug('Getting dataset export stream')
80
- onProgress({step: 'Exporting documents...'})
74
+ let resolve
75
+ let reject
76
+ const result = new Promise((res, rej) => {
77
+ resolve = res
78
+ reject = rej
79
+ })
81
80
 
82
- let documentCount = 0
83
- let lastReported = Date.now()
84
- const reportDocumentCount = (chunk, enc, cb) => {
85
- ++documentCount
81
+ miss.finished(archive, async (archiveErr) => {
82
+ if (archiveErr) {
83
+ debug('Archiving errored: %s', archiveErr.stack)
84
+ await cleanup()
85
+ reject(archiveErr)
86
+ return
87
+ }
86
88
 
87
- const now = Date.now()
88
- if (now - lastReported > 50) {
89
- onProgress({
90
- step: 'Exporting documents...',
91
- current: documentCount,
92
- total: '?',
93
- update: true,
94
- })
89
+ debug('Archive finished')
90
+ })
95
91
 
96
- lastReported = now
97
- }
92
+ debug('Getting dataset export stream')
93
+ onProgress({step: 'Exporting documents...'})
98
94
 
99
- cb(null, chunk)
100
- }
101
-
102
- const inputStream = await getDocumentsStream(options.client, options.dataset)
103
- debug('Got HTTP %d', inputStream.statusCode)
104
- debug('Response headers: %o', inputStream.headers)
105
-
106
- const jsonStream = miss.pipeline(
107
- inputStream,
108
- logFirstChunk(),
109
- split(tryParseJson),
110
- rejectOnApiError(),
111
- filterSystemDocuments(),
112
- assetStreamHandler,
113
- filterDocumentTypes(options.types),
114
- options.drafts ? miss.through.obj() : filterDrafts(),
115
- stringifyStream(),
116
- miss.through(reportDocumentCount),
117
- )
118
-
119
- miss.finished(jsonStream, async (err) => {
120
- if (err) {
121
- return
122
- }
95
+ let documentCount = 0
96
+ let lastReported = Date.now()
97
+ const reportDocumentCount = (chunk, enc, cb) => {
98
+ ++documentCount
123
99
 
100
+ const now = Date.now()
101
+ if (now - lastReported > 50) {
124
102
  onProgress({
125
103
  step: 'Exporting documents...',
126
104
  current: documentCount,
127
- total: documentCount,
105
+ total: '?',
128
106
  update: true,
129
107
  })
130
108
 
131
- if (!options.raw && options.assets) {
132
- onProgress({step: 'Downloading assets...'})
133
- }
109
+ lastReported = now
110
+ }
134
111
 
135
- let prevCompleted = 0
136
- const progressInterval = setInterval(() => {
137
- const completed =
138
- assetHandler.queueSize - assetHandler.queue.size - assetHandler.queue.pending
139
-
140
- if (prevCompleted === completed) {
141
- return
142
- }
143
-
144
- prevCompleted = completed
145
- onProgress({
146
- step: 'Downloading assets...',
147
- current: completed,
148
- total: assetHandler.queueSize,
149
- update: true,
150
- })
151
- }, 500)
152
-
153
- debug('Waiting for asset handler to complete downloads')
154
- try {
155
- const assetMap = await assetHandler.finish()
156
-
157
- // Make sure we mark the progress as done (eg 100/100 instead of 99/100)
158
- onProgress({
159
- step: 'Downloading assets...',
160
- current: assetHandler.queueSize,
161
- total: assetHandler.queueSize,
162
- update: true,
163
- })
164
-
165
- archive.append(JSON.stringify(assetMap), {name: 'assets.json', prefix})
166
- clearInterval(progressInterval)
167
- } catch (assetErr) {
168
- clearInterval(progressInterval)
169
- await cleanup()
170
- reject(assetErr)
171
- return
172
- }
112
+ cb(null, chunk)
113
+ }
173
114
 
174
- // Add all downloaded assets to archive
175
- archive.directory(path.join(tmpDir, 'files'), `${prefix}/files`, {store: true})
176
- archive.directory(path.join(tmpDir, 'images'), `${prefix}/images`, {store: true})
115
+ const inputStream = await getDocumentsStream(options)
116
+ debug('Got HTTP %d', inputStream.statusCode)
117
+ debug('Response headers: %o', inputStream.headers)
118
+
119
+ const jsonStream = miss.pipeline(
120
+ inputStream,
121
+ logFirstChunk(),
122
+ split(tryParseJson),
123
+ rejectOnApiError(),
124
+ filterSystemDocuments(),
125
+ assetStreamHandler,
126
+ filterDocumentTypes(options.types),
127
+ options.drafts ? miss.through.obj() : filterDrafts(),
128
+ stringifyStream(),
129
+ miss.through(reportDocumentCount),
130
+ )
177
131
 
178
- debug('Finalizing archive, flushing streams')
179
- onProgress({step: 'Adding assets to archive...'})
180
- archive.finalize()
181
- })
132
+ miss.pipe(jsonStream, fs.createWriteStream(dataPath), async (err) => {
133
+ if (err) {
134
+ debug('Export stream error: ', err)
135
+ reject(err)
136
+ return
137
+ }
182
138
 
183
- archive.on('warning', (err) => {
184
- debug('Archive warning: %s', err.message)
139
+ debug('Export stream completed')
140
+ onProgress({
141
+ step: 'Exporting documents...',
142
+ current: documentCount,
143
+ total: documentCount,
144
+ update: true,
185
145
  })
186
146
 
187
- archive.append(jsonStream, {name: 'data.ndjson', prefix})
188
- miss.pipe(archive, outputStream, onComplete)
147
+ debug('Adding data.ndjson to archive')
148
+ archive.file(dataPath, {name: 'data.ndjson', prefix})
189
149
 
190
- async function onComplete(err) {
191
- onProgress({step: 'Clearing temporary files...'})
192
- await cleanup()
150
+ if (!options.raw && options.assets) {
151
+ onProgress({step: 'Downloading assets...'})
152
+ }
153
+
154
+ let prevCompleted = 0
155
+ const progressInterval = setInterval(() => {
156
+ const completed =
157
+ assetHandler.queueSize - assetHandler.queue.size - assetHandler.queue.pending
193
158
 
194
- if (!err) {
195
- resolve()
159
+ if (prevCompleted === completed) {
196
160
  return
197
161
  }
198
162
 
199
- debug('Error during streaming: %s', err.stack)
200
- assetHandler.clear()
201
- reject(err)
163
+ prevCompleted = completed
164
+ onProgress({
165
+ step: 'Downloading assets...',
166
+ current: completed,
167
+ total: assetHandler.queueSize,
168
+ update: true,
169
+ })
170
+ }, 500)
171
+
172
+ debug('Waiting for asset handler to complete downloads')
173
+ try {
174
+ const assetMap = await assetHandler.finish()
175
+
176
+ // Make sure we mark the progress as done (eg 100/100 instead of 99/100)
177
+ onProgress({
178
+ step: 'Downloading assets...',
179
+ current: assetHandler.queueSize,
180
+ total: assetHandler.queueSize,
181
+ update: true,
182
+ })
183
+
184
+ archive.append(JSON.stringify(assetMap), {name: 'assets.json', prefix})
185
+ clearInterval(progressInterval)
186
+ } catch (assetErr) {
187
+ clearInterval(progressInterval)
188
+ await cleanup()
189
+ reject(assetErr)
190
+ return
202
191
  }
192
+
193
+ // Add all downloaded assets to archive
194
+ archive.directory(path.join(tmpDir, 'files'), `${prefix}/files`, {store: true})
195
+ archive.directory(path.join(tmpDir, 'images'), `${prefix}/images`, {store: true})
196
+
197
+ debug('Finalizing archive, flushing streams')
198
+ onProgress({step: 'Adding assets to archive...'})
199
+ await archive.finalize()
200
+ })
201
+
202
+ archive.on('warning', (err) => {
203
+ debug('Archive warning: %s', err.message)
203
204
  })
205
+
206
+ miss.pipe(archive, outputStream, onComplete)
207
+
208
+ async function onComplete(err) {
209
+ onProgress({step: 'Clearing temporary files...'})
210
+ await cleanup()
211
+
212
+ if (!err) {
213
+ debug('Export completed')
214
+ resolve({
215
+ outputPath: options.outputPath,
216
+ documentCount,
217
+ assetCount: assetHandler.filesWritten,
218
+ })
219
+ return
220
+ }
221
+
222
+ debug('Error during streaming: %s', err.stack)
223
+ assetHandler.clear()
224
+ reject(err)
225
+ }
226
+
227
+ return result
204
228
  }
205
229
 
206
230
  function isWritableStream(val) {
@@ -1,15 +1,15 @@
1
1
  const pkg = require('../package.json')
2
2
  const requestStream = require('./requestStream')
3
3
 
4
- module.exports = (client, dataset) => {
4
+ module.exports = (options) => {
5
5
  // Sanity client doesn't handle streams natively since we want to support node/browser
6
6
  // with same API. We're just using it here to get hold of URLs and tokens.
7
- const url = client.getUrl(`/data/export/${dataset}`)
8
- const token = client.config().token
7
+ const url = options.client.getUrl(`/data/export/${options.dataset}`)
8
+ const token = options.client.config().token
9
9
  const headers = {
10
10
  'User-Agent': `${pkg.name}@${pkg.version}`,
11
11
  ...(token ? {Authorization: `Bearer ${token}`} : {}),
12
12
  }
13
13
 
14
- return requestStream({url, headers})
14
+ return requestStream({url, headers, maxRetries: options.maxRetries})
15
15
  }
@@ -3,7 +3,13 @@ const miss = require('mississippi')
3
3
  module.exports = () =>
4
4
  miss.through.obj((doc, enc, callback) => {
5
5
  if (doc.error && doc.statusCode) {
6
- callback(new Error([doc.statusCode, doc.error].join(': ')))
6
+ callback(
7
+ new Error(
8
+ ['Export', `HTTP ${doc.statusCode}`, doc.error, doc.message]
9
+ .filter((part) => typeof part === 'string')
10
+ .join(': '),
11
+ ),
12
+ )
7
13
  return
8
14
  }
9
15
 
@@ -1,13 +1,15 @@
1
1
  const {getIt} = require('get-it')
2
2
  const {keepAlive, promise} = require('get-it/middleware')
3
3
  const debug = require('./debug')
4
+ const {extractFirstError} = require('./util/extractFirstError')
5
+ const {DOCUMENT_STREAM_MAX_RETRIES} = require('./constants')
4
6
 
5
7
  const request = getIt([keepAlive(), promise({onlyBody: true})])
6
8
  const socketsWithTimeout = new WeakSet()
7
9
 
8
10
  const CONNECTION_TIMEOUT = 15 * 1000 // 15 seconds
9
11
  const READ_TIMEOUT = 3 * 60 * 1000 // 3 minutes
10
- const MAX_RETRIES = 5
12
+ const RETRY_DELAY_MS = 1500 // 1.5 seconds
11
13
 
12
14
  function delay(ms) {
13
15
  return new Promise((resolve) => setTimeout(resolve, ms))
@@ -15,8 +17,11 @@ function delay(ms) {
15
17
 
16
18
  /* eslint-disable no-await-in-loop, max-depth */
17
19
  module.exports = async (options) => {
20
+ const maxRetries =
21
+ typeof options.maxRetries === 'number' ? options.maxRetries : DOCUMENT_STREAM_MAX_RETRIES
22
+
18
23
  let error
19
- for (let i = 0; i < MAX_RETRIES; i++) {
24
+ for (let i = 0; i < maxRetries; i++) {
20
25
  try {
21
26
  const response = await request({
22
27
  ...options,
@@ -33,23 +38,24 @@ module.exports = async (options) => {
33
38
  socketsWithTimeout.add(response.connection)
34
39
  response.connection.setTimeout(READ_TIMEOUT, () => {
35
40
  response.destroy(
36
- new Error(`Read timeout: No data received on socket for ${READ_TIMEOUT} ms`),
41
+ new Error(`Export: Read timeout: No data received on socket for ${READ_TIMEOUT} ms`),
37
42
  )
38
43
  })
39
44
  }
40
45
 
41
46
  return response
42
47
  } catch (err) {
43
- error = err
48
+ error = extractFirstError(err)
44
49
 
45
50
  if (err.response && err.response.statusCode && err.response.statusCode < 500) {
46
51
  break
47
52
  }
48
53
 
49
- debug('Error, retrying after 1500ms: %s', err.message)
50
- await delay(1500)
54
+ debug('Error, retrying after %d ms: %s', RETRY_DELAY_MS, error.message)
55
+ await delay(RETRY_DELAY_MS)
51
56
  }
52
57
  }
53
58
 
59
+ error.message = `Export: Failed to fetch ${options.url}: ${error.message}`
54
60
  throw error
55
61
  }
@@ -0,0 +1,14 @@
1
+ exports.extractFirstError = function extractFirstError(err) {
2
+ if (
3
+ // eslint-disable-next-line no-undef
4
+ ((typeof AggregateError !== 'undefined' && err instanceof AggregateError) ||
5
+ ('name' in err && err.name === 'AggregateError')) &&
6
+ Array.isArray(err.errors) &&
7
+ err.errors.length > 0 &&
8
+ 'message' in err.errors[0]
9
+ ) {
10
+ return err.errors[0]
11
+ }
12
+
13
+ return err
14
+ }
@@ -1,12 +1,16 @@
1
1
  const defaults = require('lodash/defaults')
2
+ const {DOCUMENT_STREAM_MAX_RETRIES, ASSET_DOWNLOAD_MAX_RETRIES} = require('./constants')
2
3
 
3
4
  const clientMethods = ['getUrl', 'config']
4
5
  const booleanFlags = ['assets', 'raw', 'compress', 'drafts']
6
+ const numberFlags = ['maxAssetRetries', 'maxRetries', 'assetConcurrency']
5
7
  const exportDefaults = {
6
8
  compress: true,
7
9
  drafts: true,
8
10
  assets: true,
9
11
  raw: false,
12
+ maxRetries: DOCUMENT_STREAM_MAX_RETRIES,
13
+ maxAssetRetries: ASSET_DOWNLOAD_MAX_RETRIES,
10
14
  }
11
15
 
12
16
  function validateOptions(opts) {
@@ -42,6 +46,12 @@ function validateOptions(opts) {
42
46
  }
43
47
  })
44
48
 
49
+ numberFlags.forEach((flag) => {
50
+ if (typeof options[flag] !== 'undefined' && typeof options[flag] !== 'number') {
51
+ throw new Error(`Flag ${flag} must be a number if specified`)
52
+ }
53
+ })
54
+
45
55
  if (!options.outputPath) {
46
56
  throw new Error('outputPath must be specified (- for stdout)')
47
57
  }