@formfeed/sdk 0.3.11 → 0.3.13
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 +32 -7
- package/index.cjs +323 -62
- package/index.js +323 -62
- 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 +111 -0
- package/types/lib/client.d.ts +111 -0
package/README.md
CHANGED
|
@@ -18,11 +18,19 @@ npm install @formfeed/sdk
|
|
|
18
18
|
```ts
|
|
19
19
|
import { Formfeed } from '@formfeed/sdk';
|
|
20
20
|
|
|
21
|
-
const client = new Formfeed({
|
|
21
|
+
const client = new Formfeed({
|
|
22
|
+
apiKey: process.env.FORMFEED_API_KEY!,
|
|
23
|
+
region: 'eu',
|
|
24
|
+
});
|
|
22
25
|
|
|
23
26
|
const render = await client.renders.create({
|
|
24
27
|
template: 'invoice-de',
|
|
25
|
-
data: {
|
|
28
|
+
data: {
|
|
29
|
+
invoice: {
|
|
30
|
+
number: '2026-0042',
|
|
31
|
+
lines: [{ description: 'Consulting', qty: 8, price: 120 }],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
26
34
|
});
|
|
27
35
|
console.log(render.download_url, render.page_count);
|
|
28
36
|
|
|
@@ -32,7 +40,11 @@ const pdf = await client.renders.download(render); // the bytes
|
|
|
32
40
|
## Async renders and batches
|
|
33
41
|
|
|
34
42
|
```ts
|
|
35
|
-
const queued = await client.renders.create({
|
|
43
|
+
const queued = await client.renders.create({
|
|
44
|
+
template: 'invoice-de',
|
|
45
|
+
data,
|
|
46
|
+
mode: 'async',
|
|
47
|
+
});
|
|
36
48
|
const done = await client.renders.waitFor(queued.id);
|
|
37
49
|
|
|
38
50
|
const job = await client.renders.batch({
|
|
@@ -58,14 +70,23 @@ await client.templates.create({
|
|
|
58
70
|
publish: true,
|
|
59
71
|
});
|
|
60
72
|
|
|
61
|
-
const filled = await client.renders.create({
|
|
73
|
+
const filled = await client.renders.create({
|
|
74
|
+
template: 'offer',
|
|
75
|
+
output: 'docx',
|
|
76
|
+
data,
|
|
77
|
+
}); // or output: 'pdf'
|
|
62
78
|
|
|
63
79
|
// a new version with a new document; without `file` the latest document is kept
|
|
64
|
-
await client.templates.versions.create('offer', {
|
|
80
|
+
await client.templates.versions.create('offer', {
|
|
81
|
+
file: { data: await readFile('offer-v2.docx'), name: 'offer.docx' },
|
|
82
|
+
});
|
|
65
83
|
const docx = await client.templates.versions.file('offer', 'latest'); // the document's bytes
|
|
66
84
|
|
|
67
85
|
// any office document to PDF (Word, Excel, PowerPoint, OpenDocument, RTF)
|
|
68
|
-
const pdf = await client.pdf.convert(
|
|
86
|
+
const pdf = await client.pdf.convert(
|
|
87
|
+
{ file: { data: await readFile('report.xlsx'), name: 'report.xlsx' } },
|
|
88
|
+
{ single_page_sheets: true },
|
|
89
|
+
);
|
|
69
90
|
```
|
|
70
91
|
|
|
71
92
|
## Webhooks
|
|
@@ -74,7 +95,11 @@ const pdf = await client.pdf.convert({ file: { data: await readFile('report.xlsx
|
|
|
74
95
|
import { parseWebhookEvent } from '@formfeed/sdk';
|
|
75
96
|
|
|
76
97
|
// pass the raw request body, not re-serialised JSON; throws when the signature does not match
|
|
77
|
-
const event = await parseWebhookEvent(
|
|
98
|
+
const event = await parseWebhookEvent(
|
|
99
|
+
secret,
|
|
100
|
+
request.headers.get('webhook-signature'),
|
|
101
|
+
rawBody,
|
|
102
|
+
);
|
|
78
103
|
if (event.type === 'render.completed') console.log(event.data); // the render object
|
|
79
104
|
```
|
|
80
105
|
|
package/index.cjs
CHANGED
|
@@ -40,7 +40,10 @@ function multipart(fields, file) {
|
|
|
40
40
|
form.set("file", blob, name);
|
|
41
41
|
for (const [key, value] of Object.entries(fields)) {
|
|
42
42
|
if (value === void 0) continue;
|
|
43
|
-
form.set(
|
|
43
|
+
form.set(
|
|
44
|
+
key,
|
|
45
|
+
typeof value === "string" ? value : typeof value === "object" ? JSON.stringify(value) : String(value)
|
|
46
|
+
);
|
|
44
47
|
}
|
|
45
48
|
return form;
|
|
46
49
|
}
|
|
@@ -70,7 +73,10 @@ function randomKey() {
|
|
|
70
73
|
if (c?.randomUUID) return c.randomUUID();
|
|
71
74
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
72
75
|
}
|
|
73
|
-
var withKey = (options) => ({
|
|
76
|
+
var withKey = (options) => ({
|
|
77
|
+
...options,
|
|
78
|
+
idempotencyKey: options.idempotencyKey ?? randomKey()
|
|
79
|
+
});
|
|
74
80
|
var idOf = (source) => typeof source === "string" ? source : source.id;
|
|
75
81
|
var Formfeed = class {
|
|
76
82
|
baseUrl;
|
|
@@ -80,14 +86,28 @@ var Formfeed = class {
|
|
|
80
86
|
timeoutMs;
|
|
81
87
|
extraHeaders;
|
|
82
88
|
constructor(options) {
|
|
83
|
-
if (!options.apiKey)
|
|
89
|
+
if (!options.apiKey)
|
|
90
|
+
throw new FormfeedError("invalid_request", "apiKey is required", 0);
|
|
84
91
|
this.apiKey = options.apiKey;
|
|
85
|
-
this.baseUrl = (options.baseUrl ?? hosts[options.region ?? "eu"]).replace(
|
|
92
|
+
this.baseUrl = (options.baseUrl ?? hosts[options.region ?? "eu"]).replace(
|
|
93
|
+
/\/$/,
|
|
94
|
+
""
|
|
95
|
+
);
|
|
86
96
|
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
87
97
|
this.maxRetries = options.maxRetries ?? 3;
|
|
88
98
|
this.timeoutMs = options.timeoutMs ?? 12e4;
|
|
89
|
-
this.extraHeaders = Object.fromEntries(
|
|
90
|
-
|
|
99
|
+
this.extraHeaders = Object.fromEntries(
|
|
100
|
+
Object.entries(options.headers ?? {}).map(([name, value]) => [
|
|
101
|
+
name.toLowerCase(),
|
|
102
|
+
value
|
|
103
|
+
])
|
|
104
|
+
);
|
|
105
|
+
if (!this.fetchImpl)
|
|
106
|
+
throw new FormfeedError(
|
|
107
|
+
"invalid_request",
|
|
108
|
+
"fetch is not available; pass options.fetch",
|
|
109
|
+
0
|
|
110
|
+
);
|
|
91
111
|
}
|
|
92
112
|
renders = {
|
|
93
113
|
/** Renders a template, HTML or URL. Sync by default; `mode: 'async'` returns a queued render. */
|
|
@@ -95,16 +115,26 @@ var Formfeed = class {
|
|
|
95
115
|
...options,
|
|
96
116
|
idempotencyKey: options.idempotencyKey ?? randomKey()
|
|
97
117
|
}),
|
|
98
|
-
get: (id, options = {}) => this.request(
|
|
118
|
+
get: (id, options = {}) => this.request(
|
|
119
|
+
"GET",
|
|
120
|
+
`/renders/${encodeURIComponent(id)}`,
|
|
121
|
+
void 0,
|
|
122
|
+
options
|
|
123
|
+
),
|
|
99
124
|
/** Polls until the render succeeded or failed (async renders). */
|
|
100
125
|
waitFor: async (id, options = {}) => {
|
|
101
126
|
const deadline = Date.now() + (options.timeoutMs ?? 12e4);
|
|
102
127
|
const interval = options.intervalMs ?? 1e3;
|
|
103
128
|
for (; ; ) {
|
|
104
129
|
const render = await this.renders.get(id, { signal: options.signal });
|
|
105
|
-
if (render.status === "succeeded" || render.status === "failed")
|
|
130
|
+
if (render.status === "succeeded" || render.status === "failed")
|
|
131
|
+
return render;
|
|
106
132
|
if (Date.now() + interval > deadline)
|
|
107
|
-
throw new FormfeedError(
|
|
133
|
+
throw new FormfeedError(
|
|
134
|
+
"timeout",
|
|
135
|
+
`render ${id} did not finish within the wait time`,
|
|
136
|
+
0
|
|
137
|
+
);
|
|
108
138
|
await sleep(interval, options.signal);
|
|
109
139
|
}
|
|
110
140
|
},
|
|
@@ -112,17 +142,34 @@ var Formfeed = class {
|
|
|
112
142
|
download: async (render, options = {}) => {
|
|
113
143
|
const target = typeof render === "string" ? await this.renders.get(render, options) : render;
|
|
114
144
|
if (!target.download_url)
|
|
115
|
-
throw new FormfeedError(
|
|
116
|
-
|
|
117
|
-
|
|
145
|
+
throw new FormfeedError(
|
|
146
|
+
"not_ready",
|
|
147
|
+
`render ${target.id} has no output (${target.status})`,
|
|
148
|
+
0
|
|
149
|
+
);
|
|
150
|
+
const res = await this.fetchImpl(target.download_url, {
|
|
151
|
+
signal: options.signal
|
|
152
|
+
});
|
|
153
|
+
if (!res.ok)
|
|
154
|
+
throw new FormfeedError(
|
|
155
|
+
"download_failed",
|
|
156
|
+
`download answered HTTP ${res.status}`,
|
|
157
|
+
res.status
|
|
158
|
+
);
|
|
118
159
|
return new Uint8Array(await res.arrayBuffer());
|
|
119
160
|
},
|
|
120
161
|
/** Page of renders, newest first. */
|
|
121
162
|
list: (query = {}, options = {}) => {
|
|
122
163
|
const params = new URLSearchParams();
|
|
123
|
-
for (const [k, v] of Object.entries(query))
|
|
164
|
+
for (const [k, v] of Object.entries(query))
|
|
165
|
+
if (v !== void 0 && v !== null && v !== "") params.set(k, String(v));
|
|
124
166
|
const qs = params.toString();
|
|
125
|
-
return this.request(
|
|
167
|
+
return this.request(
|
|
168
|
+
"GET",
|
|
169
|
+
`/renders${qs ? `?${qs}` : ""}`,
|
|
170
|
+
void 0,
|
|
171
|
+
options
|
|
172
|
+
);
|
|
126
173
|
},
|
|
127
174
|
/** Every render matching the query, following the cursor. */
|
|
128
175
|
all: async (query = {}, options = {}) => {
|
|
@@ -136,12 +183,22 @@ var Formfeed = class {
|
|
|
136
183
|
return out;
|
|
137
184
|
},
|
|
138
185
|
/** Removes the stored files of a render before they expire. Needs the `file:delete` scope. */
|
|
139
|
-
deleteOutputs: (id, options = {}) => this.request(
|
|
186
|
+
deleteOutputs: (id, options = {}) => this.request(
|
|
187
|
+
"DELETE",
|
|
188
|
+
`/renders/${encodeURIComponent(id)}/outputs`,
|
|
189
|
+
void 0,
|
|
190
|
+
options
|
|
191
|
+
),
|
|
140
192
|
/**
|
|
141
193
|
* The stored request of a render, to render it again. Needs `render:input`; throws
|
|
142
194
|
* `render_input_expired` (410) when the workspace keeps no requests or the period ended.
|
|
143
195
|
*/
|
|
144
|
-
input: (id, options = {}) => this.request(
|
|
196
|
+
input: (id, options = {}) => this.request(
|
|
197
|
+
"GET",
|
|
198
|
+
`/renders/${encodeURIComponent(id)}/input`,
|
|
199
|
+
void 0,
|
|
200
|
+
options
|
|
201
|
+
),
|
|
145
202
|
batch: (request, options = {}) => this.request("POST", "/renders/batch", request, {
|
|
146
203
|
...options,
|
|
147
204
|
idempotencyKey: options.idempotencyKey ?? randomKey()
|
|
@@ -153,10 +210,30 @@ var Formfeed = class {
|
|
|
153
210
|
* 0.5 units each on live keys; info is free.
|
|
154
211
|
*/
|
|
155
212
|
pdf = {
|
|
156
|
-
merge: (sources, output = {}, options = {}) => this.request(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
213
|
+
merge: (sources, output = {}, options = {}) => this.request(
|
|
214
|
+
"POST",
|
|
215
|
+
"/pdf/merge",
|
|
216
|
+
{ ...output, sources: sources.map(idOf) },
|
|
217
|
+
withKey(options)
|
|
218
|
+
),
|
|
219
|
+
protect: (source, protect, options = {}) => this.request(
|
|
220
|
+
"POST",
|
|
221
|
+
"/pdf/protect",
|
|
222
|
+
{ ...protect, source: idOf(source) },
|
|
223
|
+
withKey(options)
|
|
224
|
+
),
|
|
225
|
+
watermark: (source, watermark, options = {}) => this.request(
|
|
226
|
+
"POST",
|
|
227
|
+
"/pdf/watermark",
|
|
228
|
+
{ ...watermark, source: idOf(source) },
|
|
229
|
+
withKey(options)
|
|
230
|
+
),
|
|
231
|
+
info: (source, options = {}) => this.request(
|
|
232
|
+
"POST",
|
|
233
|
+
"/pdf/info",
|
|
234
|
+
{ source: idOf(source) },
|
|
235
|
+
options
|
|
236
|
+
),
|
|
160
237
|
/**
|
|
161
238
|
* Converts an office document to a PDF render (Starter plan and above): an uploaded file (Word,
|
|
162
239
|
* Excel, PowerPoint, OpenDocument, RTF or HTML, up to 20 MB, not kept), or the output of a Word or
|
|
@@ -165,8 +242,18 @@ var Formfeed = class {
|
|
|
165
242
|
*/
|
|
166
243
|
convert: (source, convert = {}, options = {}) => {
|
|
167
244
|
if (typeof source === "object" && "file" in source)
|
|
168
|
-
return this.request(
|
|
169
|
-
|
|
245
|
+
return this.request(
|
|
246
|
+
"POST",
|
|
247
|
+
"/pdf/convert",
|
|
248
|
+
multipart({ ...convert }, source.file),
|
|
249
|
+
withKey(options)
|
|
250
|
+
);
|
|
251
|
+
return this.request(
|
|
252
|
+
"POST",
|
|
253
|
+
"/pdf/convert",
|
|
254
|
+
{ ...convert, source: idOf(source) },
|
|
255
|
+
withKey(options)
|
|
256
|
+
);
|
|
170
257
|
}
|
|
171
258
|
};
|
|
172
259
|
/**
|
|
@@ -178,7 +265,10 @@ var Formfeed = class {
|
|
|
178
265
|
files = {
|
|
179
266
|
/** Uploads a file; the same name replaces the existing file in place and keeps its URL. */
|
|
180
267
|
upload: (file, options = {}) => {
|
|
181
|
-
const blob = file.data instanceof Blob ? file.data : new Blob(
|
|
268
|
+
const blob = file.data instanceof Blob ? file.data : new Blob(
|
|
269
|
+
[file.data],
|
|
270
|
+
file.contentType ? { type: file.contentType } : {}
|
|
271
|
+
);
|
|
182
272
|
const form = new FormData();
|
|
183
273
|
form.set("file", blob, file.name);
|
|
184
274
|
form.set("name", file.name);
|
|
@@ -187,9 +277,15 @@ var Formfeed = class {
|
|
|
187
277
|
/** Page of files, newest first. */
|
|
188
278
|
list: (query = {}, options = {}) => {
|
|
189
279
|
const params = new URLSearchParams();
|
|
190
|
-
for (const [k, v] of Object.entries(query))
|
|
280
|
+
for (const [k, v] of Object.entries(query))
|
|
281
|
+
if (v !== void 0 && v !== null && v !== "") params.set(k, String(v));
|
|
191
282
|
const qs = params.toString();
|
|
192
|
-
return this.request(
|
|
283
|
+
return this.request(
|
|
284
|
+
"GET",
|
|
285
|
+
`/files${qs ? `?${qs}` : ""}`,
|
|
286
|
+
void 0,
|
|
287
|
+
options
|
|
288
|
+
);
|
|
193
289
|
},
|
|
194
290
|
/** Every file matching the query, following the cursor. */
|
|
195
291
|
all: async (query = {}, options = {}) => {
|
|
@@ -202,9 +298,19 @@ var Formfeed = class {
|
|
|
202
298
|
} while (cursor);
|
|
203
299
|
return out;
|
|
204
300
|
},
|
|
205
|
-
get: (id, options = {}) => this.request(
|
|
301
|
+
get: (id, options = {}) => this.request(
|
|
302
|
+
"GET",
|
|
303
|
+
`/files/${encodeURIComponent(id)}`,
|
|
304
|
+
void 0,
|
|
305
|
+
options
|
|
306
|
+
),
|
|
206
307
|
/** Removes the file and its bytes; templates that name it render a missing image afterwards. */
|
|
207
|
-
delete: (file, options = {}) => this.request(
|
|
308
|
+
delete: (file, options = {}) => this.request(
|
|
309
|
+
"DELETE",
|
|
310
|
+
`/files/${encodeURIComponent(idOf(file))}`,
|
|
311
|
+
void 0,
|
|
312
|
+
options
|
|
313
|
+
)
|
|
208
314
|
};
|
|
209
315
|
/** The organisation's brand kit, read-only through the API (it is edited in the app). Needs `template:read`. */
|
|
210
316
|
brand = {
|
|
@@ -216,19 +322,44 @@ var Formfeed = class {
|
|
|
216
322
|
*/
|
|
217
323
|
partials = {
|
|
218
324
|
/** Every shared partial, without its source. */
|
|
219
|
-
list: async (options = {}) => (await this.request(
|
|
325
|
+
list: async (options = {}) => (await this.request(
|
|
326
|
+
"GET",
|
|
327
|
+
"/partials",
|
|
328
|
+
void 0,
|
|
329
|
+
options
|
|
330
|
+
)).data,
|
|
220
331
|
/** One partial with its source. */
|
|
221
|
-
get: (name, options = {}) => this.request(
|
|
332
|
+
get: (name, options = {}) => this.request(
|
|
333
|
+
"GET",
|
|
334
|
+
`/partials/${encodeURIComponent(name)}`,
|
|
335
|
+
void 0,
|
|
336
|
+
options
|
|
337
|
+
),
|
|
222
338
|
/** Creates or replaces a partial; pass `base_version` to fail with 409 when it changed since you read it. */
|
|
223
339
|
put: async (name, input, options = {}) => {
|
|
224
|
-
const { status, body } = await this.send(
|
|
340
|
+
const { status, body } = await this.send(
|
|
341
|
+
"PUT",
|
|
342
|
+
`/partials/${encodeURIComponent(name)}`,
|
|
343
|
+
input,
|
|
344
|
+
options
|
|
345
|
+
);
|
|
225
346
|
return { partial: body, created: status === 201 };
|
|
226
347
|
},
|
|
227
348
|
/** Removes a partial; templates that still include it fail to render afterwards. */
|
|
228
|
-
delete: (name, options = {}) => this.request(
|
|
349
|
+
delete: (name, options = {}) => this.request(
|
|
350
|
+
"DELETE",
|
|
351
|
+
`/partials/${encodeURIComponent(name)}`,
|
|
352
|
+
void 0,
|
|
353
|
+
options
|
|
354
|
+
)
|
|
229
355
|
};
|
|
230
356
|
jobs = {
|
|
231
|
-
get: (id, options = {}) => this.request(
|
|
357
|
+
get: (id, options = {}) => this.request(
|
|
358
|
+
"GET",
|
|
359
|
+
`/jobs/${encodeURIComponent(id)}`,
|
|
360
|
+
void 0,
|
|
361
|
+
options
|
|
362
|
+
),
|
|
232
363
|
waitFor: async (id, options = {}) => {
|
|
233
364
|
const deadline = Date.now() + (options.timeoutMs ?? 6e5);
|
|
234
365
|
const interval = options.intervalMs ?? 2e3;
|
|
@@ -236,37 +367,77 @@ var Formfeed = class {
|
|
|
236
367
|
const job = await this.jobs.get(id, { signal: options.signal });
|
|
237
368
|
if (job.status !== "queued" && job.status !== "processing") return job;
|
|
238
369
|
if (Date.now() + interval > deadline)
|
|
239
|
-
throw new FormfeedError(
|
|
370
|
+
throw new FormfeedError(
|
|
371
|
+
"timeout",
|
|
372
|
+
`job ${id} did not finish within the wait time`,
|
|
373
|
+
0
|
|
374
|
+
);
|
|
240
375
|
await sleep(interval, options.signal);
|
|
241
376
|
}
|
|
242
377
|
}
|
|
243
378
|
};
|
|
244
379
|
webhooks = {
|
|
245
|
-
list: async (options = {}) => (await this.request(
|
|
380
|
+
list: async (options = {}) => (await this.request(
|
|
381
|
+
"GET",
|
|
382
|
+
"/webhooks",
|
|
383
|
+
void 0,
|
|
384
|
+
options
|
|
385
|
+
)).data,
|
|
246
386
|
create: (input, options = {}) => this.request("POST", "/webhooks", input, options),
|
|
247
|
-
update: (id, patch, options = {}) => this.request(
|
|
248
|
-
|
|
249
|
-
|
|
387
|
+
update: (id, patch, options = {}) => this.request(
|
|
388
|
+
"PUT",
|
|
389
|
+
`/webhooks/${encodeURIComponent(id)}`,
|
|
390
|
+
patch,
|
|
391
|
+
options
|
|
392
|
+
),
|
|
393
|
+
delete: (id, options = {}) => this.request(
|
|
394
|
+
"DELETE",
|
|
395
|
+
`/webhooks/${encodeURIComponent(id)}`,
|
|
396
|
+
void 0,
|
|
397
|
+
options
|
|
398
|
+
),
|
|
399
|
+
test: (id, options = {}) => this.request(
|
|
400
|
+
"POST",
|
|
401
|
+
`/webhooks/${encodeURIComponent(id)}/test`,
|
|
402
|
+
void 0,
|
|
403
|
+
options
|
|
404
|
+
),
|
|
250
405
|
/**
|
|
251
406
|
* Sends a stored event (`evt_…`) again as a new delivery to a registered endpoint (`webhook:manage`)
|
|
252
407
|
* or a running listen session (`webhook:listen`).
|
|
253
408
|
*/
|
|
254
|
-
resend: (eventId, endpointId, options = {}) => this.request(
|
|
409
|
+
resend: (eventId, endpointId, options = {}) => this.request(
|
|
410
|
+
"POST",
|
|
411
|
+
`/webhooks/events/${encodeURIComponent(eventId)}/resend`,
|
|
412
|
+
{ endpoint_id: endpointId },
|
|
413
|
+
options
|
|
414
|
+
),
|
|
255
415
|
/**
|
|
256
416
|
* Listen sessions, what `formfeed listen` uses. Needs `webhook:listen`. Starting a session ends the
|
|
257
417
|
* previous one of the same key; the WebSocket itself is up to the caller.
|
|
258
418
|
*/
|
|
259
419
|
listen: {
|
|
260
420
|
start: (input = {}, options = {}) => this.request("POST", "/webhooks/listen", input, options),
|
|
261
|
-
end: (id, options = {}) => this.request(
|
|
421
|
+
end: (id, options = {}) => this.request(
|
|
422
|
+
"DELETE",
|
|
423
|
+
`/webhooks/listen/${encodeURIComponent(id)}`,
|
|
424
|
+
void 0,
|
|
425
|
+
options
|
|
426
|
+
)
|
|
262
427
|
}
|
|
263
428
|
};
|
|
264
429
|
templates = {
|
|
265
430
|
list: (query = {}, options = {}) => {
|
|
266
431
|
const params = new URLSearchParams();
|
|
267
|
-
for (const [k, v] of Object.entries(query))
|
|
432
|
+
for (const [k, v] of Object.entries(query))
|
|
433
|
+
if (v !== void 0 && v !== null && v !== "") params.set(k, String(v));
|
|
268
434
|
const qs = params.toString();
|
|
269
|
-
return this.request(
|
|
435
|
+
return this.request(
|
|
436
|
+
"GET",
|
|
437
|
+
`/templates${qs ? `?${qs}` : ""}`,
|
|
438
|
+
void 0,
|
|
439
|
+
options
|
|
440
|
+
);
|
|
270
441
|
},
|
|
271
442
|
/** Every template of the workspace, following the cursor. */
|
|
272
443
|
all: async (query = {}, options = {}) => {
|
|
@@ -279,27 +450,63 @@ var Formfeed = class {
|
|
|
279
450
|
} while (cursor);
|
|
280
451
|
return out;
|
|
281
452
|
},
|
|
282
|
-
get: (idOrSlug, options = {}) => this.request(
|
|
453
|
+
get: (idOrSlug, options = {}) => this.request(
|
|
454
|
+
"GET",
|
|
455
|
+
`/templates/${encodeURIComponent(idOrSlug)}`,
|
|
456
|
+
void 0,
|
|
457
|
+
options
|
|
458
|
+
),
|
|
283
459
|
/** A Word or PowerPoint template (`kind` `docx` or `pptx`) is created from its `file`, sent as multipart. */
|
|
284
460
|
create: (input, options = {}) => {
|
|
285
461
|
if (isOfficeInput(input) && input.file) {
|
|
286
462
|
const { file, ...fields } = input;
|
|
287
|
-
return this.request(
|
|
463
|
+
return this.request(
|
|
464
|
+
"POST",
|
|
465
|
+
"/templates",
|
|
466
|
+
multipart(fields, file),
|
|
467
|
+
options
|
|
468
|
+
);
|
|
288
469
|
}
|
|
289
470
|
return this.request("POST", "/templates", input, options);
|
|
290
471
|
},
|
|
291
|
-
update: (idOrSlug, patch, options = {}) => this.request(
|
|
292
|
-
|
|
472
|
+
update: (idOrSlug, patch, options = {}) => this.request(
|
|
473
|
+
"PUT",
|
|
474
|
+
`/templates/${encodeURIComponent(idOrSlug)}`,
|
|
475
|
+
patch,
|
|
476
|
+
options
|
|
477
|
+
),
|
|
478
|
+
archive: (idOrSlug, options = {}) => this.request(
|
|
479
|
+
"DELETE",
|
|
480
|
+
`/templates/${encodeURIComponent(idOrSlug)}`,
|
|
481
|
+
void 0,
|
|
482
|
+
options
|
|
483
|
+
),
|
|
293
484
|
versions: {
|
|
294
|
-
list: async (idOrSlug, options = {}) => (await this.request(
|
|
485
|
+
list: async (idOrSlug, options = {}) => (await this.request(
|
|
486
|
+
"GET",
|
|
487
|
+
`/templates/${encodeURIComponent(idOrSlug)}/versions`,
|
|
488
|
+
void 0,
|
|
489
|
+
options
|
|
490
|
+
)).data,
|
|
295
491
|
/** `which`: 'published', 'latest', a version number or a channel name (its main version). Carries the files. */
|
|
296
|
-
get: (idOrSlug, which = "published", options = {}) => this.request(
|
|
492
|
+
get: (idOrSlug, which = "published", options = {}) => this.request(
|
|
493
|
+
"GET",
|
|
494
|
+
`/templates/${encodeURIComponent(idOrSlug)}/versions/${encodeURIComponent(String(which))}`,
|
|
495
|
+
void 0,
|
|
496
|
+
options
|
|
497
|
+
),
|
|
297
498
|
/** For a Word or PowerPoint template, pass `file` to replace the document; without it the latest file is kept. */
|
|
298
499
|
create: (idOrSlug, input, options = {}) => {
|
|
299
500
|
const path = `/templates/${encodeURIComponent(idOrSlug)}/versions`;
|
|
300
501
|
if (isOfficeInput(input)) {
|
|
301
502
|
const { file, ...fields } = input;
|
|
302
|
-
if (file)
|
|
503
|
+
if (file)
|
|
504
|
+
return this.request(
|
|
505
|
+
"POST",
|
|
506
|
+
path,
|
|
507
|
+
multipart(fields, file),
|
|
508
|
+
options
|
|
509
|
+
);
|
|
303
510
|
return this.request("POST", path, fields, options);
|
|
304
511
|
}
|
|
305
512
|
return this.request("POST", path, input, options);
|
|
@@ -328,7 +535,12 @@ var Formfeed = class {
|
|
|
328
535
|
* uses the version the channel points at. Moves reach renders within a minute.
|
|
329
536
|
*/
|
|
330
537
|
channels: {
|
|
331
|
-
list: (idOrSlug, options = {}) => this.request(
|
|
538
|
+
list: (idOrSlug, options = {}) => this.request(
|
|
539
|
+
"GET",
|
|
540
|
+
`/templates/${encodeURIComponent(idOrSlug)}/channels`,
|
|
541
|
+
void 0,
|
|
542
|
+
options
|
|
543
|
+
),
|
|
332
544
|
/** Creates or moves a channel; on `published` this publishes. */
|
|
333
545
|
set: (idOrSlug, name, move, options = {}) => {
|
|
334
546
|
const { allowBreaking, ...rest } = options;
|
|
@@ -362,19 +574,39 @@ var Formfeed = class {
|
|
|
362
574
|
schema: (idOrSlug, options = {}) => {
|
|
363
575
|
const { version, ...rest } = options;
|
|
364
576
|
const query = version === void 0 ? "" : `?version=${encodeURIComponent(String(version))}`;
|
|
365
|
-
return this.request(
|
|
577
|
+
return this.request(
|
|
578
|
+
"GET",
|
|
579
|
+
`/templates/${encodeURIComponent(idOrSlug)}/schema${query}`,
|
|
580
|
+
void 0,
|
|
581
|
+
rest
|
|
582
|
+
);
|
|
366
583
|
},
|
|
367
584
|
/**
|
|
368
585
|
* Checks a version with data without rendering: syntax, header and footer, missing partials,
|
|
369
586
|
* runtime errors and the data against the stored schema. Free. Without `data`, the version's
|
|
370
587
|
* sample data is checked. `version` takes a channel name too.
|
|
371
588
|
*/
|
|
372
|
-
validate: (idOrSlug, input = {}, options = {}) => this.request(
|
|
589
|
+
validate: (idOrSlug, input = {}, options = {}) => this.request(
|
|
590
|
+
"POST",
|
|
591
|
+
`/templates/${encodeURIComponent(idOrSlug)}/validate`,
|
|
592
|
+
input,
|
|
593
|
+
options
|
|
594
|
+
)
|
|
373
595
|
};
|
|
374
596
|
account = {
|
|
375
|
-
get: (options = {}) => this.request(
|
|
597
|
+
get: (options = {}) => this.request(
|
|
598
|
+
"GET",
|
|
599
|
+
"/account",
|
|
600
|
+
void 0,
|
|
601
|
+
options
|
|
602
|
+
),
|
|
376
603
|
/** Units of the current period, or of `YYYY-MM`, with a daily series and a breakdown per template. */
|
|
377
|
-
usage: (period = "current", options = {}) => this.request(
|
|
604
|
+
usage: (period = "current", options = {}) => this.request(
|
|
605
|
+
"GET",
|
|
606
|
+
`/usage?period=${encodeURIComponent(period)}`,
|
|
607
|
+
void 0,
|
|
608
|
+
options
|
|
609
|
+
)
|
|
378
610
|
};
|
|
379
611
|
/** Workspaces of the key's organisation. Needs `workspace:delete`, which no key has by default. */
|
|
380
612
|
workspaces = {
|
|
@@ -382,7 +614,12 @@ var Formfeed = class {
|
|
|
382
614
|
* Deletes a workspace: it leaves the app and the API at once, its keys are revoked and its content
|
|
383
615
|
* is removed after 30 days. The organisation's last workspace is refused with `last_workspace`.
|
|
384
616
|
*/
|
|
385
|
-
delete: (id, options = {}) => this.request(
|
|
617
|
+
delete: (id, options = {}) => this.request(
|
|
618
|
+
"DELETE",
|
|
619
|
+
`/workspaces/${encodeURIComponent(id)}`,
|
|
620
|
+
void 0,
|
|
621
|
+
options
|
|
622
|
+
)
|
|
386
623
|
};
|
|
387
624
|
channelAction(idOrSlug, name, action, options) {
|
|
388
625
|
const { allowBreaking, ...rest } = options;
|
|
@@ -410,13 +647,21 @@ var Formfeed = class {
|
|
|
410
647
|
...this.extraHeaders
|
|
411
648
|
};
|
|
412
649
|
const isForm = typeof FormData !== "undefined" && body instanceof FormData;
|
|
413
|
-
if (body !== void 0 && !isForm)
|
|
414
|
-
|
|
650
|
+
if (body !== void 0 && !isForm)
|
|
651
|
+
headers["content-type"] = "application/json";
|
|
652
|
+
if (options.idempotencyKey)
|
|
653
|
+
headers["idempotency-key"] = options.idempotencyKey;
|
|
415
654
|
let attempt = 0;
|
|
416
655
|
for (; ; ) {
|
|
417
656
|
const controller = new AbortController();
|
|
418
|
-
const timer = setTimeout(
|
|
419
|
-
|
|
657
|
+
const timer = setTimeout(
|
|
658
|
+
() => controller.abort(new Error("request timed out")),
|
|
659
|
+
this.timeoutMs
|
|
660
|
+
);
|
|
661
|
+
options.signal?.addEventListener(
|
|
662
|
+
"abort",
|
|
663
|
+
() => controller.abort(options.signal?.reason)
|
|
664
|
+
);
|
|
420
665
|
let res;
|
|
421
666
|
try {
|
|
422
667
|
res = await this.fetchImpl(url, {
|
|
@@ -432,22 +677,38 @@ var Formfeed = class {
|
|
|
432
677
|
await sleep(this.backoff(attempt, null), options.signal);
|
|
433
678
|
continue;
|
|
434
679
|
}
|
|
435
|
-
throw new FormfeedError(
|
|
680
|
+
throw new FormfeedError(
|
|
681
|
+
"network_error",
|
|
682
|
+
e instanceof Error ? e.message : String(e),
|
|
683
|
+
0
|
|
684
|
+
);
|
|
436
685
|
}
|
|
437
686
|
clearTimeout(timer);
|
|
438
687
|
if ((res.status === 429 || res.status === 503) && attempt < this.maxRetries) {
|
|
439
688
|
attempt++;
|
|
440
|
-
await sleep(
|
|
689
|
+
await sleep(
|
|
690
|
+
this.backoff(attempt, res.headers.get("retry-after")),
|
|
691
|
+
options.signal
|
|
692
|
+
);
|
|
441
693
|
continue;
|
|
442
694
|
}
|
|
443
695
|
if (res.status === 204) return { status: 204, body: void 0 };
|
|
444
|
-
if (as === "bytes" && res.ok)
|
|
696
|
+
if (as === "bytes" && res.ok)
|
|
697
|
+
return {
|
|
698
|
+
status: res.status,
|
|
699
|
+
body: new Uint8Array(await res.arrayBuffer())
|
|
700
|
+
};
|
|
445
701
|
const text = await res.text();
|
|
446
702
|
let json = null;
|
|
447
703
|
try {
|
|
448
704
|
json = text ? JSON.parse(text) : null;
|
|
449
705
|
} catch {
|
|
450
|
-
if (res.ok)
|
|
706
|
+
if (res.ok)
|
|
707
|
+
throw new FormfeedError(
|
|
708
|
+
"invalid_response",
|
|
709
|
+
`the API answered HTTP ${res.status} with a body that is not JSON`,
|
|
710
|
+
res.status
|
|
711
|
+
);
|
|
451
712
|
}
|
|
452
713
|
if (!res.ok) {
|
|
453
714
|
const problem = json ?? {};
|