@formfeed/sdk 0.1.0 → 0.3.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/README.md +21 -0
- package/index.cjs +231 -14
- package/index.js +231 -14
- package/package.json +1 -1
- package/types/index.d.cts +1 -1
- package/types/index.d.ts +1 -1
- package/types/lib/client.d.cts +527 -23
- package/types/lib/client.d.ts +527 -23
package/README.md
CHANGED
|
@@ -43,6 +43,27 @@ const job = await client.renders.batch({
|
|
|
43
43
|
const finished = await client.jobs.waitFor(job.id);
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
## Word and PowerPoint
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { readFile } from 'node:fs/promises';
|
|
50
|
+
|
|
51
|
+
// a template is a .docx or .pptx with tags such as {{ customer.name }} in its text
|
|
52
|
+
await client.templates.create({
|
|
53
|
+
name: 'Offer',
|
|
54
|
+
slug: 'offer',
|
|
55
|
+
kind: 'docx',
|
|
56
|
+
engine: 'jinja2',
|
|
57
|
+
file: { data: await readFile('offer.docx'), name: 'offer.docx' },
|
|
58
|
+
publish: true,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const filled = await client.renders.create({ template: 'offer', output: 'docx', data }); // or output: 'pdf'
|
|
62
|
+
|
|
63
|
+
// any office document to PDF (Word, Excel, PowerPoint, OpenDocument, RTF)
|
|
64
|
+
const pdf = await client.pdf.convert({ file: { data: await readFile('report.xlsx'), name: 'report.xlsx' } }, { single_page_sheets: true });
|
|
65
|
+
```
|
|
66
|
+
|
|
46
67
|
## Webhooks
|
|
47
68
|
|
|
48
69
|
```ts
|
package/index.cjs
CHANGED
|
@@ -28,6 +28,22 @@ __export(index_exports, {
|
|
|
28
28
|
module.exports = __toCommonJS(index_exports);
|
|
29
29
|
|
|
30
30
|
// packages/sdk-ts/src/lib/client.ts
|
|
31
|
+
var isOfficeInput = (input) => "file" in input;
|
|
32
|
+
function blobOf(file) {
|
|
33
|
+
const blob = file.data instanceof Blob ? file.data : new Blob([file.data]);
|
|
34
|
+
const own = typeof File !== "undefined" && file.data instanceof File ? file.data.name : void 0;
|
|
35
|
+
return { blob, name: file.name ?? own ?? "document" };
|
|
36
|
+
}
|
|
37
|
+
function multipart(fields, file) {
|
|
38
|
+
const form = new FormData();
|
|
39
|
+
const { blob, name } = blobOf(file);
|
|
40
|
+
form.set("file", blob, name);
|
|
41
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
42
|
+
if (value === void 0) continue;
|
|
43
|
+
form.set(key, typeof value === "string" ? value : typeof value === "object" ? JSON.stringify(value) : String(value));
|
|
44
|
+
}
|
|
45
|
+
return form;
|
|
46
|
+
}
|
|
31
47
|
var FormfeedError = class extends Error {
|
|
32
48
|
constructor(code, message, status, problem = null, requestId = null) {
|
|
33
49
|
super(message);
|
|
@@ -54,6 +70,8 @@ function randomKey() {
|
|
|
54
70
|
if (c?.randomUUID) return c.randomUUID();
|
|
55
71
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
56
72
|
}
|
|
73
|
+
var withKey = (options) => ({ ...options, idempotencyKey: options.idempotencyKey ?? randomKey() });
|
|
74
|
+
var idOf = (source) => typeof source === "string" ? source : source.id;
|
|
57
75
|
var Formfeed = class {
|
|
58
76
|
baseUrl;
|
|
59
77
|
apiKey;
|
|
@@ -117,11 +135,96 @@ var Formfeed = class {
|
|
|
117
135
|
},
|
|
118
136
|
/** Removes the stored files of a render before they expire. Needs the `file:delete` scope. */
|
|
119
137
|
deleteOutputs: (id, options = {}) => this.request("DELETE", `/renders/${encodeURIComponent(id)}/outputs`, void 0, options),
|
|
138
|
+
/**
|
|
139
|
+
* The stored request of a render, to render it again. Needs `render:input`; throws
|
|
140
|
+
* `render_input_expired` (410) when the workspace keeps no requests or the period ended.
|
|
141
|
+
*/
|
|
142
|
+
input: (id, options = {}) => this.request("GET", `/renders/${encodeURIComponent(id)}/input`, void 0, options),
|
|
120
143
|
batch: (request, options = {}) => this.request("POST", "/renders/batch", request, {
|
|
121
144
|
...options,
|
|
122
145
|
idempotencyKey: options.idempotencyKey ?? randomKey()
|
|
123
146
|
})
|
|
124
147
|
};
|
|
148
|
+
/**
|
|
149
|
+
* PDF tools on the outputs of earlier renders (a `Render` or its id) and on PDFs of the file library
|
|
150
|
+
* (a `LibraryFile`, its id or its name). merge, protect and watermark return a new render and cost
|
|
151
|
+
* 0.5 units each on live keys; info is free.
|
|
152
|
+
*/
|
|
153
|
+
pdf = {
|
|
154
|
+
merge: (sources, output = {}, options = {}) => this.request("POST", "/pdf/merge", { ...output, sources: sources.map(idOf) }, withKey(options)),
|
|
155
|
+
protect: (source, protect, options = {}) => this.request("POST", "/pdf/protect", { ...protect, source: idOf(source) }, withKey(options)),
|
|
156
|
+
watermark: (source, watermark, options = {}) => this.request("POST", "/pdf/watermark", { ...watermark, source: idOf(source) }, withKey(options)),
|
|
157
|
+
info: (source, options = {}) => this.request("POST", "/pdf/info", { source: idOf(source) }, options),
|
|
158
|
+
/**
|
|
159
|
+
* Converts an office document to a PDF render (Starter plan and above): an uploaded file (Word,
|
|
160
|
+
* Excel, PowerPoint, OpenDocument, RTF or HTML, up to 20 MB, not kept), or the output of a Word or
|
|
161
|
+
* PowerPoint template render. Costs follow the PDF rule. A busy converter answers 503, which is
|
|
162
|
+
* retried like any other.
|
|
163
|
+
*/
|
|
164
|
+
convert: (source, convert = {}, options = {}) => {
|
|
165
|
+
if (typeof source === "object" && "file" in source)
|
|
166
|
+
return this.request("POST", "/pdf/convert", multipart({ ...convert }, source.file), withKey(options));
|
|
167
|
+
return this.request("POST", "/pdf/convert", { ...convert, source: idOf(source) }, withKey(options));
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* The workspace file library (`/files`): images and PDFs a template references by name with
|
|
172
|
+
* `asset('logo.png')`, and the sources of image watermarks and merges. Needs `file:read`,
|
|
173
|
+
* `file:write` or `file:delete`. Files are served without a signature, so keep confidential
|
|
174
|
+
* documents out of it.
|
|
175
|
+
*/
|
|
176
|
+
files = {
|
|
177
|
+
/** Uploads a file; the same name replaces the existing file in place and keeps its URL. */
|
|
178
|
+
upload: (file, options = {}) => {
|
|
179
|
+
const blob = file.data instanceof Blob ? file.data : new Blob([file.data], file.contentType ? { type: file.contentType } : {});
|
|
180
|
+
const form = new FormData();
|
|
181
|
+
form.set("file", blob, file.name);
|
|
182
|
+
form.set("name", file.name);
|
|
183
|
+
return this.request("POST", "/files", form, options);
|
|
184
|
+
},
|
|
185
|
+
/** Page of files, newest first. */
|
|
186
|
+
list: (query = {}, options = {}) => {
|
|
187
|
+
const params = new URLSearchParams();
|
|
188
|
+
for (const [k, v] of Object.entries(query)) if (v !== void 0 && v !== null && v !== "") params.set(k, String(v));
|
|
189
|
+
const qs = params.toString();
|
|
190
|
+
return this.request("GET", `/files${qs ? `?${qs}` : ""}`, void 0, options);
|
|
191
|
+
},
|
|
192
|
+
/** Every file matching the query, following the cursor. */
|
|
193
|
+
all: async (query = {}, options = {}) => {
|
|
194
|
+
const out = [];
|
|
195
|
+
let cursor = null;
|
|
196
|
+
do {
|
|
197
|
+
const page = await this.files.list({ ...query, cursor }, options);
|
|
198
|
+
out.push(...page.data);
|
|
199
|
+
cursor = page.next_cursor;
|
|
200
|
+
} while (cursor);
|
|
201
|
+
return out;
|
|
202
|
+
},
|
|
203
|
+
get: (id, options = {}) => this.request("GET", `/files/${encodeURIComponent(id)}`, void 0, options),
|
|
204
|
+
/** Removes the file and its bytes; templates that name it render a missing image afterwards. */
|
|
205
|
+
delete: (file, options = {}) => this.request("DELETE", `/files/${encodeURIComponent(idOf(file))}`, void 0, options)
|
|
206
|
+
};
|
|
207
|
+
/** The organisation's brand kit, read-only through the API (it is edited in the app). Needs `template:read`. */
|
|
208
|
+
brand = {
|
|
209
|
+
get: (options = {}) => this.request("GET", "/brand", void 0, options)
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* Shared partials of the organisation. Renders use the current source, so a change reaches every
|
|
213
|
+
* template that includes the partial without a new version. Needs `template:read` or `template:write`.
|
|
214
|
+
*/
|
|
215
|
+
partials = {
|
|
216
|
+
/** Every shared partial, without its source. */
|
|
217
|
+
list: async (options = {}) => (await this.request("GET", "/partials", void 0, options)).data,
|
|
218
|
+
/** One partial with its source. */
|
|
219
|
+
get: (name, options = {}) => this.request("GET", `/partials/${encodeURIComponent(name)}`, void 0, options),
|
|
220
|
+
/** Creates or replaces a partial; pass `base_version` to fail with 409 when it changed since you read it. */
|
|
221
|
+
put: async (name, input, options = {}) => {
|
|
222
|
+
const { status, body } = await this.send("PUT", `/partials/${encodeURIComponent(name)}`, input, options);
|
|
223
|
+
return { partial: body, created: status === 201 };
|
|
224
|
+
},
|
|
225
|
+
/** Removes a partial; templates that still include it fail to render afterwards. */
|
|
226
|
+
delete: (name, options = {}) => this.request("DELETE", `/partials/${encodeURIComponent(name)}`, void 0, options)
|
|
227
|
+
};
|
|
125
228
|
jobs = {
|
|
126
229
|
get: (id, options = {}) => this.request("GET", `/jobs/${encodeURIComponent(id)}`, void 0, options),
|
|
127
230
|
waitFor: async (id, options = {}) => {
|
|
@@ -141,7 +244,20 @@ var Formfeed = class {
|
|
|
141
244
|
create: (input, options = {}) => this.request("POST", "/webhooks", input, options),
|
|
142
245
|
update: (id, patch, options = {}) => this.request("PUT", `/webhooks/${encodeURIComponent(id)}`, patch, options),
|
|
143
246
|
delete: (id, options = {}) => this.request("DELETE", `/webhooks/${encodeURIComponent(id)}`, void 0, options),
|
|
144
|
-
test: (id, options = {}) => this.request("POST", `/webhooks/${encodeURIComponent(id)}/test`, void 0, options)
|
|
247
|
+
test: (id, options = {}) => this.request("POST", `/webhooks/${encodeURIComponent(id)}/test`, void 0, options),
|
|
248
|
+
/**
|
|
249
|
+
* Sends a stored event (`evt_…`) again as a new delivery to a registered endpoint (`webhook:manage`)
|
|
250
|
+
* or a running listen session (`webhook:listen`).
|
|
251
|
+
*/
|
|
252
|
+
resend: (eventId, endpointId, options = {}) => this.request("POST", `/webhooks/events/${encodeURIComponent(eventId)}/resend`, { endpoint_id: endpointId }, options),
|
|
253
|
+
/**
|
|
254
|
+
* Listen sessions, what `formfeed listen` uses. Needs `webhook:listen`. Starting a session ends the
|
|
255
|
+
* previous one of the same key; the WebSocket itself is up to the caller.
|
|
256
|
+
*/
|
|
257
|
+
listen: {
|
|
258
|
+
start: (input = {}, options = {}) => this.request("POST", "/webhooks/listen", input, options),
|
|
259
|
+
end: (id, options = {}) => this.request("DELETE", `/webhooks/listen/${encodeURIComponent(id)}`, void 0, options)
|
|
260
|
+
}
|
|
145
261
|
};
|
|
146
262
|
templates = {
|
|
147
263
|
list: (query = {}, options = {}) => {
|
|
@@ -162,32 +278,127 @@ var Formfeed = class {
|
|
|
162
278
|
return out;
|
|
163
279
|
},
|
|
164
280
|
get: (idOrSlug, options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}`, void 0, options),
|
|
165
|
-
|
|
281
|
+
/** A Word or PowerPoint template (`kind` `docx` or `pptx`) is created from its `file`, sent as multipart. */
|
|
282
|
+
create: (input, options = {}) => {
|
|
283
|
+
if (isOfficeInput(input) && input.file) {
|
|
284
|
+
const { file, ...fields } = input;
|
|
285
|
+
return this.request("POST", "/templates", multipart(fields, file), options);
|
|
286
|
+
}
|
|
287
|
+
return this.request("POST", "/templates", input, options);
|
|
288
|
+
},
|
|
166
289
|
update: (idOrSlug, patch, options = {}) => this.request("PUT", `/templates/${encodeURIComponent(idOrSlug)}`, patch, options),
|
|
167
290
|
archive: (idOrSlug, options = {}) => this.request("DELETE", `/templates/${encodeURIComponent(idOrSlug)}`, void 0, options),
|
|
168
291
|
versions: {
|
|
169
292
|
list: async (idOrSlug, options = {}) => (await this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/versions`, void 0, options)).data,
|
|
170
|
-
/** `which`: 'published', 'latest' or a version
|
|
171
|
-
get: (idOrSlug, which = "published", options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/versions/${which}`, void 0, options),
|
|
172
|
-
|
|
173
|
-
|
|
293
|
+
/** `which`: 'published', 'latest', a version number or a channel name (its main version). Carries the files. */
|
|
294
|
+
get: (idOrSlug, which = "published", options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/versions/${encodeURIComponent(String(which))}`, void 0, options),
|
|
295
|
+
/** For a Word or PowerPoint template, pass `file` to replace the document; without it the latest file is kept. */
|
|
296
|
+
create: (idOrSlug, input, options = {}) => {
|
|
297
|
+
const path = `/templates/${encodeURIComponent(idOrSlug)}/versions`;
|
|
298
|
+
if (isOfficeInput(input)) {
|
|
299
|
+
const { file, ...fields } = input;
|
|
300
|
+
if (file) return this.request("POST", path, multipart(fields, file), options);
|
|
301
|
+
return this.request("POST", path, fields, options);
|
|
302
|
+
}
|
|
303
|
+
return this.request("POST", path, input, options);
|
|
304
|
+
},
|
|
305
|
+
/** The Word or PowerPoint file of a version (`which` as for `get`); needs `template:read`. */
|
|
306
|
+
file: async (idOrSlug, which = "published", options = {}) => {
|
|
307
|
+
const path = `/templates/${encodeURIComponent(idOrSlug)}/versions/${encodeURIComponent(String(which))}/file`;
|
|
308
|
+
return (await this.send("GET", path, void 0, options, "bytes")).body;
|
|
309
|
+
},
|
|
310
|
+
/**
|
|
311
|
+
* Publishes a version. When both versions store a data schema and the new one breaks callers of the
|
|
312
|
+
* published one, the API refuses with `schema_breaking_change` unless `allowBreaking` is set.
|
|
313
|
+
*/
|
|
314
|
+
publish: (idOrSlug, number, options = {}) => {
|
|
315
|
+
const { allowBreaking, ...rest } = options;
|
|
316
|
+
return this.request(
|
|
317
|
+
"POST",
|
|
318
|
+
`/templates/${encodeURIComponent(idOrSlug)}/versions/${number}/publish`,
|
|
319
|
+
allowBreaking ? { allow_breaking: true } : void 0,
|
|
320
|
+
rest
|
|
321
|
+
);
|
|
322
|
+
}
|
|
174
323
|
},
|
|
175
|
-
|
|
324
|
+
/**
|
|
325
|
+
* Release channels (`published` and named ones such as `staging`): a render with `version: 'staging'`
|
|
326
|
+
* uses the version the channel points at. Moves reach renders within a minute.
|
|
327
|
+
*/
|
|
328
|
+
channels: {
|
|
329
|
+
list: (idOrSlug, options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/channels`, void 0, options),
|
|
330
|
+
/** Creates or moves a channel; on `published` this publishes. */
|
|
331
|
+
set: (idOrSlug, name, move, options = {}) => {
|
|
332
|
+
const { allowBreaking, ...rest } = options;
|
|
333
|
+
return this.request(
|
|
334
|
+
"PUT",
|
|
335
|
+
`/templates/${encodeURIComponent(idOrSlug)}/channels/${encodeURIComponent(name)}`,
|
|
336
|
+
{ ...move, ...allowBreaking ? { allow_breaking: true } : {} },
|
|
337
|
+
rest
|
|
338
|
+
);
|
|
339
|
+
},
|
|
340
|
+
/** The canary becomes the main version. */
|
|
341
|
+
promote: (idOrSlug, name, options = {}) => this.channelAction(idOrSlug, name, "promote", options),
|
|
342
|
+
/** Back to the version before the last move. */
|
|
343
|
+
rollback: (idOrSlug, name, options = {}) => this.channelAction(idOrSlug, name, "rollback", options),
|
|
344
|
+
/** Refused with `channel_in_use` while renders of the last hour used the channel, unless `force`. */
|
|
345
|
+
delete: (idOrSlug, name, options = {}) => {
|
|
346
|
+
const { force, ...rest } = options;
|
|
347
|
+
return this.request(
|
|
348
|
+
"DELETE",
|
|
349
|
+
`/templates/${encodeURIComponent(idOrSlug)}/channels/${encodeURIComponent(name)}${force ? "?force=true" : ""}`,
|
|
350
|
+
void 0,
|
|
351
|
+
rest
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
schema: (idOrSlug, options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/schema`, void 0, options),
|
|
356
|
+
/**
|
|
357
|
+
* Checks a version with data without rendering: syntax, header and footer, missing partials,
|
|
358
|
+
* runtime errors and the data against the stored schema. Free. Without `data`, the version's
|
|
359
|
+
* sample data is checked.
|
|
360
|
+
*/
|
|
361
|
+
validate: (idOrSlug, input = {}, options = {}) => this.request("POST", `/templates/${encodeURIComponent(idOrSlug)}/validate`, input, options)
|
|
176
362
|
};
|
|
177
363
|
account = {
|
|
178
364
|
get: (options = {}) => this.request("GET", "/account", void 0, options),
|
|
179
365
|
/** Units of the current period, or of `YYYY-MM`, with a daily series and a breakdown per template. */
|
|
180
366
|
usage: (period = "current", options = {}) => this.request("GET", `/usage?period=${encodeURIComponent(period)}`, void 0, options)
|
|
181
367
|
};
|
|
368
|
+
/** Workspaces of the key's organisation. Needs `workspace:delete`, which no key has by default. */
|
|
369
|
+
workspaces = {
|
|
370
|
+
/**
|
|
371
|
+
* Deletes a workspace: it leaves the app and the API at once, its keys are revoked and its content
|
|
372
|
+
* is removed after 30 days. The organisation's last workspace is refused with `last_workspace`.
|
|
373
|
+
*/
|
|
374
|
+
delete: (id, options = {}) => this.request("DELETE", `/workspaces/${encodeURIComponent(id)}`, void 0, options)
|
|
375
|
+
};
|
|
376
|
+
channelAction(idOrSlug, name, action, options) {
|
|
377
|
+
const { allowBreaking, ...rest } = options;
|
|
378
|
+
return this.request(
|
|
379
|
+
"POST",
|
|
380
|
+
`/templates/${encodeURIComponent(idOrSlug)}/channels/${encodeURIComponent(name)}/${action}`,
|
|
381
|
+
allowBreaking ? { allow_breaking: true } : void 0,
|
|
382
|
+
rest
|
|
383
|
+
);
|
|
384
|
+
}
|
|
182
385
|
/** Raw request with auth, retries and problem mapping; use it for endpoints without a helper. */
|
|
183
386
|
async request(method, path, body, options = {}) {
|
|
387
|
+
return (await this.send(method, path, body, options)).body;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* `request` with the HTTP status, for endpoints whose answer depends on it (201 created, 200 updated).
|
|
391
|
+
* `bytes` returns a successful body as bytes (file downloads); problems are JSON either way.
|
|
392
|
+
*/
|
|
393
|
+
async send(method, path, body, options = {}, as = "json") {
|
|
184
394
|
const url = `${this.baseUrl}${path}`;
|
|
185
395
|
const headers = {
|
|
186
396
|
authorization: `Bearer ${this.apiKey}`,
|
|
187
|
-
accept: "application/json",
|
|
188
|
-
"user-agent": "formfeed-sdk-ts/0.
|
|
397
|
+
accept: as === "bytes" ? "*/*" : "application/json",
|
|
398
|
+
"user-agent": "formfeed-sdk-ts/0.3"
|
|
189
399
|
};
|
|
190
|
-
|
|
400
|
+
const isForm = typeof FormData !== "undefined" && body instanceof FormData;
|
|
401
|
+
if (body !== void 0 && !isForm) headers["content-type"] = "application/json";
|
|
191
402
|
if (options.idempotencyKey) headers["idempotency-key"] = options.idempotencyKey;
|
|
192
403
|
let attempt = 0;
|
|
193
404
|
for (; ; ) {
|
|
@@ -199,7 +410,7 @@ var Formfeed = class {
|
|
|
199
410
|
res = await this.fetchImpl(url, {
|
|
200
411
|
method,
|
|
201
412
|
headers,
|
|
202
|
-
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
413
|
+
body: body === void 0 ? void 0 : isForm ? body : JSON.stringify(body),
|
|
203
414
|
signal: controller.signal
|
|
204
415
|
});
|
|
205
416
|
} catch (e) {
|
|
@@ -217,9 +428,15 @@ var Formfeed = class {
|
|
|
217
428
|
await sleep(this.backoff(attempt, res.headers.get("retry-after")), options.signal);
|
|
218
429
|
continue;
|
|
219
430
|
}
|
|
220
|
-
if (res.status === 204) return void 0;
|
|
431
|
+
if (res.status === 204) return { status: 204, body: void 0 };
|
|
432
|
+
if (as === "bytes" && res.ok) return { status: res.status, body: new Uint8Array(await res.arrayBuffer()) };
|
|
221
433
|
const text = await res.text();
|
|
222
|
-
|
|
434
|
+
let json = null;
|
|
435
|
+
try {
|
|
436
|
+
json = text ? JSON.parse(text) : null;
|
|
437
|
+
} catch {
|
|
438
|
+
if (res.ok) throw new FormfeedError("invalid_response", `the API answered HTTP ${res.status} with a body that is not JSON`, res.status);
|
|
439
|
+
}
|
|
223
440
|
if (!res.ok) {
|
|
224
441
|
const problem = json ?? {};
|
|
225
442
|
throw new FormfeedError(
|
|
@@ -230,7 +447,7 @@ var Formfeed = class {
|
|
|
230
447
|
res.headers.get("x-request-id")
|
|
231
448
|
);
|
|
232
449
|
}
|
|
233
|
-
return json;
|
|
450
|
+
return { status: res.status, body: json };
|
|
234
451
|
}
|
|
235
452
|
}
|
|
236
453
|
backoff(attempt, retryAfter) {
|