@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
package/src/options.ts ADDED
@@ -0,0 +1,138 @@
1
+ import {
2
+ ASSET_DOWNLOAD_MAX_RETRIES,
3
+ DOCUMENT_STREAM_MAX_RETRIES,
4
+ MODE_CURSOR,
5
+ MODE_STREAM,
6
+ REQUEST_READ_TIMEOUT,
7
+ } from './constants.js'
8
+ import type {ExportOptions, ExportSource, NormalizedExportOptions, SanityDocument} from './types.js'
9
+
10
+ const booleanFlags = ['assets', 'raw', 'compress', 'drafts'] as const
11
+ const numberFlags = ['maxAssetRetries', 'maxRetries', 'assetConcurrency', 'readTimeout'] as const
12
+
13
+ const exportDefaults = {
14
+ compress: true,
15
+ drafts: true,
16
+ assets: true,
17
+ assetsMap: true,
18
+ raw: false,
19
+ mode: MODE_STREAM,
20
+ maxRetries: DOCUMENT_STREAM_MAX_RETRIES,
21
+ maxAssetRetries: ASSET_DOWNLOAD_MAX_RETRIES,
22
+ readTimeout: REQUEST_READ_TIMEOUT,
23
+ filterDocument: (): boolean => true,
24
+ transformDocument: (doc: SanityDocument): SanityDocument => doc,
25
+ } as const
26
+
27
+ export function validateOptions(opts: ExportOptions): NormalizedExportOptions {
28
+ const options = {...exportDefaults, ...opts}
29
+
30
+ const dataset =
31
+ 'dataset' in options && typeof options.dataset === 'string' ? options.dataset : undefined
32
+ const mediaLibraryId =
33
+ 'mediaLibraryId' in options && typeof options.mediaLibraryId === 'string'
34
+ ? options.mediaLibraryId
35
+ : undefined
36
+
37
+ if (dataset && mediaLibraryId) {
38
+ throw new Error(
39
+ 'either `options.dataset` or `options.mediaLibraryId` must be specified, got both',
40
+ )
41
+ }
42
+
43
+ if (!dataset && !mediaLibraryId) {
44
+ throw new Error(
45
+ 'either `options.dataset` or `options.mediaLibraryId` must be specified, got neither',
46
+ )
47
+ }
48
+
49
+ const source = getSource(options)
50
+ if (!source.id.trim()) {
51
+ throw new Error(`Source (${source.type}) specified but was empty`)
52
+ }
53
+
54
+ // Type narrowing for mode validation
55
+ const mode = options.mode as string
56
+ if (typeof mode !== 'string' || (mode !== MODE_STREAM && mode !== MODE_CURSOR)) {
57
+ throw new Error(
58
+ `options.mode must be either "${MODE_STREAM}" or "${MODE_CURSOR}", got "${mode}"`,
59
+ )
60
+ }
61
+
62
+ if (typeof options.onProgress !== 'undefined' && typeof options.onProgress !== 'function') {
63
+ throw new Error(`options.onProgress must be a function`)
64
+ }
65
+
66
+ if (
67
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
68
+ !options.client ||
69
+ !('config' in options.client) ||
70
+ typeof options.client.getUrl !== 'function'
71
+ ) {
72
+ throw new Error('`options.client` must be set to an instance of @sanity/client')
73
+ }
74
+
75
+ const clientConfig = options.client.config()
76
+ if (!clientConfig.token) {
77
+ throw new Error('Client is not instantiated with a `token`')
78
+ }
79
+
80
+ for (const flag of booleanFlags) {
81
+ if (typeof options[flag] !== 'boolean') {
82
+ throw new Error(`Flag ${flag} must be a boolean (true/false)`)
83
+ }
84
+ }
85
+
86
+ for (const flag of numberFlags) {
87
+ if (typeof options[flag] !== 'undefined' && typeof options[flag] !== 'number') {
88
+ throw new Error(`Flag ${flag} must be a number if specified`)
89
+ }
90
+ }
91
+
92
+ if (!options.outputPath) {
93
+ throw new Error('outputPath must be specified (- for stdout)')
94
+ }
95
+
96
+ if (options.assetConcurrency && (options.assetConcurrency < 1 || options.assetConcurrency > 24)) {
97
+ throw new Error('`assetConcurrency` must be between 1 and 24')
98
+ }
99
+
100
+ if (
101
+ typeof options.filterDocument !== 'undefined' &&
102
+ typeof options.filterDocument !== 'function'
103
+ ) {
104
+ throw new Error('`filterDocument` must be a function')
105
+ }
106
+
107
+ if (
108
+ typeof options.transformDocument !== 'undefined' &&
109
+ typeof options.transformDocument !== 'function'
110
+ ) {
111
+ throw new Error('`transformDocument` must be a function')
112
+ }
113
+
114
+ if (typeof options.assetsMap !== 'undefined' && typeof options.assetsMap !== 'boolean') {
115
+ throw new Error('`assetsMap` must be a boolean')
116
+ }
117
+
118
+ return options
119
+ }
120
+
121
+ /**
122
+ * Determines the source type and ID from the provided options.
123
+ *
124
+ * @param options - The export options containing either dataset or mediaLibraryId.
125
+ * @returns An object with the source type and its corresponding ID.
126
+ * @internal
127
+ */
128
+ export function getSource(options: ExportSource): {
129
+ type: 'dataset' | 'media-library'
130
+ id: string
131
+ } {
132
+ if ('dataset' in options && typeof options.dataset === 'string') {
133
+ return {type: 'dataset', id: options.dataset}
134
+ } else if ('mediaLibraryId' in options && typeof options.mediaLibraryId === 'string') {
135
+ return {type: 'media-library', id: options.mediaLibraryId}
136
+ }
137
+ throw new Error('Either dataset or mediaLibraryId must be specified')
138
+ }
@@ -0,0 +1,62 @@
1
+ import type {Transform} from 'node:stream'
2
+
3
+ import {throughObj} from './util/streamHelpers.js'
4
+ import type {SanityDocument} from './types.js'
5
+
6
+ interface ErrorDocument {
7
+ _id?: undefined
8
+ error:
9
+ | string
10
+ | {
11
+ description?: string
12
+ message?: string
13
+ }
14
+ message?: string
15
+ statusCode?: number
16
+ }
17
+
18
+ type DocumentOrError = SanityDocument | ErrorDocument
19
+
20
+ function isErrorDocument(doc: DocumentOrError): doc is ErrorDocument {
21
+ return !('_id' in doc && doc._id) && 'error' in doc
22
+ }
23
+
24
+ export function rejectOnApiError(): Transform {
25
+ return throughObj((doc: DocumentOrError, _enc, callback) => {
26
+ // check if the document passed contains a document attribute first, and return early.
27
+ if ('_id' in doc && doc._id) {
28
+ callback(null, doc)
29
+ return
30
+ }
31
+
32
+ if (isErrorDocument(doc)) {
33
+ // if we got a statusCode we can decorate the error with it
34
+ if (doc.statusCode) {
35
+ const err = doc.error
36
+ const errorMessage =
37
+ typeof err === 'string'
38
+ ? err
39
+ : typeof err === 'object'
40
+ ? (err.description ?? err.message)
41
+ : undefined
42
+ callback(
43
+ new Error(
44
+ ['Export', `HTTP ${doc.statusCode}`, errorMessage, doc.message]
45
+ .filter((part): part is string => typeof part === 'string')
46
+ .join(': '),
47
+ ),
48
+ )
49
+ return
50
+ }
51
+
52
+ // no statusCode, just serialize and return the error
53
+ const error = doc.error
54
+ const errorMessage =
55
+ typeof error === 'object' ? (error.description ?? error.message) : undefined
56
+ callback(new Error(errorMessage ?? JSON.stringify(doc)))
57
+ return
58
+ }
59
+
60
+ callback(null, doc)
61
+ })
62
+ }
@@ -0,0 +1,81 @@
1
+ import {getIt} from 'get-it'
2
+ import {keepAlive, promise} from 'get-it/middleware'
3
+
4
+ import {delay} from './util/delay.js'
5
+ import {extractFirstError} from './util/extractFirstError.js'
6
+ import {tryThrowFriendlyError} from './util/friendlyError.js'
7
+ import {
8
+ DEFAULT_RETRY_DELAY,
9
+ DOCUMENT_STREAM_MAX_RETRIES,
10
+ REQUEST_READ_TIMEOUT,
11
+ } from './constants.js'
12
+ import {debug} from './debug.js'
13
+ import type {RequestStreamOptions, ResponseStream} from './types.js'
14
+
15
+ interface GetItResponse extends ResponseStream {
16
+ statusCode: number
17
+ headers: Record<string, string | string[] | undefined>
18
+ }
19
+
20
+ interface ErrorWithResponse extends Error {
21
+ response?: {
22
+ statusCode?: number
23
+ }
24
+ }
25
+
26
+ const request = getIt([keepAlive(), promise({onlyBody: true})]) as unknown as (
27
+ options: Record<string, unknown>,
28
+ ) => Promise<GetItResponse>
29
+
30
+ const CONNECTION_TIMEOUT = 15 * 1000 // 15 seconds
31
+
32
+ export async function requestStream(options: RequestStreamOptions): Promise<ResponseStream> {
33
+ const maxRetries =
34
+ options.maxRetries !== undefined ? options.maxRetries : DOCUMENT_STREAM_MAX_RETRIES
35
+
36
+ const readTimeout = options.readTimeout !== undefined ? options.readTimeout : REQUEST_READ_TIMEOUT
37
+
38
+ const retryDelayMs =
39
+ options.retryDelayMs !== undefined ? options.retryDelayMs : DEFAULT_RETRY_DELAY
40
+
41
+ let error: ErrorWithResponse | undefined
42
+
43
+ let i = 0
44
+ do {
45
+ i++
46
+
47
+ try {
48
+ return await request({
49
+ ...options,
50
+ stream: true,
51
+ maxRedirects: 0,
52
+ timeout: {connect: CONNECTION_TIMEOUT, socket: readTimeout},
53
+ })
54
+ } catch (err) {
55
+ const firstError = extractFirstError(err) as ErrorWithResponse | null
56
+ error = firstError !== null ? firstError : (err as ErrorWithResponse)
57
+
58
+ if (maxRetries === 0) {
59
+ throw error
60
+ }
61
+
62
+ if (error.response?.statusCode && error.response.statusCode < 500) {
63
+ break
64
+ }
65
+
66
+ if (i < maxRetries) {
67
+ debug('Error, retrying after %d ms: %s', retryDelayMs, error.message)
68
+ await delay(retryDelayMs)
69
+ }
70
+ }
71
+ } while (i < maxRetries)
72
+
73
+ await tryThrowFriendlyError(error)
74
+
75
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
76
+ if (!error) {
77
+ throw new Error(`Export: Failed to fetch ${options.url}: Unknown error`)
78
+ }
79
+
80
+ throw new Error(`Export: Failed to fetch ${options.url}: ${error.message}`)
81
+ }
@@ -0,0 +1,7 @@
1
+ import type {Transform} from 'node:stream'
2
+
3
+ import {throughObj} from './util/streamHelpers.js'
4
+
5
+ export function stringifyStream(): Transform {
6
+ return throughObj((doc: unknown, _enc, callback) => callback(null, `${JSON.stringify(doc)}\n`))
7
+ }
@@ -1,30 +1,30 @@
1
- /**
2
- * Safe JSON parser that is able to handle lines interrupted by an error object.
3
- *
4
- * This may occur when streaming NDJSON from the Export HTTP API.
5
- *
6
- * @internal
7
- * @see {@link https://github.com/sanity-io/sanity/pull/1787 | Initial pull request}
8
- */
9
- export const tryParseJson = createSafeJsonParser({
10
- errorLabel: 'Error streaming dataset',
11
- })
1
+ interface SafeJsonParserOptions {
2
+ errorLabel: string
3
+ }
12
4
 
