@larc-iu/plaid-client 0.2.0-alpha.8 → 0.2.0-alpha.9
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/index.d.ts +41 -3
- package/package.json +1 -1
- package/src/http.js +193 -39
- package/src/index.js +216 -68
- package/src/provenance.js +248 -27
- package/src/services.js +145 -32
package/index.d.ts
CHANGED
|
@@ -312,14 +312,30 @@ interface CommentFilters {
|
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
/** Entities that can carry a comment. */
|
|
315
|
-
type CommentableType =
|
|
315
|
+
type CommentableType =
|
|
316
|
+
| "document"
|
|
317
|
+
| "text"
|
|
318
|
+
| "token"
|
|
319
|
+
| "span"
|
|
320
|
+
| "relation"
|
|
321
|
+
| "vocab-item";
|
|
316
322
|
|
|
317
323
|
interface Comment {
|
|
318
324
|
id: string;
|
|
319
|
-
|
|
320
|
-
|
|
325
|
+
/** The owning project, or null for a comment on a vocabulary entry. */
|
|
326
|
+
projectId: string | null;
|
|
327
|
+
/** The owning document, or null for a comment on a vocabulary entry. */
|
|
328
|
+
documentId: string | null;
|
|
329
|
+
/** The owning vocab layer, set only for a comment on a vocabulary entry. */
|
|
330
|
+
vocabLayerId: string | null;
|
|
321
331
|
entityType: CommentableType;
|
|
322
332
|
entityId: string;
|
|
333
|
+
/**
|
|
334
|
+
* What the comment is about, in words, captured when it was posted. A
|
|
335
|
+
* comment outlives its anchor, and this is what is left to show once the
|
|
336
|
+
* anchor has been deleted.
|
|
337
|
+
*/
|
|
338
|
+
anchorLabel: string | null;
|
|
323
339
|
authorId: string;
|
|
324
340
|
body: string;
|
|
325
341
|
createdAt: string;
|
|
@@ -333,6 +349,7 @@ interface CommentsBundle {
|
|
|
333
349
|
entityType: CommentableType,
|
|
334
350
|
entityId: string,
|
|
335
351
|
body: string,
|
|
352
|
+
opts?: { anchorLabel?: string },
|
|
336
353
|
): Promise<Comment>;
|
|
337
354
|
get(id: string): Promise<Comment>;
|
|
338
355
|
update(id: string, body: string): Promise<Comment>;
|
|
@@ -351,6 +368,20 @@ interface CommentsBundle {
|
|
|
351
368
|
projectId: string,
|
|
352
369
|
filters?: CommentFilters,
|
|
353
370
|
): Promise<Record<string, number>>;
|
|
371
|
+
/** Comments on a vocabulary's entries, oldest first. */
|
|
372
|
+
listInVocab(
|
|
373
|
+
vocabId: string,
|
|
374
|
+
filters?: { entityId?: string },
|
|
375
|
+
): Promise<Comment[]>;
|
|
376
|
+
listInVocabPage(
|
|
377
|
+
vocabId: string,
|
|
378
|
+
opts?: { entityId?: string; limit?: number; cursor?: string },
|
|
379
|
+
): Promise<Page<Comment>>;
|
|
380
|
+
/** `{entryId: count}` over a vocabulary; keys are raw entry ids. */
|
|
381
|
+
countsInVocab(
|
|
382
|
+
vocabId: string,
|
|
383
|
+
filters?: { entityId?: string },
|
|
384
|
+
): Promise<Record<string, number>>;
|
|
354
385
|
}
|
|
355
386
|
|
|
356
387
|
interface ApiTokensBundle {
|
|
@@ -478,6 +509,10 @@ interface DocumentsBundle {
|
|
|
478
509
|
documentId: string,
|
|
479
510
|
file: File,
|
|
480
511
|
auditMessage?: string,
|
|
512
|
+
options?: {
|
|
513
|
+
/** Called with the bytes sent so far as the file goes up (`total` is null when unknown). */
|
|
514
|
+
onProgress?: (progress: { loaded: number; total: number | null }) => void;
|
|
515
|
+
},
|
|
481
516
|
): Promise<any>;
|
|
482
517
|
deleteMedia(documentId: string, auditMessage?: string): Promise<any>;
|
|
483
518
|
setMetadata(
|
|
@@ -852,6 +887,9 @@ export function cpIndexOf(s: string, sub: string, fromCp?: number): number;
|
|
|
852
887
|
export const PLAID_NAMESPACE: "plaid";
|
|
853
888
|
/** The config key, under `plaid`, holding a layer's role. */
|
|
854
889
|
export const ROLE_KEY: "role";
|
|
890
|
+
|
|
891
|
+
/** The server's cap on operations per batch request; a larger batch goes as consecutive requests. */
|
|
892
|
+
export const MAX_BATCH_OPS: 1000;
|
|
855
893
|
/** The fixed role inventory; only these values are interoperable across apps. */
|
|
856
894
|
export const ROLES: {
|
|
857
895
|
readonly BASELINE: "baseline";
|
package/package.json
CHANGED
package/src/http.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { transformRequest, transformResponse } from
|
|
1
|
+
import { transformRequest, transformResponse } from "./transforms.js";
|
|
2
2
|
|
|
3
3
|
// Default per-request timeout (ms). Applied to every request unless the client
|
|
4
4
|
// is constructed with a different `timeout` (0 / null disables it). Note: this
|
|
@@ -13,25 +13,40 @@ export const DEFAULT_BATCH_TIMEOUT_MS = 180000;
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Extract and update document versions from response headers and body.
|
|
16
|
+
*
|
|
17
|
+
* `historical` marks a read made with `as-of`: its body carries the version
|
|
18
|
+
* the document HAD then, which is not what a strict-mode write must claim
|
|
19
|
+
* next, so the body is ignored and only the header (always the live
|
|
20
|
+
* version) is learned from. Without this, viewing an old state and then
|
|
21
|
+
* writing 409s on a version that stopped being current long ago.
|
|
16
22
|
*/
|
|
17
|
-
export function extractDocumentVersions(
|
|
18
|
-
|
|
23
|
+
export function extractDocumentVersions(
|
|
24
|
+
client,
|
|
25
|
+
responseHeaders,
|
|
26
|
+
responseBody = null,
|
|
27
|
+
{ historical = false } = {},
|
|
28
|
+
) {
|
|
29
|
+
const docVersionsHeader = responseHeaders.get("X-Document-Versions");
|
|
19
30
|
if (docVersionsHeader) {
|
|
20
31
|
try {
|
|
21
32
|
const versionsMap = JSON.parse(docVersionsHeader);
|
|
22
|
-
if (typeof versionsMap ===
|
|
33
|
+
if (typeof versionsMap === "object" && versionsMap !== null) {
|
|
23
34
|
// Clone once, then assign — cloning inside the loop is O(n²) and pointless.
|
|
24
|
-
client.documentVersions = {
|
|
35
|
+
client.documentVersions = {
|
|
36
|
+
...client.documentVersions,
|
|
37
|
+
...versionsMap,
|
|
38
|
+
};
|
|
25
39
|
}
|
|
26
40
|
} catch (e) {
|
|
27
|
-
console.warn(
|
|
41
|
+
console.warn("Failed to parse document versions header:", e);
|
|
28
42
|
}
|
|
29
43
|
}
|
|
30
44
|
|
|
31
|
-
if (responseBody && typeof responseBody ===
|
|
32
|
-
if (responseBody[
|
|
45
|
+
if (!historical && responseBody && typeof responseBody === "object") {
|
|
46
|
+
if (responseBody["document/id"] && responseBody["document/version"]) {
|
|
33
47
|
client.documentVersions = { ...client.documentVersions };
|
|
34
|
-
client.documentVersions[responseBody[
|
|
48
|
+
client.documentVersions[responseBody["document/id"]] =
|
|
49
|
+
responseBody["document/version"];
|
|
35
50
|
}
|
|
36
51
|
}
|
|
37
52
|
}
|
|
@@ -43,7 +58,11 @@ export async function parseErrorBody(response) {
|
|
|
43
58
|
try {
|
|
44
59
|
return await response.json();
|
|
45
60
|
} catch (_) {
|
|
46
|
-
return {
|
|
61
|
+
return {
|
|
62
|
+
message: await response
|
|
63
|
+
.text()
|
|
64
|
+
.catch(() => "Unable to read error response"),
|
|
65
|
+
};
|
|
47
66
|
}
|
|
48
67
|
}
|
|
49
68
|
|
|
@@ -51,7 +70,11 @@ export async function parseErrorBody(response) {
|
|
|
51
70
|
* Create an enriched error from a failed HTTP response.
|
|
52
71
|
*/
|
|
53
72
|
export function makeHttpError(response, errorData, url, method) {
|
|
54
|
-
const serverMessage =
|
|
73
|
+
const serverMessage =
|
|
74
|
+
errorData?.error ||
|
|
75
|
+
errorData?.message ||
|
|
76
|
+
response.statusText ||
|
|
77
|
+
"Unknown error";
|
|
55
78
|
const error = new Error(`HTTP ${response.status} ${serverMessage} at ${url}`);
|
|
56
79
|
error.status = response.status;
|
|
57
80
|
error.statusText = response.statusText;
|
|
@@ -65,7 +88,9 @@ export function makeHttpError(response, errorData, url, method) {
|
|
|
65
88
|
* Create a network error (status 0). Timeout aborts get a clearer message.
|
|
66
89
|
*/
|
|
67
90
|
export function makeNetworkError(originalError, url, method) {
|
|
68
|
-
const timedOut =
|
|
91
|
+
const timedOut =
|
|
92
|
+
originalError?.name === "TimeoutError" ||
|
|
93
|
+
originalError?.name === "AbortError";
|
|
69
94
|
const message = timedOut
|
|
70
95
|
? `Request timed out at ${url}`
|
|
71
96
|
: `Network error: ${originalError.message} at ${url}`;
|
|
@@ -95,11 +120,10 @@ export const BUSY_BACKOFF_MS = 250;
|
|
|
95
120
|
* `attempt` must perform the whole request, not just await a prepared one:
|
|
96
121
|
* a fetch body and an AbortSignal.timeout are both single-use.
|
|
97
122
|
*/
|
|
98
|
-
export async function retryWhileBusy(
|
|
99
|
-
|
|
100
|
-
baseDelayMs = BUSY_BACKOFF_MS,
|
|
101
|
-
|
|
102
|
-
} = {}) {
|
|
123
|
+
export async function retryWhileBusy(
|
|
124
|
+
attempt,
|
|
125
|
+
{ retries = BUSY_RETRIES, baseDelayMs = BUSY_BACKOFF_MS, onRetry } = {},
|
|
126
|
+
) {
|
|
103
127
|
for (let i = 0; ; i += 1) {
|
|
104
128
|
try {
|
|
105
129
|
return await attempt();
|
|
@@ -117,12 +141,110 @@ export async function retryWhileBusy(attempt, {
|
|
|
117
141
|
* timeouts are disabled / unsupported.
|
|
118
142
|
*/
|
|
119
143
|
export function timeoutSignal(timeout) {
|
|
120
|
-
if (
|
|
144
|
+
if (
|
|
145
|
+
timeout &&
|
|
146
|
+
timeout > 0 &&
|
|
147
|
+
typeof AbortSignal !== "undefined" &&
|
|
148
|
+
AbortSignal.timeout
|
|
149
|
+
) {
|
|
121
150
|
return AbortSignal.timeout(timeout);
|
|
122
151
|
}
|
|
123
152
|
return undefined;
|
|
124
153
|
}
|
|
125
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Send a request through XMLHttpRequest so the caller can watch the bytes go
|
|
157
|
+
* up: fetch cannot report upload progress. Resolves to a Response, so the rest
|
|
158
|
+
* of the pipeline is the same either way. The timeout is a STALL timeout,
|
|
159
|
+
* re-armed by every progress event, so a large file on a slow link is never
|
|
160
|
+
* cut off while it is still moving, only once nothing has moved for `timeout`
|
|
161
|
+
* ms. Rejects the way fetch does: a TypeError for a network failure, an error
|
|
162
|
+
* named TimeoutError for a stall.
|
|
163
|
+
*/
|
|
164
|
+
export function xhrSend(
|
|
165
|
+
url,
|
|
166
|
+
{ method, headers, body },
|
|
167
|
+
{ onUploadProgress, timeout } = {},
|
|
168
|
+
) {
|
|
169
|
+
return new Promise((resolve, reject) => {
|
|
170
|
+
const xhr = new XMLHttpRequest();
|
|
171
|
+
xhr.open(method, url);
|
|
172
|
+
xhr.responseType = "arraybuffer";
|
|
173
|
+
for (const [name, value] of Object.entries(headers || {}))
|
|
174
|
+
xhr.setRequestHeader(name, value);
|
|
175
|
+
|
|
176
|
+
let timer = null;
|
|
177
|
+
const disarm = () => clearTimeout(timer);
|
|
178
|
+
const arm = () => {
|
|
179
|
+
if (!timeout || timeout <= 0) return;
|
|
180
|
+
disarm();
|
|
181
|
+
timer = setTimeout(() => {
|
|
182
|
+
xhr.abort();
|
|
183
|
+
reject(
|
|
184
|
+
Object.assign(new Error(`Request timed out at ${url}`), {
|
|
185
|
+
name: "TimeoutError",
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
}, timeout);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
xhr.upload.onprogress = (e) => {
|
|
192
|
+
arm();
|
|
193
|
+
if (onUploadProgress) {
|
|
194
|
+
onUploadProgress({
|
|
195
|
+
loaded: e.loaded,
|
|
196
|
+
total: e.lengthComputable ? e.total : null,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
xhr.onprogress = arm;
|
|
201
|
+
xhr.onload = () => {
|
|
202
|
+
disarm();
|
|
203
|
+
if (xhr.status === 0) {
|
|
204
|
+
reject(new TypeError(`Failed to fetch ${url}`));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const responseHeaders = new Headers();
|
|
208
|
+
for (const line of (xhr.getAllResponseHeaders() || "")
|
|
209
|
+
.trim()
|
|
210
|
+
.split(/\r?\n/)) {
|
|
211
|
+
const i = line.indexOf(":");
|
|
212
|
+
if (i > 0)
|
|
213
|
+
responseHeaders.append(
|
|
214
|
+
line.slice(0, i).trim(),
|
|
215
|
+
line.slice(i + 1).trim(),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
// A Response refuses a body for these statuses.
|
|
219
|
+
const responseBody = [204, 205, 304].includes(xhr.status)
|
|
220
|
+
? null
|
|
221
|
+
: xhr.response;
|
|
222
|
+
resolve(
|
|
223
|
+
new Response(responseBody, {
|
|
224
|
+
status: xhr.status,
|
|
225
|
+
statusText: xhr.statusText,
|
|
226
|
+
headers: responseHeaders,
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
};
|
|
230
|
+
xhr.onerror = () => {
|
|
231
|
+
disarm();
|
|
232
|
+
reject(new TypeError(`Failed to fetch ${url}`));
|
|
233
|
+
};
|
|
234
|
+
xhr.onabort = () => {
|
|
235
|
+
disarm();
|
|
236
|
+
reject(
|
|
237
|
+
Object.assign(new Error(`Request aborted at ${url}`), {
|
|
238
|
+
name: "AbortError",
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
arm();
|
|
244
|
+
xhr.send(body);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
126
248
|
/**
|
|
127
249
|
* Generic request method handling all fetch logic.
|
|
128
250
|
*
|
|
@@ -145,6 +267,11 @@ export function timeoutSignal(timeout) {
|
|
|
145
267
|
* timeout - Per-request timeout in ms overriding client.timeout
|
|
146
268
|
* for this call (0/null disables). Used for known-long
|
|
147
269
|
* ops like project delete.
|
|
270
|
+
* onUploadProgress - Called with `{ loaded, total }` (bytes; total null when
|
|
271
|
+
* unknown) as the request body goes up. In a browser the
|
|
272
|
+
* request then travels by XMLHttpRequest (see xhrSend),
|
|
273
|
+
* where the timeout only fires when the upload stalls;
|
|
274
|
+
* elsewhere the callback is ignored and fetch is used.
|
|
148
275
|
*/
|
|
149
276
|
export async function makeRequest(client, method, path, options = {}) {
|
|
150
277
|
const {
|
|
@@ -159,6 +286,7 @@ export async function makeRequest(client, method, path, options = {}) {
|
|
|
159
286
|
binaryResponse,
|
|
160
287
|
auditMessage,
|
|
161
288
|
timeout,
|
|
289
|
+
onUploadProgress,
|
|
162
290
|
} = options;
|
|
163
291
|
|
|
164
292
|
// Build URL
|
|
@@ -195,12 +323,15 @@ export async function makeRequest(client, method, path, options = {}) {
|
|
|
195
323
|
// semantics, while stamping every op would 409 the second op against the
|
|
196
324
|
// version bump the first op itself caused (every queued op captures the
|
|
197
325
|
// same pre-batch version).
|
|
198
|
-
if (
|
|
199
|
-
|
|
326
|
+
if (
|
|
327
|
+
client.strictModeDocumentId &&
|
|
328
|
+
method !== "GET" &&
|
|
329
|
+
!(client.isBatching && client.batchVersionStamped)
|
|
330
|
+
) {
|
|
200
331
|
const docId = client.strictModeDocumentId;
|
|
201
332
|
if (client.documentVersions[docId]) {
|
|
202
333
|
const docVersion = client.documentVersions[docId];
|
|
203
|
-
const separator = url.includes(
|
|
334
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
204
335
|
url += `${separator}document-version=${encodeURIComponent(docVersion)}`;
|
|
205
336
|
if (client.isBatching) client.batchVersionStamped = true;
|
|
206
337
|
}
|
|
@@ -211,19 +342,20 @@ export async function makeRequest(client, method, path, options = {}) {
|
|
|
211
342
|
// self-conflict, so it is stamped on every queued batch op, not just the
|
|
212
343
|
// first. The server templates `{param}` placeholders against the endpoint's
|
|
213
344
|
// own path/query/body params.
|
|
214
|
-
if (auditMessage && method !==
|
|
215
|
-
const separator = url.includes(
|
|
345
|
+
if (auditMessage && method !== "GET") {
|
|
346
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
216
347
|
url += `${separator}audit-message=${encodeURIComponent(auditMessage)}`;
|
|
217
348
|
}
|
|
218
349
|
|
|
219
350
|
// Logical-operation group (see client.beginOperation): stamp every write
|
|
220
351
|
// with the group id; the message rides along too so the server can label
|
|
221
352
|
// the group lazily on whichever tagged write lands first.
|
|
222
|
-
if (client.operationGroup && method !==
|
|
353
|
+
if (client.operationGroup && method !== "GET") {
|
|
223
354
|
const group = client.operationGroup;
|
|
224
|
-
const separator = url.includes(
|
|
355
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
225
356
|
url += `${separator}group-id=${encodeURIComponent(group.id)}`;
|
|
226
|
-
if (group.message)
|
|
357
|
+
if (group.message)
|
|
358
|
+
url += `&group-message=${encodeURIComponent(group.message)}`;
|
|
227
359
|
group.written = true;
|
|
228
360
|
}
|
|
229
361
|
|
|
@@ -236,7 +368,7 @@ export async function makeRequest(client, method, path, options = {}) {
|
|
|
236
368
|
throw new Error(`This endpoint cannot be used in batch mode: ${path}`);
|
|
237
369
|
}
|
|
238
370
|
const operation = {
|
|
239
|
-
path: url.replace(client.baseUrl,
|
|
371
|
+
path: url.replace(client.baseUrl, ""),
|
|
240
372
|
method: method.toUpperCase(),
|
|
241
373
|
};
|
|
242
374
|
if (requestBody !== undefined) {
|
|
@@ -249,10 +381,10 @@ export async function makeRequest(client, method, path, options = {}) {
|
|
|
249
381
|
// Build fetch options
|
|
250
382
|
const headers = {};
|
|
251
383
|
if (!noAuth) {
|
|
252
|
-
headers[
|
|
384
|
+
headers["Authorization"] = `Bearer ${client.token}`;
|
|
253
385
|
}
|
|
254
386
|
if (!formData) {
|
|
255
|
-
headers[
|
|
387
|
+
headers["Content-Type"] = "application/json";
|
|
256
388
|
}
|
|
257
389
|
|
|
258
390
|
const fetchOptions = { method, headers };
|
|
@@ -262,14 +394,21 @@ export async function makeRequest(client, method, path, options = {}) {
|
|
|
262
394
|
|
|
263
395
|
// A fresh AbortSignal.timeout per attempt: the signal fires once and stays
|
|
264
396
|
// aborted, so reusing it would make every retry fail instantly.
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
397
|
+
const timeoutMs = timeout !== undefined ? timeout : client.timeout;
|
|
398
|
+
const send = () => {
|
|
399
|
+
if (onUploadProgress && typeof XMLHttpRequest !== "undefined") {
|
|
400
|
+
return xhrSend(url, fetchOptions, {
|
|
401
|
+
onUploadProgress,
|
|
402
|
+
timeout: timeoutMs,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
const signal = timeoutSignal(timeoutMs);
|
|
406
|
+
return fetch(url, signal ? { ...fetchOptions, signal } : fetchOptions);
|
|
268
407
|
};
|
|
269
408
|
|
|
270
409
|
try {
|
|
271
410
|
const response = await retryWhileBusy(async () => {
|
|
272
|
-
const res = await
|
|
411
|
+
const res = await send();
|
|
273
412
|
// Surface a 503 as a throw so retryWhileBusy can see it; other failures
|
|
274
413
|
// are re-thrown from here and handled by the caller below.
|
|
275
414
|
if (res.status === 503) {
|
|
@@ -279,13 +418,26 @@ export async function makeRequest(client, method, path, options = {}) {
|
|
|
279
418
|
});
|
|
280
419
|
|
|
281
420
|
if (!response.ok) {
|
|
282
|
-
const error = makeHttpError(
|
|
421
|
+
const error = makeHttpError(
|
|
422
|
+
response,
|
|
423
|
+
await parseErrorBody(response),
|
|
424
|
+
url,
|
|
425
|
+
method,
|
|
426
|
+
);
|
|
283
427
|
// 401 means the token is missing/expired/invalid. Fire the app's auth-error
|
|
284
428
|
// handler once (it discards the token and routes back to login). 403
|
|
285
429
|
// (forbidden — authenticated but not permitted) deliberately does NOT.
|
|
286
|
-
if (
|
|
430
|
+
if (
|
|
431
|
+
response.status === 401 &&
|
|
432
|
+
typeof client.onAuthError === "function" &&
|
|
433
|
+
!client._authErrorFired
|
|
434
|
+
) {
|
|
287
435
|
client._authErrorFired = true;
|
|
288
|
-
try {
|
|
436
|
+
try {
|
|
437
|
+
client.onAuthError(error);
|
|
438
|
+
} catch (_) {
|
|
439
|
+
/* handler must not mask the original error */
|
|
440
|
+
}
|
|
289
441
|
}
|
|
290
442
|
throw error;
|
|
291
443
|
}
|
|
@@ -297,10 +449,12 @@ export async function makeRequest(client, method, path, options = {}) {
|
|
|
297
449
|
}
|
|
298
450
|
|
|
299
451
|
// JSON or text response
|
|
300
|
-
const contentType = response.headers.get(
|
|
301
|
-
if (contentType && contentType.includes(
|
|
452
|
+
const contentType = response.headers.get("content-type");
|
|
453
|
+
if (contentType && contentType.includes("application/json")) {
|
|
302
454
|
const data = await response.json();
|
|
303
|
-
extractDocumentVersions(client, response.headers, data
|
|
455
|
+
extractDocumentVersions(client, response.headers, data, {
|
|
456
|
+
historical: /[?&]as-of=/.test(url),
|
|
457
|
+
});
|
|
304
458
|
if (skipResponseTransform) {
|
|
305
459
|
return data;
|
|
306
460
|
}
|