@sanity/export 5.0.0 → 6.0.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 (115) hide show
  1. package/dist/AssetHandler.d.ts +47 -0
  2. package/dist/AssetHandler.d.ts.map +1 -0
  3. package/dist/AssetHandler.js +384 -0
  4. package/dist/AssetHandler.js.map +1 -0
  5. package/dist/constants.d.ts +45 -0
  6. package/dist/constants.d.ts.map +1 -0
  7. package/{src → dist}/constants.js +13 -18
  8. package/dist/constants.js.map +1 -0
  9. package/dist/debug.d.ts +3 -0
  10. package/dist/debug.d.ts.map +1 -0
  11. package/dist/debug.js +3 -0
  12. package/dist/debug.js.map +1 -0
  13. package/dist/export.d.ts +43 -0
  14. package/dist/export.d.ts.map +1 -0
  15. package/dist/export.js +269 -0
  16. package/dist/export.js.map +1 -0
  17. package/dist/filterDocumentTypes.d.ts +3 -0
  18. package/dist/filterDocumentTypes.d.ts.map +1 -0
  19. package/dist/filterDocumentTypes.js +16 -0
  20. package/dist/filterDocumentTypes.js.map +1 -0
  21. package/dist/filterDocuments.d.ts +3 -0
  22. package/dist/filterDocuments.d.ts.map +1 -0
  23. package/dist/filterDocuments.js +36 -0
  24. package/dist/filterDocuments.js.map +1 -0
  25. package/dist/getDocumentCursorStream.d.ts +4 -0
  26. package/dist/getDocumentCursorStream.d.ts.map +1 -0
  27. package/dist/getDocumentCursorStream.js +85 -0
  28. package/dist/getDocumentCursorStream.js.map +1 -0
  29. package/dist/getDocumentsStream.d.ts +5 -0
  30. package/dist/getDocumentsStream.d.ts.map +1 -0
  31. package/dist/getDocumentsStream.js +28 -0
  32. package/dist/getDocumentsStream.js.map +1 -0
  33. package/dist/getUserAgent.d.ts +2 -0
  34. package/dist/getUserAgent.d.ts.map +1 -0
  35. package/dist/getUserAgent.js +12 -0
  36. package/dist/getUserAgent.js.map +1 -0
  37. package/dist/index.d.ts +3 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +3 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/logFirstChunk.d.ts +3 -0
  42. package/dist/logFirstChunk.d.ts.map +1 -0
  43. package/dist/logFirstChunk.js +14 -0
  44. package/dist/logFirstChunk.js.map +1 -0
  45. package/dist/options.d.ts +14 -0
  46. package/dist/options.d.ts.map +1 -0
  47. package/dist/options.js +97 -0
  48. package/dist/options.js.map +1 -0
  49. package/dist/rejectOnApiError.d.ts +3 -0
  50. package/dist/rejectOnApiError.d.ts.map +1 -0
  51. package/dist/rejectOnApiError.js +35 -0
  52. package/dist/rejectOnApiError.js.map +1 -0
  53. package/dist/requestStream.d.ts +3 -0
  54. package/dist/requestStream.d.ts.map +1 -0
  55. package/dist/requestStream.js +48 -0
  56. package/dist/requestStream.js.map +1 -0
  57. package/dist/stringifyStream.d.ts +3 -0
  58. package/dist/stringifyStream.d.ts.map +1 -0
  59. package/dist/stringifyStream.js +5 -0
  60. package/dist/stringifyStream.js.map +1 -0
  61. package/dist/tryParseJson.d.ts +10 -0
  62. package/dist/tryParseJson.d.ts.map +1 -0
  63. package/dist/tryParseJson.js +36 -0
  64. package/dist/tryParseJson.js.map +1 -0
  65. package/dist/types.d.ts +241 -0
  66. package/dist/types.d.ts.map +1 -0
  67. package/dist/types.js +2 -0
  68. package/dist/types.js.map +1 -0
  69. package/dist/util/delay.d.ts +2 -0
  70. package/dist/util/delay.d.ts.map +1 -0
  71. package/dist/util/delay.js +4 -0
  72. package/dist/util/delay.js.map +1 -0
  73. package/dist/util/extractFirstError.d.ts +2 -0
  74. package/dist/util/extractFirstError.d.ts.map +1 -0
  75. package/dist/util/extractFirstError.js +22 -0
  76. package/dist/util/extractFirstError.js.map +1 -0
  77. package/dist/util/friendlyError.d.ts +2 -0
  78. package/dist/util/friendlyError.d.ts.map +1 -0
  79. package/dist/util/friendlyError.js +49 -0
  80. package/dist/util/friendlyError.js.map +1 -0
  81. package/dist/util/streamHelpers.d.ts +10 -0
  82. package/dist/util/streamHelpers.d.ts.map +1 -0
  83. package/dist/util/streamHelpers.js +99 -0
  84. package/dist/util/streamHelpers.js.map +1 -0
  85. package/package.json +22 -7
  86. package/src/{AssetHandler.js → AssetHandler.ts} +174 -99
  87. package/src/constants.ts +50 -0
  88. package/src/debug.ts +3 -0
  89. package/src/{export.js → export.ts} +127 -71
  90. package/src/filterDocumentTypes.ts +21 -0
  91. package/src/filterDocuments.ts +55 -0
  92. package/src/{getDocumentCursorStream.js → getDocumentCursorStream.ts} +37 -18
  93. package/src/{getDocumentsStream.js → getDocumentsStream.ts} +16 -6
  94. package/src/{getUserAgent.js → getUserAgent.ts} +8 -3
  95. package/src/index.ts +11 -0
  96. package/src/{logFirstChunk.js → logFirstChunk.ts} +6 -4
  97. package/src/options.ts +138 -0
  98. package/src/rejectOnApiError.ts +62 -0
  99. package/src/requestStream.ts +81 -0
  100. package/src/stringifyStream.ts +7 -0
  101. package/src/{tryParseJson.js → tryParseJson.ts} +29 -17
  102. package/src/types.ts +274 -0
  103. package/src/util/{delay.js → delay.ts} +1 -1
  104. package/src/util/extractFirstError.ts +31 -0
  105. package/src/util/friendlyError.ts +75 -0
  106. package/src/util/{streamHelpers.js → streamHelpers.ts} +35 -18
  107. package/src/debug.js +0 -3
  108. package/src/filterDocumentTypes.js +0 -18
  109. package/src/filterDocuments.js +0 -33
  110. package/src/rejectOnApiError.js +0 -31
  111. package/src/requestStream.js +0 -64
  112. package/src/stringifyStream.js +0 -5
  113. package/src/util/extractFirstError.js +0 -14
  114. package/src/util/friendlyError.js +0 -58
  115. package/src/validateOptions.js +0 -113