13
- function createSafeJsonParser({errorLabel}) {
14
- return function safeJsonParser(line) {
5
+ interface ParsedErrorLine {
6
+ error?: {
7
+ description?: string
8
+ }
9
+ }
10
+
11
+ function createSafeJsonParser({errorLabel}: SafeJsonParserOptions) {
12
+ return function safeJsonParser(line: string): unknown {
15
13
  try {
16
- return JSON.parse(line)
14
+ return JSON.parse(line) as unknown
17
15
  } catch (err) {
18
16
  // Catch half-done lines with an error at the end
19
17
  const errorPosition = line.lastIndexOf('{"error":')
20
18
  if (errorPosition === -1) {
21
- err.message = `${err.message} (${line})`
19
+ if (err instanceof Error) {
20
+ err.message = `${err.message} (${line})`
21
+ }
22
22
  throw err
23
23
  }
24
24
 
25
25
  const errorJson = line.slice(errorPosition)
26
- const errorLine = JSON.parse(errorJson)
27
- const error = errorLine && errorLine.error
26
+ const errorLine = JSON.parse(errorJson) as ParsedErrorLine
27
+ const error = errorLine.error
28
28
  if (error && error.description) {
29
29
  throw new Error(`${errorLabel}: ${error.description}\n\n${errorJson}\n`, {cause: err})
30
30
  }
@@ -33,3 +33,15 @@ function createSafeJsonParser({errorLabel}) {
33
33
  }
34
34
  }
35
35
  }
36
+
37
+ /**
38
+ * Safe JSON parser that is able to handle lines interrupted by an error object.
39
+ *
40
+ * This may occur when streaming NDJSON from the Export HTTP API.
41
+ *
42
+ * @internal
43
+ * @see {@link https://github.com/sanity-io/sanity/pull/1787 | Initial pull request}
44
+ */
45
+ export const tryParseJson = createSafeJsonParser({
46
+ errorLabel: 'Error streaming dataset',
47
+ })
package/src/types.ts ADDED
@@ -0,0 +1,274 @@
1
+ import type {Writable} from 'node:stream'
2
+
3
+ /**
4
+ * The mode to use when exporting documents.
5
+ *
6
+ * - `"stream"` mode uses a continuous stream of documents from the source and will
7
+ * give a consistent snapshot of the dataset at the time the export started, but
8
+ * will struggle with very large datasets.
9
+ * - `"cursor"` mode uses paginated requests to fetch documents using a cursor, and
10
+ * will handle very large datasets better, but may give an inconsistent snapshot if
11
+ * the dataset is being modified during the export.
12
+ *
13
+ * @public
14
+ */
15
+ export type ExportMode = 'stream' | 'cursor'
16
+
17
+ /**
18
+ * Minimal representation of a Sanity document.
19
+ *
20
+ * @public
21
+ */
22
+ export interface SanityDocument {
23
+ _id: string
24
+ _type: string
25
+ _rev?: string
26
+ _createdAt?: string
27
+ _updatedAt?: string
28
+ [key: string]: unknown
29
+ }
30
+
31
+ /**
32
+ * @public
33
+ */
34
+ export interface ExportProgress {
35
+ /** Description of the current export step */
36
+ step: string
37
+
38
+ /** Number of documents processed so far */
39
+ current?: number
40
+
41
+ /** Total number of documents, if known - otherwise `?` */
42
+ total?: number | '?'
43
+
44
+ /** Set to `true` if the progress update is a repeat of a previous one but with new values */
45
+ update?: boolean
46
+ }
47
+
48
+ /**
49
+ * @public
50
+ */
51
+ export interface SanityClientLike {
52
+ getUrl: (path: string) => string
53
+ config: () => {token?: string}
54
+ }
55
+
56
+ /**
57
+ * The options used to configure an export operation.
58
+ *
59
+ * @public
60
+ */
61
+ export type ExportOptions = {
62
+ /**
63
+ * An instance of `@sanity/client`, configured with the project ID and authentication
64
+ * token to be used for the export operation.
65
+ */
66
+ client: SanityClientLike
67
+
68
+ /**
69
+ * Either a filesystem path to write the output `tar.gz` file to, or a writable stream
70
+ */
71
+ outputPath: string | Writable
72
+
73
+ /**
74
+ * Whether or not to include asset files in the export
75
+ */
76
+ assets?: boolean
77
+
78
+ /**
79
+ * Whether or not to export the documents "raw", meaning asset documents will be
80
+ * included as-is, referencing asset file URLs in the source dataset and project ID or
81
+ * media library ID. Also skips downloading assets. Generally this is only useful if
82
+ * importing to the same source and you do not want to download the assets and
83
+ * re-upload them to the same source.
84
+ *
85
+ * @note This will usually cause undesirable results if imported into another project
86
+ * or media library!
87
+ */
88
+ raw?: boolean
89
+
90
+ /**
91
+ * Whether or not to include draft documents in the export.
92
+ */
93
+ drafts?: boolean
94
+
95
+ /**
96
+ * An array of document type names to include in the export. If not specified, all types
97
+ * will be included.
98
+ */
99
+ types?: string[] | undefined
100
+
101
+ /**
102
+ * How many asset downloads to perform concurrently. Must be between 1 and 24 if specified.
103
+ */
104
+ assetConcurrency?: number
105
+
106
+ /**
107
+ * Maximum number of times to retry downloading a failed asset.
108
+ *
109
+ * @note Only certain errors are retried (like network errors and 5xx responses).
110
+ */
111
+ maxAssetRetries?: number
112
+
113
+ /**
114
+ * Maximum number of times to retry fetching documents from the source.
115
+ *
116
+ * @note Only certain errors are retried (like network errors and 5xx responses).
117
+ */
118
+ maxRetries?: number
119
+
120
+ /**
121
+ * Delay between retry attempts in milliseconds.
122
+ */
123
+ retryDelayMs?: number
124
+
125
+ /**
126
+ * Timeout for read operations in milliseconds.
127
+ */
128
+ readTimeout?: number
129
+
130
+ /**
131
+ * The mode to use when exporting documents, either `"stream"` or `"cursor"`.
132
+ */
133
+ mode?: ExportMode
134
+
135
+ /**
136
+ * Whether or not to compress the output tarball (gzip). Note that the output will
137
+ * still be a gzipped tarball even if this is set to `false`, setting it to `false`
138
+ * only sets the gzip compression level to 0 (no compression).
139
+ */
140
+ compress?: boolean
141
+
142
+ /**
143
+ * Whether or not to include an `assets.json` file in the export, mapping asset IDs to
144
+ * their custom metadata (like original filename, etc).
145
+ */
146
+ assetsMap?: boolean
147
+
148
+ /**
149
+ * Optional filter function to determine whether or not a document should be included
150
+ * in the export. Note that this is run after any built-in document filtering such as
151
+ * draft exclusion, document type filtering, etc.
152
+ *
153
+ * @param doc - The document to evaluate
154
+ * @returns Whether or not to include the document in the export
155
+ */
156
+ filterDocument?: (doc: SanityDocument) => boolean
157
+
158
+ /**
159
+ * Optional transform function to modify a document before it is included in the export.
160
+ * Note that this is run post-filtering, and post asset document processing.
161
+ *
162
+ * @param doc - The document to transform
163
+ * @returns The transformed document
164
+ */
165
+ transformDocument?: (doc: SanityDocument) => SanityDocument
166
+
167
+ /**
168
+ * Optional progress callback that will be called periodically during the export.
169
+ *
170
+ * @param progress - The current export progress
171
+ */
172
+ onProgress?: (progress: ExportProgress) => void
173
+ } & ExportSource
174
+
175
+ /**
176
+ * The source of data to export, either a dataset name or a media library ID.
177
+ *
178
+ * @public
179
+ */
180
+ export type ExportSource =
181
+ | {
182
+ /**
183
+ * The name of the dataset to export from.
184
+ */
185
+ dataset: string
186
+ }
187
+ | {
188
+ /**
189
+ * The ID of the media library to export from.
190
+ */
191
+ mediaLibraryId: string
192
+ }
193
+
194
+ /**
195
+ * @public
196
+ */
197
+ export interface ExportResult<T = string | Writable> {
198
+ /**
199
+ * The filesystem path or writable stream that was passed as `options.outputPath`.
200
+ */
201
+ outputPath: T
202
+
203
+ /**
204
+ * The number of documents exported.
205
+ */
206
+ documentCount: number
207
+
208
+ /**
209
+ * The number of assets exported.
210
+ */
211
+ assetCount: number
212
+ }
213
+
214
+ /**
215
+ * @internal
216
+ */
217
+ export type NormalizedExportOptions = ExportOptions & {
218
+ assets: boolean
219
+ raw: boolean
220
+ drafts: boolean
221
+ maxAssetRetries: number
222
+ maxRetries: number
223
+ readTimeout: number
224
+ mode: ExportMode
225
+ compress: boolean
226
+ assetsMap: boolean
227
+ filterDocument: (doc: SanityDocument) => boolean
228
+ transformDocument: (doc: SanityDocument) => SanityDocument
229
+ }
230
+
231
+ /**
232
+ * @internal
233
+ */
234
+ export interface AssetMetadata {
235
+ [key: string]: unknown
236
+ }
237
+
238
+ /**
239
+ * @internal
240
+ */
241
+ export interface AssetMap {
242
+ [assetId: string]: AssetMetadata
243
+ }
244
+
245
+ /**
246
+ * @internal
247
+ */
248
+ export interface RequestStreamOptions {
249
+ url: string
250
+ headers?: Record<string, string>
251
+ maxRetries?: number
252
+ retryDelayMs?: number
253
+ readTimeout?: number
254
+ }
255
+
256
+ /**
257
+ * @internal
258
+ */
259
+ export interface ResponseStream extends NodeJS.ReadableStream {
260
+ statusCode?: number
261
+ headers?: Record<string, string | string[] | undefined>
262
+ }
263
+
264
+ /**
265
+ * @internal
266
+ */
267
+ export interface AssetDocument extends SanityDocument {
268
+ _type: 'sanity.imageAsset' | 'sanity.fileAsset'
269
+ url?: string
270
+ path?: string
271
+ assetId?: string
272
+ extension?: string
273
+ mimeType?: string
274
+ }
@@ -1,3 +1,3 @@
1
- export function delay(ms) {
1
+ export function delay(ms: number): Promise<void> {
2
2
  return new Promise((resolve) => setTimeout(resolve, ms))
3
3
  }
@@ -0,0 +1,31 @@
1
+ interface AggregateErrorLike {
2
+ name: string
3
+ errors: Array<{message: string}>
4
+ }
5
+
6
+ function isAggregateError(err: unknown): err is AggregateErrorLike {
7
+ if (typeof err !== 'object' || err === null) {
8
+ return false
9
+ }
10
+
11
+ if (err instanceof AggregateError) {
12
+ return true
13
+ }
14
+
15
+ const record = err as Record<string, unknown>
16
+ return (
17
+ record.name === 'AggregateError' &&
18
+ Array.isArray(record.errors) &&
19
+ record.errors.length > 0 &&
20
+ typeof record.errors[0] === 'object' &&
21
+ record.errors[0] !== null &&
22
+ 'message' in record.errors[0]
23
+ )
24
+ }
25
+
26
+ export function extractFirstError(err: unknown): unknown {
27
+ if (isAggregateError(err)) {
28
+ return err.errors[0]
29
+ }
30
+ return err
31
+ }