@sanity/export 3.39.0 → 3.40.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/export",
3
- "version": "3.39.0",
3
+ "version": "3.40.0",
4
4
  "description": "Export Sanity documents and assets",
5
5
  "keywords": [
6
6
  "sanity",
package/src/constants.js CHANGED
@@ -33,3 +33,11 @@ exports.DOCUMENT_STREAM_DEBUG_INTERVAL = 10000
33
33
  * @internal
34
34
  */
35
35
  exports.REQUEST_READ_TIMEOUT = 3 * 60 * 1000 // 3 minutes
36
+
37
+ /**
38
+ What mode to use when exporting documents.
39
+ stream: Export all documents in the dataset in one request, this will be consistent but might be slow on large datasets.
40
+ cursor: Export documents using a cursor, this might lead to inconsistent results if a mutation is performed while exporting.
41
+ */
42
+ exports.MODE_STREAM = 'stream'
43
+ exports.MODE_CURSOR = 'cursor'
package/src/export.js CHANGED
@@ -11,13 +11,14 @@ const filterDocumentTypes = require('./filterDocumentTypes')
11
11
  const filterDrafts = require('./filterDrafts')
12
12
  const filterSystemDocuments = require('./filterSystemDocuments')
13
13
  const getDocumentsStream = require('./getDocumentsStream')
14
+ const getDocumentCursorStream = require('./getDocumentCursorStream')
14
15
  const logFirstChunk = require('./logFirstChunk')
15
16
  const rejectOnApiError = require('./rejectOnApiError')
16
17
  const stringifyStream = require('./stringifyStream')
17
18
  const tryParseJson = require('./tryParseJson')
18
19
  const rimraf = require('./util/rimraf')
19
20
  const validateOptions = require('./validateOptions')
20
- const {DOCUMENT_STREAM_DEBUG_INTERVAL} = require('./constants')
21
+ const {DOCUMENT_STREAM_DEBUG_INTERVAL, MODE_CURSOR, MODE_STREAM} = require('./constants')
21
22
 
22
23
  const noop = () => null
23
24
 
@@ -118,7 +119,7 @@ async function exportDataset(opts) {
118
119
  cb(null, doc)
119
120
  }
120
121
 
121
- const inputStream = await getDocumentsStream(options)
122
+ const inputStream = await getDocumentInputStream(options)
122
123
  debug('Got HTTP %d', inputStream.statusCode)
123
124
  debug('Response headers: %o', inputStream.headers)
124
125
 
@@ -250,6 +251,17 @@ async function exportDataset(opts) {
250
251
  return result
251
252
  }
252
253
 
254
+ function getDocumentInputStream(options) {
255
+ if (options.mode === MODE_STREAM) {
256
+ return getDocumentsStream(options)
257
+ }
258
+ if (options.mode === MODE_CURSOR) {
259
+ return getDocumentCursorStream(options)
260
+ }
261
+
262
+ throw new Error(`Invalid mode: ${options.mode}`)
263
+ }
264
+
253
265
  function isWritableStream(val) {
254
266
  return (
255
267
  val !== null &&
@@ -0,0 +1,73 @@
1
+ const {Transform} = require('node:stream')
2
+
3
+ const pkg = require('../package.json')
4
+ const requestStream = require('./requestStream')
5
+
6
+ module.exports = async (options) => {
7
+ let streamsInflight = 0
8
+ const stream = new Transform({
9
+ async transform(chunk, encoding, callback) {
10
+ if (encoding !== 'buffer' && encoding !== 'string') {
11
+ callback(null, chunk)
12
+ return
13
+ }
14
+
15
+ let parsedChunk = null
16
+ try {
17
+ parsedChunk = JSON.parse(chunk.toString())
18
+ } catch (err) {
19
+ // Ignore JSON parse errors
20
+ // this can happen if the chunk is not a JSON object. We just pass it through and let the caller handle it.
21
+ }
22
+
23
+ if (
24
+ parsedChunk !== null &&
25
+ typeof parsedChunk === 'object' &&
26
+ 'nextCursor' in parsedChunk &&
27
+ typeof parsedChunk.nextCursor === 'string' &&
28
+ !('_id' in parsedChunk)
29
+ ) {
30
+ streamsInflight++
31
+
32
+ const reqStream = await startStream(options, parsedChunk.nextCursor)
33
+ reqStream.on('end', () => {
34
+ streamsInflight--
35
+ if (streamsInflight === 0) {
36
+ stream.end()
37
+ }
38
+ })
39
+ reqStream.pipe(this, {end: false})
40
+
41
+ callback()
42
+ return
43
+ }
44
+
45
+ callback(null, chunk)
46
+ },
47
+ })
48
+
49
+ streamsInflight++
50
+ const reqStream = await startStream(options, '')
51
+ reqStream.on('end', () => {
52
+ streamsInflight--
53
+ if (streamsInflight === 0) {
54
+ stream.end()
55
+ }
56
+ })
57
+
58
+ reqStream.pipe(stream, {end: false})
59
+ return stream
60
+ }
61
+
62
+ function startStream(options, nextCursor) {
63
+ const url = options.client.getUrl(
64
+ `/data/export/${options.dataset}?nextCursor=${encodeURIComponent(nextCursor)}`,
65
+ )
66
+ const token = options.client.config().token
67
+ const headers = {
68
+ 'User-Agent': `${pkg.name}@${pkg.version}`,
69
+ ...(token ? {Authorization: `Bearer ${token}`} : {}),
70
+ }
71
+
72
+ return requestStream({url, headers, maxRetries: options.maxRetries})
73
+ }
@@ -3,6 +3,8 @@ const {
3
3
  DOCUMENT_STREAM_MAX_RETRIES,
4
4
  ASSET_DOWNLOAD_MAX_RETRIES,
5
5
  REQUEST_READ_TIMEOUT,
6
+ MODE_STREAM,
7
+ MODE_CURSOR,
6
8
  } = require('./constants')
7
9
 
8
10
  const clientMethods = ['getUrl', 'config']
@@ -13,6 +15,7 @@ const exportDefaults = {
13
15
  drafts: true,
14
16
  assets: true,
15
17
  raw: false,
18
+ mode: MODE_STREAM,
16
19
  maxRetries: DOCUMENT_STREAM_MAX_RETRIES,
17
20
  maxAssetRetries: ASSET_DOWNLOAD_MAX_RETRIES,
18
21
  readTimeout: REQUEST_READ_TIMEOUT,
@@ -25,6 +28,15 @@ function validateOptions(opts) {
25
28
  throw new Error(`options.dataset must be a valid dataset name`)
26
29
  }
27
30
 
31
+ if (
32
+ typeof options.mode !== 'string' ||
33
+ (options.mode !== MODE_STREAM && options.mode !== MODE_CURSOR)
34
+ ) {
35
+ throw new Error(
36
+ `options.mode must be either "${MODE_STREAM}" or "${MODE_CURSOR}", got "${options.mode}"`,
37
+ )
38
+ }
39
+
28
40
  if (options.onProgress && typeof options.onProgress !== 'function') {
29
41
  throw new Error(`options.onProgress must be a function`)
30
42
  }