@@ -4,8 +4,9 @@ import {join as joinPath} from 'node:path'
4
4
  import {pipeline} from 'node:stream/promises'
5
5
 
6
6
  import PQueue from 'p-queue'
7
- import {rimraf} from 'rimraf'
8
7
 
8
+ import {delay} from './util/delay.js'
9
+ import {through, throughObj} from './util/streamHelpers.js'
9
10
  import {
10
11
  ASSET_DOWNLOAD_CONCURRENCY,
11
12
  ASSET_DOWNLOAD_MAX_RETRIES,
@@ -14,16 +15,70 @@ import {
14
15
  import {debug} from './debug.js'
15
16
  import {getUserAgent} from './getUserAgent.js'
16
17
  import {requestStream} from './requestStream.js'
17
- import {delay} from './util/delay.js'
18
- import {through, throughObj} from './util/streamHelpers.js'
18
+ import type {
19
+ AssetDocument,
20
+ AssetMap,
21
+ AssetMetadata,
22
+ ResponseStream,
23
+ SanityClientLike,
24
+ SanityDocument,
25
+ } from './types.js'
26
+ import {rm} from 'node:fs/promises'
19
27
 
20
28
  const EXCLUDE_PROPS = ['_id', '_type', 'assetId', 'extension', 'mimeType', 'path', 'url']
21
- const ACTION_REMOVE = 'remove'
22
- const ACTION_REWRITE = 'rewrite'
29
+ const ACTION_REMOVE = 'remove' as const
30
+ const ACTION_REWRITE = 'rewrite' as const
31
+
32
+ type AssetAction = typeof ACTION_REMOVE | typeof ACTION_REWRITE
33
+
34
+ interface AssetHandlerOptions {
35
+ client: SanityClientLike
36
+ tmpDir: string
37
+ prefix?: string
38
+ concurrency?: number
39
+ maxRetries?: number
40
+ retryDelayMs?: number
41
+ queue?: PQueue
42
+ }
43
+
44
+ interface AssetRequestOptions {
45
+ url: string
46
+ headers: Record<string, string>
47
+ }
48
+
49
+ interface AssetField {
50
+ asset: {
51
+ _ref: string
52
+ }
53
+ [key: string]: unknown
54
+ }
55
+
56
+ interface RewrittenAssetField {
57
+ _sanityAsset: string
58
+ [key: string]: unknown
59
+ }
60
+
61
+ interface DownloadError extends Error {
62
+ statusCode?: number
63
+ }
23
64
 
24
65
  export class AssetHandler {
25
- constructor(options) {
26
- const concurrency = options.concurrency || ASSET_DOWNLOAD_CONCURRENCY
66
+ client: SanityClientLike
67
+ tmpDir: string
68
+ assetDirsCreated: boolean
69
+ downloading: string[]
70
+ assetsSeen: Map<string, string>
71
+ assetMap: AssetMap
72
+ filesWritten: number
73
+ queueSize: number
74
+ maxRetries: number
75
+ retryDelayMs: number | undefined
76
+ queue: PQueue
77
+ rejectedError: Error | null
78
+ reject: (err: Error) => void
79
+
80
+ constructor(options: AssetHandlerOptions) {
81
+ const concurrency = options.concurrency ?? ASSET_DOWNLOAD_CONCURRENCY
27
82
  debug('Using asset download concurrency of %d', concurrency)
28
83
 
29
84
  this.client = options.client
@@ -35,23 +90,23 @@ export class AssetHandler {
35
90
  this.assetMap = {}
36
91
  this.filesWritten = 0
37
92
  this.queueSize = 0
38
- this.maxRetries = options.maxRetries || ASSET_DOWNLOAD_MAX_RETRIES
93
+ this.maxRetries = options.maxRetries ?? ASSET_DOWNLOAD_MAX_RETRIES
39
94
  this.retryDelayMs = options.retryDelayMs
40
- this.queue = options.queue || new PQueue({concurrency})
95
+ this.queue = options.queue ?? new PQueue({concurrency})
41
96
 
42
97
  this.rejectedError = null
43
- this.reject = (err) => {
98
+ this.reject = (err: Error): void => {
44
99
  this.rejectedError = err
45
100
  }
46
101
  }
47
102
 
48
- clear() {
103
+ clear(): void {
49
104
  this.assetsSeen.clear()
50
105
  this.queue.clear()
51
106
  this.queueSize = 0
52
107
  }
53
108
 
54
- finish() {
109
+ finish(): Promise<AssetMap> {
55
110
  return new Promise((resolve, reject) => {
56
111
  if (this.rejectedError) {
57
112
  reject(this.rejectedError)
@@ -59,28 +114,31 @@ export class AssetHandler {
59
114
  }
60
115
 
61
116
  this.reject = reject
62
- this.queue.onIdle().then(() => resolve(this.assetMap))
117
+ void this.queue.onIdle().then(() => resolve(this.assetMap))
63
118
  })
64
119
  }
65
120
 
66
121
  // Called when we want to download all assets to local filesystem and rewrite documents to hold
67
122
  // placeholder asset references (_sanityAsset: 'image@file:///local/path')
68
- rewriteAssets = throughObj(async (doc, enc, callback) => {
69
- if (['sanity.imageAsset', 'sanity.fileAsset'].includes(doc._type)) {
70
- const type = doc._type === 'sanity.imageAsset' ? 'image' : 'file'
71
- const filePath = `${type}s/${generateFilename(doc._id)}`
72
- this.assetsSeen.set(doc._id, type)
73
- this.queueAssetDownload(doc, filePath, type)
74
- callback()
75
- return
76
- }
123
+ rewriteAssets = throughObj(
124
+ (doc: SanityDocument | AssetDocument, _enc: BufferEncoding, callback) => {
125
+ if (['sanity.imageAsset', 'sanity.fileAsset'].includes(doc._type)) {
126
+ const assetDoc = doc as AssetDocument
127
+ const type = doc._type === 'sanity.imageAsset' ? 'image' : 'file'
128
+ const filePath = `${type}s/${generateFilename(doc._id)}`
129
+ this.assetsSeen.set(doc._id, type)
130
+ this.queueAssetDownload(assetDoc, filePath)
131
+ callback()
132
+ return
133
+ }
77
134
 
78
- callback(null, this.findAndModify(doc, ACTION_REWRITE))
79
- })
135
+ callback(null, this.findAndModify(doc, ACTION_REWRITE))
136
+ },
137
+ )
80
138
 
81
139
  // Called in the case where we don't _want_ assets, so basically just remove all asset documents
82
140
  // as well as references to assets (*.asset._ref ^= (image|file)-)
83
- stripAssets = throughObj(async (doc, enc, callback) => {
141
+ stripAssets = throughObj((doc: SanityDocument, _enc: BufferEncoding, callback) => {
84
142
  if (['sanity.imageAsset', 'sanity.fileAsset'].includes(doc._type)) {
85
143
  callback()
86
144
  return
@@ -91,7 +149,7 @@ export class AssetHandler {
91
149
 
92
150
  // Called when we are using raw export mode along with `assets: false`, where we simply
93
151
  // want to skip asset documents but retain asset references (useful for data mangling)
94
- skipAssets = throughObj((doc, enc, callback) => {
152
+ skipAssets = throughObj((doc: SanityDocument, _enc: BufferEncoding, callback) => {
95
153
  const isAsset = ['sanity.imageAsset', 'sanity.fileAsset'].includes(doc._type)
96
154
  if (isAsset) {
97
155
  callback()
@@ -101,9 +159,9 @@ export class AssetHandler {
101
159
  callback(null, doc)
102
160
  })
103
161
 
104
- noop = throughObj((doc, enc, callback) => callback(null, doc))
162
+ noop = throughObj((doc: SanityDocument, _enc: BufferEncoding, callback) => callback(null, doc))
105
163
 
106
- queueAssetDownload(assetDoc, dstPath) {
164
+ queueAssetDownload(assetDoc: AssetDocument, dstPath: string): void {
107
165
  if (!assetDoc.url) {
108
166
  debug('Asset document "%s" does not have a URL property, skipping', assetDoc._id)
109
167
  return
@@ -113,20 +171,21 @@ export class AssetHandler {
113
171
  this.queueSize++
114
172
  this.downloading.push(assetDoc.url)
115
173
 
116
- const doDownload = async () => {
117
- let dlError
174
+ const doDownload = async (): Promise<boolean> => {
175
+ let dlError: DownloadError | undefined
118
176
  for (let attempt = 0; attempt < this.maxRetries; attempt++) {
119
177
  try {
120
178
  return await this.downloadAsset(assetDoc, dstPath)
121
179
  } catch (err) {
180
+ const downloadError = err as DownloadError
122
181
  // Ignore inaccessible assets
123
- switch (err.statusCode) {
182
+ switch (downloadError.statusCode) {
124
183
  case 401:
125
184
  case 403:
126
185
  case 404:
127
186
  console.warn(
128
187
  `⚠ Asset failed with HTTP %d (ignoring): %s`,
129
- err.statusCode,
188
+ downloadError.statusCode,
130
189
  assetDoc._id,
131
190
  )
132
191
  return true
@@ -141,59 +200,62 @@ export class AssetHandler {
141
200
  err,
142
201
  )
143
202
 
144
- dlError = err
203
+ dlError = downloadError
145
204
 
146
- if ('statusCode' in err && err.statusCode >= 400 && err.statusCode < 500) {
205
+ if (
206
+ downloadError.statusCode &&
207
+ downloadError.statusCode >= 400 &&
208
+ downloadError.statusCode < 500
209
+ ) {
147
210
  // Don't retry on client errors
148
211
  break
149
212
  }
150
213
 
151
- await delay(this.retryDelayMs || DEFAULT_RETRY_DELAY)
214
+ await delay(this.retryDelayMs ?? DEFAULT_RETRY_DELAY)
152
215
  }
153
216
  }
154
- throw dlError
217
+ throw new Error(dlError?.message ?? 'Unknown error downloading asset')
155
218
  }
156
219
 
157
220
  this.queue
158
221
  .add(() =>
159
- doDownload().catch((err) => {
222
+ doDownload().catch((err: unknown) => {
160
223
  debug('Failed to download the asset, aborting download', err)
161
224
  this.queue.clear()
162
- this.reject(err)
225
+ this.reject(err instanceof Error ? err : new Error(String(err)))
163
226
  }),
164
227
  )
165
- .catch((error) => {
228
+ .catch((error: unknown) => {
166
229
  debug('Queued task failed', error)
167
230
  })
168
231
  }
169
232
 
170
- maybeCreateAssetDirs() {
233
+ maybeCreateAssetDirs(): void {
171
234
  if (this.assetDirsCreated) {
172
235
  return
173
236
  }
174
237
 
175
- /* eslint-disable no-sync */
176
238
  mkdirSync(joinPath(this.tmpDir, 'files'), {recursive: true})
177
239
  mkdirSync(joinPath(this.tmpDir, 'images'), {recursive: true})
178
- /* eslint-enable no-sync */
179
240
  this.assetDirsCreated = true
180
241
  }
181
242
 
182
- getAssetRequestOptions(assetDoc) {
243
+ getAssetRequestOptions(assetDoc: AssetDocument): AssetRequestOptions {
183
244
  const token = this.client.config().token
184
- const headers = {'User-Agent': getUserAgent()}
245
+ const headers: Record<string, string> = {'User-Agent': getUserAgent()}
185
246
  const isImage = assetDoc._type === 'sanity.imageAsset'
186
247
 
187
- const url = URL.parse(assetDoc.url)
248
+ const url = URL.parse(assetDoc.url ?? '')
188
249
  // If we can't parse it, return as-is
189
250
  if (!url) {
190
- return {url: assetDoc.url, headers}
251
+ return {url: assetDoc.url ?? '', headers}
191
252
  }
192
253
 
193
254
  if (
194
255
  isImage &&
195
256
  token &&
196
- (['cdn.sanity.io', 'cdn.sanity.work'].includes(url.hostname) ||
257
+ (url.hostname === 'cdn.sanity.io' ||
258
+ url.hostname === 'cdn.sanity.work' ||
197
259
  // used in tests. use a very specific port to avoid conflicts
198
260
  url.host === 'localhost:43216')
199
261
  ) {
@@ -204,15 +266,14 @@ export class AssetHandler {
204
266
  return {url: url.toString(), headers}
205
267
  }
206
268
 
207
- // eslint-disable-next-line max-statements
208
- async downloadAsset(assetDoc, dstPath) {
269
+ async downloadAsset(assetDoc: AssetDocument, dstPath: string): Promise<boolean> {
209
270
  const {url} = assetDoc
210
271
 
211
272
  debug('Downloading asset %s', url)
212
273
 
213
274
  const options = this.getAssetRequestOptions(assetDoc)
214
275
 
215
- let stream
276
+ let stream: ResponseStream
216
277
  try {
217
278
  stream = await requestStream({
218
279
  maxRetries: 0, // We handle retries ourselves in queueAssetDownload
@@ -220,8 +281,7 @@ export class AssetHandler {
220
281
  })
221
282
  } catch (err) {
222
283
  const message = 'Failed to create asset stream'
223
- if (typeof err.message === 'string') {
224
- // try to re-assign the error message so the stack trace is more visible
284
+ if (err instanceof Error) {
225
285
  err.message = `${message}: ${err.message}`
226
286
  throw err
227
287
  }
@@ -230,7 +290,7 @@ export class AssetHandler {
230
290
  }
231
291
 
232
292
  if (stream.statusCode !== 200) {
233
- let errMsg
293
+ let errMsg: string
234
294
  try {
235
295
  const err = await tryGetErrorFromStream(stream)
236
296
  errMsg = `Referenced asset URL "${url}" returned HTTP ${stream.statusCode}`
@@ -239,8 +299,7 @@ export class AssetHandler {
239
299
  }
240
300
  } catch (err) {
241
301
  const message = 'Failed to parse error response from asset stream'
242
- if (typeof err.message === 'string') {
243
- // try to re-assign the error message so the stack trace is more visible
302
+ if (err instanceof Error) {
244
303
  err.message = `${message}: ${err.message}`
245
304
  throw err
246
305
  }
@@ -248,8 +307,10 @@ export class AssetHandler {
248
307
  throw new Error(message, {cause: err})
249
308
  }
250
309
 
251
- const streamError = new Error(errMsg)
252
- streamError.statusCode = stream.statusCode
310
+ const streamError: DownloadError = new Error(errMsg)
311
+ if (stream.statusCode !== undefined) {
312
+ streamError.statusCode = stream.statusCode
313
+ }
253
314
  throw streamError
254
315
  }
255
316
 
@@ -268,7 +329,7 @@ export class AssetHandler {
268
329
  } catch (err) {
269
330
  const message = 'Failed to write asset stream to filesystem'
270
331
 
271
- if (typeof err.message === 'string') {
332
+ if (err instanceof Error) {
272
333
  err.message = `${message}: ${err.message}`
273
334
  throw err
274
335
  }
@@ -277,9 +338,9 @@ export class AssetHandler {
277
338
  }
278
339
 
279
340
  // Verify it against our downloaded stream to make sure we have the same copy
280
- const contentLength = stream.headers['content-length']
281
- const remoteSha1 = stream.headers['x-sanity-sha1']
282
- const remoteMd5 = stream.headers['x-sanity-md5']
341
+ const contentLength = stream.headers?.['content-length']
342
+ const remoteSha1 = stream.headers?.['x-sanity-sha1']
343
+ const remoteMd5 = stream.headers?.['x-sanity-md5']
283
344
  const hasHash = Boolean(remoteSha1 || remoteMd5)
284
345
  const method = sha1 ? 'sha1' : 'md5'
285
346
 
@@ -298,13 +359,13 @@ export class AssetHandler {
298
359
  : `sha1 should be ${remoteSha1}, got ${sha1}`),
299
360
 
300
361
  contentLength &&
301
- parseInt(contentLength, 10) !== size &&
362
+ parseInt(String(contentLength), 10) !== size &&
302
363
  `Asset should be ${contentLength} bytes, got ${size}`,
303
364
  ]
304
365
 
305
366
  const detailsString = `Details:\n - ${details.filter(Boolean).join('\n - ')}`
306
367
 
307
- await rimraf(tmpPath)
368
+ await rm(tmpPath, {recursive: true, force: true})
308
369
 
309
370
  throw new Error(`Failed to download asset at ${assetDoc.url}. ${detailsString}`)
310
371
  }
@@ -327,43 +388,44 @@ export class AssetHandler {
327
388
  return true
328
389
  }
329
390
 
330
- findAndModify = (item, action) => {
391
+ findAndModify = (item: unknown, action: AssetAction): unknown => {
331
392
  if (Array.isArray(item)) {
332
- const children = item.map((child) => this.findAndModify(child, action))
333
- return children.filter(function (child) {
334
- return child !== null && child !== undefined
335
- })
393
+ const children = item.map((child: unknown) => this.findAndModify(child, action))
394
+ return children.filter((child): child is NonNullable<typeof child> => child != null)
336
395
  }
337
396
 
338
397
  if (!item || typeof item !== 'object') {
339
398
  return item
340
399
  }
341
400
 
342
- const isAsset = isAssetField(item)
401
+ const record = item as Record<string, unknown>
402
+
403
+ const isAsset = isAssetField(record)
343
404
  if (isAsset && action === ACTION_REMOVE) {
344
405
  return undefined
345
406
  }
346
407
 
347
408
  if (isAsset && action === ACTION_REWRITE) {
348
- const {asset, ...other} = item
409
+ const {asset, ...other} = record
349
410
  const assetId = asset._ref
350
- const assetType = getAssetType(item)
411
+ const assetType = getAssetType(record)
351
412
  const filePath = `${assetType}s/${generateFilename(assetId)}`
413
+ const modified = this.findAndModify(other, action)
352
414
  return {
353
415
  _sanityAsset: `${assetType}@file://./${filePath}`,
354
- ...this.findAndModify(other, action),
355
- }
416
+ ...(typeof modified === 'object' && modified !== null ? modified : {}),
417
+ } as RewrittenAssetField
356
418
  }
357
419
 
358
- const newItem = {}
359
- const keys = Object.keys(item)
360
- for (let i = 0; i < keys.length; i++) {
361
- const key = keys[i]
362
- const value = item[key]
420
+ const newItem: Record<string, unknown> = {}
421
+ const keys = Object.keys(record)
422
+ for (const key of keys) {
423
+ const value = record[key]
363
424
 
364
425
  newItem[key] = this.findAndModify(value, action)
365
426
 
366
427
  if (typeof newItem[key] === 'undefined') {
428
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
367
429
  delete newItem[key]
368
430
  }
369
431
  }
@@ -372,38 +434,51 @@ export class AssetHandler {
372
434
  }
373
435
  }
374
436
 
375
- function isAssetField(item) {
376
- return item.asset && item.asset._ref && isSanityAsset(item.asset._ref)
437
+ function isAssetField(item: Record<string, unknown>): item is AssetField {
438
+ const asset = item.asset as {_ref?: unknown} | undefined
439
+ return Boolean(asset?._ref && typeof asset._ref === 'string' && isSanityAsset(asset._ref))
377
440
  }
378
441
 
379
- function getAssetType(item) {
380
- if (!item.asset || typeof item.asset._ref !== 'string') {
442
+ function getAssetType(item: Record<string, unknown>): string | null {
443
+ const asset = item.asset as {_ref?: unknown} | undefined
444
+ if (!asset || typeof asset._ref !== 'string') {
381
445
  return null
382
446
  }
383
447
 
384
- const [, type] = item.asset._ref.match(/^(image|file)-/) || []
385
- return type || null
448
+ const match = asset._ref.match(/^(image|file)-/)
449
+ return match?.[1] ?? null
386
450
  }
387
451
 
388
- function isSanityAsset(assetId) {
452
+ function isSanityAsset(assetId: string): boolean {
389
453
  return (
390
454
  /^image-[a-f0-9]{40}-\d+x\d+-[a-z]+$/.test(assetId) ||
391
455
  /^file-[a-f0-9]{40}-[a-z0-9]+$/.test(assetId)
392
456
  )
393
457
  }
394
458
 
395
- function generateFilename(assetId) {
396
- const [, , asset, ext] = assetId.match(/^(image|file)-(.*?)(-[a-z]+)?$/) || []
397
- const extension = (ext || 'bin').replace(/^-/, '')
459
+ function generateFilename(assetId: string): string {
460
+ const match = assetId.match(/^(image|file)-(.*?)(-[a-z]+)?$/)
461
+ const asset = match?.[2]
462
+ const ext = match?.[3]
463
+ const extension = (ext ?? 'bin').replace(/^-/, '')
398
464
  return asset ? `${asset}.${extension}` : `${assetId}.bin`
399
465
  }
400
466
 
401
- async function writeHashedStream(filePath, stream) {
467
+ interface HashResult {
468
+ size: number
469
+ sha1: string
470
+ md5: string
471
+ }
472
+
473
+ async function writeHashedStream(
474
+ filePath: string,
475
+ stream: NodeJS.ReadableStream,
476
+ ): Promise<HashResult> {
402
477
  let size = 0
403
478
  const md5 = createHash('md5')
404
479
  const sha1 = createHash('sha1')
405
480
 
406
- const hasher = through((chunk, enc, cb) => {
481
+ const hasher = through((chunk, _enc, cb) => {
407
482
  size += chunk.length
408
483
  md5.update(chunk)
409
484
  sha1.update(chunk)
@@ -418,12 +493,12 @@ async function writeHashedStream(filePath, stream) {
418
493
  }
419
494
  }
420
495
 
421
- function tryGetErrorFromStream(stream) {
496
+ function tryGetErrorFromStream(stream: NodeJS.ReadableStream): Promise<string | null> {
422
497
  return new Promise((resolve, reject) => {
423
- const chunks = []
498
+ const chunks: Buffer[] = []
424
499
  let receivedData = false
425
500
 
426
- stream.on('data', (chunk) => {
501
+ stream.on('data', (chunk: Buffer) => {
427
502
  receivedData = true
428
503
  chunks.push(chunk)
429
504
  })
@@ -436,8 +511,8 @@ function tryGetErrorFromStream(stream) {
436
511
 
437
512
  const body = Buffer.concat(chunks)
438
513
  try {
439
- const parsed = JSON.parse(body.toString('utf8'))
440
- resolve(parsed.message || parsed.error || null)
514
+ const parsed = JSON.parse(body.toString('utf8')) as {message?: string; error?: string}
515
+ resolve(parsed.message ?? parsed.error ?? null)
441
516
  } catch {
442
517
  resolve(body.toString('utf8').slice(0, 16000))
443
518
  }
@@ -447,12 +522,12 @@ function tryGetErrorFromStream(stream) {
447
522
  })
448
523
  }
449
524
 
450
- function omit(obj, keys) {
451
- const copy = {}
452
- Object.entries(obj).forEach(([key, value]) => {
525
+ function omit(obj: Record<string, unknown>, keys: string[]): AssetMetadata {
526
+ const copy: AssetMetadata = {}
527
+ for (const [key, value] of Object.entries(obj)) {
453
528
  if (!keys.includes(key)) {
454
529
  copy[key] = value
455
530
  }
456
- })
531
+ }
457
532
  return copy
458
533
  }
@@ -0,0 +1,50 @@
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
+ export const DOCUMENT_STREAM_MAX_RETRIES: number = 5
9
+
10
+ /**
11
+ * How many retries to attempt when downloading an asset.
12
+ * User overridable as `options.maxAssetRetries`.
13
+ * @internal
14
+ */
15
+ export const ASSET_DOWNLOAD_MAX_RETRIES: number = 10
16
+
17
+ /**
18
+ * Default delay between retries when retrieving assets or document stream.
19
+ * User overridable as `options.retryDelayMs`.
20
+ * @internal
21
+ */
22
+ export const DEFAULT_RETRY_DELAY: number = 1500
23
+
24
+ /**
25
+ * How many concurrent asset downloads to allow.
26
+ * User overridable as `options.assetConcurrency`.
27
+ * @internal
28
+ */
29
+ export const ASSET_DOWNLOAD_CONCURRENCY: number = 8
30
+
31
+ /**
32
+ * How frequently we will `debug` log while streaming the documents.
33
+ * @internal
34
+ */
35
+ export const DOCUMENT_STREAM_DEBUG_INTERVAL: number = 10000
36
+
37
+ /**
38
+ * How long to wait before timing out the read of a request due to inactivity.
39
+ * User overridable as `options.readTimeout`.
40
+ * @internal
41
+ */
42
+ export const REQUEST_READ_TIMEOUT: number = 3 * 60 * 1000 // 3 minutes
43
+
44
+ /**
45
+ * What mode to use when exporting documents.
46
+ * stream: Export all documents in the dataset in one request, this will be consistent but might be slow on large datasets.
47
+ * cursor: Export documents using a cursor, this might lead to inconsistent results if a mutation is performed while exporting.
48
+ */
49
+ export const MODE_STREAM = 'stream' as const
50
+ export const MODE_CURSOR = 'cursor' as const
package/src/debug.ts ADDED
@@ -0,0 +1,3 @@
1
+ import createDebug, {type Debugger} from 'debug'
2
+
3
+ export const debug: Debugger = createDebug('sanity:export')