@nohead/sdk 0.7.0 → 0.9.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/CHANGELOG.md +96 -0
- package/README.md +58 -26
- package/dist/client.d.ts +9 -1
- package/dist/client.js +14 -2
- package/dist/core.d.ts +22 -5
- package/dist/core.js +158 -45
- package/dist/errors.d.ts +13 -1
- package/dist/errors.js +74 -16
- package/dist/generated/operations.gen.d.ts +0 -8
- package/dist/generated/operations.gen.js +0 -1
- package/dist/generated/schemas.gen.d.ts +1 -1
- package/dist/generated/types.gen.d.ts +23 -59
- package/dist/images.js +8 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -0
- package/dist/resources/assets.d.ts +6 -4
- package/dist/resources/assets.js +23 -18
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/webhooks.d.ts +2 -1
- package/dist/webhooks.js +6 -5
- package/package.json +2 -1
package/dist/core.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { APIPromise, Page, PagePromise, } from "./api-promise.js";
|
|
2
|
-
import { AbortError, ConnectionError, NoheadError, TimeoutError, apiError, retryAfterSeconds, } from "./errors.js";
|
|
2
|
+
import { AbortError, ConnectionError, NoheadError, TimeoutError, apiError, isObject, retryAfterSeconds, } from "./errors.js";
|
|
3
3
|
import { OPERATIONS, } from "./generated/operations.gen.js";
|
|
4
4
|
import { VERSION } from "./version.js";
|
|
5
5
|
export const DEFAULT_BASE_URL = "https://api.nohead.io";
|
|
6
|
-
/**
|
|
7
|
-
* The string with lone surrogates (half an emoji, from cutting a string)
|
|
8
|
-
* replaced by U+FFFD, as `String.prototype.toWellFormed` does, so
|
|
9
|
-
* `encodeURIComponent` can encode it.
|
|
10
|
-
*/
|
|
11
|
-
const wellFormed = (text) => text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD");
|
|
12
6
|
const RETRYABLE_STATUSES = new Set([429, 500, 502, 503, 504]);
|
|
13
7
|
const MAX_RETRY_AFTER_SECONDS = 60;
|
|
8
|
+
// Once per process, not per client: an app that makes a client per request
|
|
9
|
+
// (as on Workers) would otherwise warn on every one.
|
|
14
10
|
const warnedDeprecations = new Set();
|
|
15
11
|
let warnedUsage = false;
|
|
16
12
|
export class Core {
|
|
@@ -22,37 +18,51 @@ export class Core {
|
|
|
22
18
|
#timeout;
|
|
23
19
|
#warnings;
|
|
24
20
|
#onRetry;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// Empty environment variables count as unset.
|
|
29
|
-
const apiKey = options.apiKey
|
|
21
|
+
// The project lookup, shared with the client's copies (`withOptions`).
|
|
22
|
+
#project;
|
|
23
|
+
constructor(options, project = {}) {
|
|
24
|
+
// Empty options and environment variables count as unset.
|
|
25
|
+
const apiKey = options.apiKey || environment().NOHEAD_API_KEY;
|
|
30
26
|
if (!apiKey) {
|
|
31
27
|
throw new NoheadError("Missing API key: pass `apiKey` or set NOHEAD_API_KEY");
|
|
32
28
|
}
|
|
33
29
|
this.#apiKey = apiKey;
|
|
34
|
-
this.#baseUrl =
|
|
30
|
+
this.#baseUrl = baseUrlOf(options.baseUrl || DEFAULT_BASE_URL);
|
|
35
31
|
this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
36
32
|
this.#headers = options.headers ?? {};
|
|
37
33
|
this.#maxRetries = options.maxRetries ?? 2;
|
|
38
34
|
this.#timeout = options.timeout ?? 60_000;
|
|
39
35
|
this.#warnings = options.warnings ?? true;
|
|
40
36
|
this.#onRetry = options.onRetry;
|
|
37
|
+
this.#project = project;
|
|
41
38
|
if (options.projectId)
|
|
42
|
-
|
|
39
|
+
project.id ??= Promise.resolve(options.projectId);
|
|
40
|
+
}
|
|
41
|
+
/** A copy with other settings, sharing the project lookup. */
|
|
42
|
+
withOptions(options) {
|
|
43
|
+
return new Core({
|
|
44
|
+
apiKey: this.#apiKey,
|
|
45
|
+
baseUrl: this.#baseUrl,
|
|
46
|
+
fetch: this.#fetch,
|
|
47
|
+
headers: { ...this.#headers, ...options.headers },
|
|
48
|
+
maxRetries: options.maxRetries ?? this.#maxRetries,
|
|
49
|
+
timeout: options.timeout ?? this.#timeout,
|
|
50
|
+
warnings: this.#warnings,
|
|
51
|
+
onRetry: options.onRetry ?? this.#onRetry,
|
|
52
|
+
}, this.#project);
|
|
43
53
|
}
|
|
44
54
|
/** The API key's project, from `GET /v1/me` the first time. */
|
|
45
55
|
projectId() {
|
|
46
|
-
this.#
|
|
56
|
+
this.#project.id ??= this.request("me_get", {}).then((me) => {
|
|
47
57
|
if (!me.api_key) {
|
|
48
58
|
throw new NoheadError("The credentials are not a project API key");
|
|
49
59
|
}
|
|
50
60
|
return me.api_key.project_id;
|
|
51
61
|
}, (error) => {
|
|
52
|
-
this.#
|
|
62
|
+
this.#project.id = undefined;
|
|
53
63
|
throw error;
|
|
54
64
|
});
|
|
55
|
-
return this.#
|
|
65
|
+
return this.#project.id;
|
|
56
66
|
}
|
|
57
67
|
request(operation, input = {}, options = {}) {
|
|
58
68
|
return new APIPromise(this.#send(operation, input, options));
|
|
@@ -63,21 +73,32 @@ export class Core {
|
|
|
63
73
|
const query = { ...input.query, ...(cursor ? { cursor } : {}) };
|
|
64
74
|
const result = await this.#send(operation, { ...input, query }, options);
|
|
65
75
|
const list = result.data;
|
|
76
|
+
// A proxy's page, say, rather than the API's list.
|
|
77
|
+
if (!isObject(list) ||
|
|
78
|
+
!Array.isArray(list.data) ||
|
|
79
|
+
!isObject(list.meta)) {
|
|
80
|
+
throw new NoheadError(`The API's response could not be read (HTTP ${result.response.status})`);
|
|
81
|
+
}
|
|
66
82
|
const page = new Page(list.data, list.meta, (next) => fetchPage(next).then((r) => r.data));
|
|
67
83
|
return { ...result, data: page };
|
|
68
84
|
};
|
|
69
85
|
const first = input.query?.cursor;
|
|
70
86
|
return new PagePromise(fetchPage(first));
|
|
71
87
|
}
|
|
72
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* `fetch` with this client's retries, for an upload's presigned URL. fetch
|
|
90
|
+
* shows no upload progress, so each attempt may take as long as sending
|
|
91
|
+
* `byteSize` at 64 KiB/s, if that is longer than the timeout.
|
|
92
|
+
*/
|
|
73
93
|
async fetchWithRetries(url, init, options) {
|
|
74
94
|
const maxRetries = options.retry
|
|
75
95
|
? (options.maxRetries ?? this.#maxRetries)
|
|
76
96
|
: 0;
|
|
97
|
+
const timeout = Math.max(options.timeout ?? this.#timeout, (options.byteSize / 65_536) * 1000);
|
|
77
98
|
for (let attempt = 0;; attempt++) {
|
|
78
99
|
let failed;
|
|
79
100
|
try {
|
|
80
|
-
const { response } = await this.#attempt(url, init, options, false);
|
|
101
|
+
const { response } = await this.#attempt(url, init, { ...options, timeout }, false);
|
|
81
102
|
if (!RETRYABLE_STATUSES.has(response.status) || attempt >= maxRetries) {
|
|
82
103
|
return response;
|
|
83
104
|
}
|
|
@@ -96,33 +117,39 @@ export class Core {
|
|
|
96
117
|
const { method, path } = OPERATIONS[operation];
|
|
97
118
|
const url = new URL(this.#baseUrl + (await this.#path(path, input.path)));
|
|
98
119
|
appendQuery(url.searchParams, input.query);
|
|
99
|
-
|
|
120
|
+
// Each name once, whatever its case: the defaults, then the client's
|
|
121
|
+
// headers, the call's, and those of the request options, which win.
|
|
122
|
+
const headers = {};
|
|
123
|
+
setHeaders(headers, {
|
|
100
124
|
Accept: "application/json",
|
|
101
125
|
Authorization: `Bearer ${this.#apiKey}`,
|
|
102
126
|
"Nohead-Client": `sdk-typescript/${VERSION}`,
|
|
103
127
|
...USER_AGENT,
|
|
104
|
-
|
|
105
|
-
|
|
128
|
+
});
|
|
129
|
+
setHeaders(headers, this.#headers);
|
|
130
|
+
setHeaders(headers, options.headers);
|
|
131
|
+
const own = {};
|
|
106
132
|
if (input.body !== undefined)
|
|
107
|
-
|
|
133
|
+
own["Content-Type"] = "application/json";
|
|
108
134
|
if (method !== "GET") {
|
|
109
|
-
|
|
135
|
+
own["Idempotency-Key"] = options.idempotencyKey || crypto.randomUUID();
|
|
110
136
|
}
|
|
111
|
-
if (options.ifMatch !== undefined) {
|
|
112
|
-
|
|
113
|
-
? options.ifMatch.revision
|
|
114
|
-
: options.ifMatch;
|
|
115
|
-
headers["If-Match"] = `"${revision}"`;
|
|
137
|
+
if (options.ifMatch !== undefined && options.ifMatch !== null) {
|
|
138
|
+
own["If-Match"] = `"${revisionOf(options.ifMatch)}"`;
|
|
116
139
|
}
|
|
117
140
|
// Percent-encoded UTF-8: header values are bytes, and fetch refuses
|
|
118
|
-
// anything past Latin-1 (the API decodes it).
|
|
141
|
+
// anything past Latin-1 (the API decodes it). Lone surrogates (half an
|
|
142
|
+
// emoji, from cutting a string) become U+FFFD, so encodeURIComponent
|
|
143
|
+
// can encode them.
|
|
119
144
|
if (options.changeNote)
|
|
120
|
-
|
|
121
|
-
|
|
145
|
+
own["Nohead-Change-Note"] = encodeURIComponent(options.changeNote.toWellFormed());
|
|
146
|
+
setHeaders(headers, own);
|
|
122
147
|
const init = {
|
|
123
148
|
method,
|
|
124
149
|
headers,
|
|
125
|
-
body: input.body === undefined
|
|
150
|
+
body: input.body === undefined
|
|
151
|
+
? undefined
|
|
152
|
+
: JSON.stringify(input.body, finiteNumbers),
|
|
126
153
|
};
|
|
127
154
|
const maxRetries = options.maxRetries ?? this.#maxRetries;
|
|
128
155
|
for (let attempt = 0;; attempt++) {
|
|
@@ -154,14 +181,32 @@ export class Core {
|
|
|
154
181
|
return sleep(delay, signal);
|
|
155
182
|
}
|
|
156
183
|
async #attempt(url, init, options, readBody) {
|
|
184
|
+
// The timeout is the longest wait for progress, so a response that
|
|
185
|
+
// keeps arriving never times out: the timer starts again when the
|
|
186
|
+
// response comes, and with each chunk of its body.
|
|
157
187
|
const timeout = options.timeout ?? this.#timeout;
|
|
158
|
-
const timer =
|
|
188
|
+
const timer = new AbortController();
|
|
189
|
+
let timeoutId;
|
|
190
|
+
const progress = () => {
|
|
191
|
+
clearTimeout(timeoutId);
|
|
192
|
+
timeoutId = setTimeout(() => timer.abort(), timeout);
|
|
193
|
+
};
|
|
159
194
|
const signal = options.signal
|
|
160
|
-
? AbortSignal.any([options.signal, timer])
|
|
161
|
-
: timer;
|
|
195
|
+
? AbortSignal.any([options.signal, timer.signal])
|
|
196
|
+
: timer.signal;
|
|
162
197
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
198
|
+
progress();
|
|
199
|
+
// Redirects aren't followed: one would send the API key to another
|
|
200
|
+
// host, or a write as a GET.
|
|
201
|
+
const response = await this.#fetch(url, {
|
|
202
|
+
...init,
|
|
203
|
+
redirect: "manual",
|
|
204
|
+
signal,
|
|
205
|
+
});
|
|
206
|
+
progress();
|
|
207
|
+
const body = readBody
|
|
208
|
+
? await parseBody(response, signal, progress)
|
|
209
|
+
: undefined;
|
|
165
210
|
return { response, body };
|
|
166
211
|
}
|
|
167
212
|
catch (error) {
|
|
@@ -170,11 +215,14 @@ export class Core {
|
|
|
170
215
|
cause: options.signal.reason,
|
|
171
216
|
});
|
|
172
217
|
}
|
|
173
|
-
if (timer.aborted) {
|
|
218
|
+
if (timer.signal.aborted) {
|
|
174
219
|
throw new TimeoutError(`The request timed out after ${timeout} ms`);
|
|
175
220
|
}
|
|
176
221
|
throw new ConnectionError(`Could not reach ${new URL(url).host}: ${error.message}`, { cause: error });
|
|
177
222
|
}
|
|
223
|
+
finally {
|
|
224
|
+
clearTimeout(timeoutId);
|
|
225
|
+
}
|
|
178
226
|
}
|
|
179
227
|
async #path(template, values = {}) {
|
|
180
228
|
let path = template;
|
|
@@ -183,7 +231,7 @@ export class Core {
|
|
|
183
231
|
for (const name of Object.keys(values))
|
|
184
232
|
if (!names.includes(name))
|
|
185
233
|
throw new TypeError(`${name} is not a path parameter of ${template}`);
|
|
186
|
-
for (const
|
|
234
|
+
for (const name of names) {
|
|
187
235
|
let value = values[name];
|
|
188
236
|
if (value === undefined && name === "project_id") {
|
|
189
237
|
value = await this.projectId();
|
|
@@ -191,6 +239,10 @@ export class Core {
|
|
|
191
239
|
if (value === undefined || value === "") {
|
|
192
240
|
throw new NoheadError(`Missing ${name}`);
|
|
193
241
|
}
|
|
242
|
+
// Encoding leaves these as they are, and URLs resolve them.
|
|
243
|
+
if (value === "." || value === "..") {
|
|
244
|
+
throw new NoheadError(`Invalid ${name}: "${value}"`);
|
|
245
|
+
}
|
|
194
246
|
path = path.replace(`{${name}}`, encodeURIComponent(String(value)));
|
|
195
247
|
}
|
|
196
248
|
return path;
|
|
@@ -249,8 +301,8 @@ function sleep(seconds, signal) {
|
|
|
249
301
|
signal?.addEventListener("abort", aborted, { once: true });
|
|
250
302
|
});
|
|
251
303
|
}
|
|
252
|
-
async function parseBody(response) {
|
|
253
|
-
const text = await response
|
|
304
|
+
async function parseBody(response, signal, progress) {
|
|
305
|
+
const text = await readText(response, signal, progress);
|
|
254
306
|
if (!text)
|
|
255
307
|
return undefined;
|
|
256
308
|
if (!response.headers.get("content-type")?.includes("json"))
|
|
@@ -262,6 +314,31 @@ async function parseBody(response) {
|
|
|
262
314
|
return text;
|
|
263
315
|
}
|
|
264
316
|
}
|
|
317
|
+
/** The body's text, calling `progress` for each chunk that arrives. */
|
|
318
|
+
async function readText(response, signal, progress) {
|
|
319
|
+
if (!response.body)
|
|
320
|
+
return "";
|
|
321
|
+
const reader = response.body.getReader();
|
|
322
|
+
// A fetch ends the body when the signal aborts, but not every fetch is
|
|
323
|
+
// the platform's.
|
|
324
|
+
const cancel = () => void reader.cancel().catch(() => undefined);
|
|
325
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
326
|
+
const decoder = new TextDecoder();
|
|
327
|
+
let text = "";
|
|
328
|
+
try {
|
|
329
|
+
for (;;) {
|
|
330
|
+
const { done, value } = await reader.read();
|
|
331
|
+
signal.throwIfAborted();
|
|
332
|
+
if (done)
|
|
333
|
+
return text + decoder.decode();
|
|
334
|
+
progress();
|
|
335
|
+
text += decoder.decode(value, { stream: true });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
finally {
|
|
339
|
+
signal.removeEventListener("abort", cancel);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
265
342
|
/**
|
|
266
343
|
* Query parameters as the API reads them: objects become `key[sub]=…`
|
|
267
344
|
* (`filter[price][lt]=50`), arrays are comma-separated (`expand=author,tags`)
|
|
@@ -308,12 +385,48 @@ function runtimeVersion() {
|
|
|
308
385
|
return `node/${g.process.versions.node}`;
|
|
309
386
|
return undefined;
|
|
310
387
|
}
|
|
311
|
-
/** `url` without trailing slashes
|
|
312
|
-
function
|
|
388
|
+
/** `url` without trailing slashes, if it is an http or https URL. */
|
|
389
|
+
function baseUrlOf(url) {
|
|
390
|
+
// A loop, as a regex backtracks on long runs of slashes.
|
|
313
391
|
let end = url.length;
|
|
314
392
|
while (end > 0 && url[end - 1] === "/")
|
|
315
393
|
end--;
|
|
316
|
-
|
|
394
|
+
const base = url.slice(0, end);
|
|
395
|
+
let protocol;
|
|
396
|
+
try {
|
|
397
|
+
protocol = new URL(base).protocol;
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
// Not a URL at all.
|
|
401
|
+
}
|
|
402
|
+
if (protocol !== "http:" && protocol !== "https:") {
|
|
403
|
+
throw new NoheadError("baseUrl must be an http or https URL");
|
|
404
|
+
}
|
|
405
|
+
return base;
|
|
406
|
+
}
|
|
407
|
+
/** Sets each header, replacing one of the same name in any case. */
|
|
408
|
+
function setHeaders(headers, values = {}) {
|
|
409
|
+
for (const [name, value] of Object.entries(values)) {
|
|
410
|
+
for (const key of Object.keys(headers))
|
|
411
|
+
if (key.toLowerCase() === name.toLowerCase())
|
|
412
|
+
delete headers[key];
|
|
413
|
+
headers[name] = value;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/** `ifMatch`'s revision: a number, or a record's or a draft's. */
|
|
417
|
+
function revisionOf(ifMatch) {
|
|
418
|
+
const revision = isObject(ifMatch) ? ifMatch.revision : ifMatch;
|
|
419
|
+
if (!Number.isInteger(revision)) {
|
|
420
|
+
throw new TypeError("ifMatch takes a revision number, or a record, draft or object with a revision");
|
|
421
|
+
}
|
|
422
|
+
return revision;
|
|
423
|
+
}
|
|
424
|
+
// JSON has no NaN or Infinity, which JSON.stringify would send as null.
|
|
425
|
+
function finiteNumbers(key, value) {
|
|
426
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
427
|
+
throw new TypeError(`JSON can't carry ${value}${key ? ` (in ${key})` : ""}`);
|
|
428
|
+
}
|
|
429
|
+
return value;
|
|
317
430
|
}
|
|
318
431
|
function environment() {
|
|
319
432
|
const process = globalThis.process;
|
package/dist/errors.d.ts
CHANGED
|
@@ -3,7 +3,11 @@ import type { ErrorDetail, ErrorType } from "./generated/types.gen.ts";
|
|
|
3
3
|
export declare class NoheadError extends Error {
|
|
4
4
|
name: string;
|
|
5
5
|
}
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* The API answered with an error status. Subclasses follow the error `type`.
|
|
8
|
+
* Its `message` names the status, the type and the request's ID along with
|
|
9
|
+
* the API's message: `422 validation_error: Title is required (request req_…)`.
|
|
10
|
+
*/
|
|
7
11
|
export declare class APIError extends NoheadError {
|
|
8
12
|
name: string;
|
|
9
13
|
/** The HTTP status. */
|
|
@@ -13,6 +17,8 @@ export declare class APIError extends NoheadError {
|
|
|
13
17
|
* none. A type newer than this SDK is kept as its string.
|
|
14
18
|
*/
|
|
15
19
|
readonly type: ErrorType | (string & {}) | undefined;
|
|
20
|
+
/** The API's own message, without the status, type and ID `message` adds. */
|
|
21
|
+
readonly apiMessage: string;
|
|
16
22
|
/** The request's ID (`req_…`), for support requests and logs. */
|
|
17
23
|
readonly requestId: string | undefined;
|
|
18
24
|
/** Field-level details, e.g. `[{ field: "title", code: "required", … }]`. */
|
|
@@ -85,4 +91,10 @@ export declare class WebhookVerificationError extends NoheadError {
|
|
|
85
91
|
}
|
|
86
92
|
/** The error for a response, by its `type`, or by status when it has none. */
|
|
87
93
|
export declare function apiError(status: number, body: unknown, headers: Headers): APIError;
|
|
94
|
+
/**
|
|
95
|
+
* `Retry-After` in seconds: a number of seconds (`120`, `1.5`), or an HTTP
|
|
96
|
+
* date. Anything else (`-1`, `1e3`, `0x10`, an ISO 8601 time) is undefined,
|
|
97
|
+
* as the Python and Ruby SDKs read it.
|
|
98
|
+
*/
|
|
88
99
|
export declare function retryAfterSeconds(headers: Headers): number | undefined;
|
|
100
|
+
export declare const isObject: (value: unknown) => value is Record<string, unknown>;
|
package/dist/errors.js
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
export class NoheadError extends Error {
|
|
3
3
|
name = "NoheadError";
|
|
4
4
|
}
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* The API answered with an error status. Subclasses follow the error `type`.
|
|
7
|
+
* Its `message` names the status, the type and the request's ID along with
|
|
8
|
+
* the API's message: `422 validation_error: Title is required (request req_…)`.
|
|
9
|
+
*/
|
|
6
10
|
export class APIError extends NoheadError {
|
|
7
11
|
name = "APIError";
|
|
8
12
|
/** The HTTP status. */
|
|
@@ -12,6 +16,8 @@ export class APIError extends NoheadError {
|
|
|
12
16
|
* none. A type newer than this SDK is kept as its string.
|
|
13
17
|
*/
|
|
14
18
|
type;
|
|
19
|
+
/** The API's own message, without the status, type and ID `message` adds. */
|
|
20
|
+
apiMessage;
|
|
15
21
|
/** The request's ID (`req_…`), for support requests and logs. */
|
|
16
22
|
requestId;
|
|
17
23
|
/** Field-level details, e.g. `[{ field: "title", code: "required", … }]`. */
|
|
@@ -20,12 +26,17 @@ export class APIError extends NoheadError {
|
|
|
20
26
|
/** The parsed response body, or its text when it was not JSON. */
|
|
21
27
|
body;
|
|
22
28
|
constructor(status, body, headers) {
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
// Below 400 (and not 2xx) is a redirect, which the SDK doesn't follow:
|
|
30
|
+
// `0` is a browser's, which shows nothing of it.
|
|
31
|
+
const error = status < 400 ? redirect(headers) : errorEnvelope(body);
|
|
32
|
+
const requestId = error?.request_id ?? headers.get("x-request-id") ?? undefined;
|
|
33
|
+
const apiMessage = error?.message || `Request failed with status ${status}`;
|
|
34
|
+
super(`${status} ${error?.type || "error"}: ${apiMessage}` +
|
|
35
|
+
(requestId ? ` (request ${requestId})` : ""));
|
|
25
36
|
this.status = status;
|
|
37
|
+
this.apiMessage = apiMessage;
|
|
26
38
|
this.type = error?.type;
|
|
27
|
-
this.requestId =
|
|
28
|
-
error?.request_id ?? headers.get("x-request-id") ?? undefined;
|
|
39
|
+
this.requestId = requestId;
|
|
29
40
|
this.details = error?.details ?? [];
|
|
30
41
|
this.headers = headers;
|
|
31
42
|
this.body = body;
|
|
@@ -118,6 +129,8 @@ const CLASSES = new Map(Object.entries({
|
|
|
118
129
|
}));
|
|
119
130
|
/** The error for a response, by its `type`, or by status when it has none. */
|
|
120
131
|
export function apiError(status, body, headers) {
|
|
132
|
+
if (status < 400)
|
|
133
|
+
return new APIError(status, body, headers);
|
|
121
134
|
const type = errorEnvelope(body)?.type;
|
|
122
135
|
const Class = (type && CLASSES.get(type)) ||
|
|
123
136
|
(status === 503
|
|
@@ -127,18 +140,49 @@ export function apiError(status, body, headers) {
|
|
|
127
140
|
: APIError);
|
|
128
141
|
return new Class(status, body, headers);
|
|
129
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* `Retry-After` in seconds: a number of seconds (`120`, `1.5`), or an HTTP
|
|
145
|
+
* date. Anything else (`-1`, `1e3`, `0x10`, an ISO 8601 time) is undefined,
|
|
146
|
+
* as the Python and Ruby SDKs read it.
|
|
147
|
+
*/
|
|
130
148
|
export function retryAfterSeconds(headers) {
|
|
131
|
-
const value = headers.get("retry-after");
|
|
149
|
+
const value = headers.get("retry-after")?.trim();
|
|
132
150
|
if (!value)
|
|
133
151
|
return undefined;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return Number.isNaN(date)
|
|
152
|
+
if (/^\d+(\.\d+)?$/.test(value))
|
|
153
|
+
return Number(value);
|
|
154
|
+
const date = httpDate(value);
|
|
155
|
+
return date === undefined
|
|
139
156
|
? undefined
|
|
140
157
|
: Math.max(0, (date - Date.now()) / 1000);
|
|
141
158
|
}
|
|
159
|
+
// The three forms of an HTTP date (RFC 9110), all in GMT. Not Date.parse,
|
|
160
|
+
// which takes more than these, and asctime's in the local time zone.
|
|
161
|
+
const HTTP_DATES = [
|
|
162
|
+
// IMF-fixdate: Sun, 06 Nov 1994 08:49:37 GMT
|
|
163
|
+
/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (?<day>\d{2}) (?<month>\w{3}) (?<year>\d{4}) (?<time>\d{2}:\d{2}:\d{2}) GMT$/,
|
|
164
|
+
// RFC 850: Sunday, 06-Nov-94 08:49:37 GMT
|
|
165
|
+
/^(?:Mon|Tues|Wednes|Thurs|Fri|Satur|Sun)day, (?<day>\d{2})-(?<month>\w{3})-(?<year>\d{2}) (?<time>\d{2}:\d{2}:\d{2}) GMT$/,
|
|
166
|
+
// asctime: Sun Nov 6 08:49:37 1994
|
|
167
|
+
/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (?<month>\w{3}) (?<day>[ \d]\d) (?<time>\d{2}:\d{2}:\d{2}) (?<year>\d{4})$/,
|
|
168
|
+
];
|
|
169
|
+
const MONTHS = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" ");
|
|
170
|
+
/** An HTTP date in milliseconds, or undefined if `value` isn't one. */
|
|
171
|
+
function httpDate(value) {
|
|
172
|
+
const date = HTTP_DATES.map((pattern) => pattern.exec(value)?.groups).find(Boolean);
|
|
173
|
+
const month = MONTHS.indexOf(date?.month ?? "");
|
|
174
|
+
if (!date || month < 0)
|
|
175
|
+
return undefined;
|
|
176
|
+
let year = Number(date.year);
|
|
177
|
+
// RFC 850's two digits: the latest such year not over 50 years ahead.
|
|
178
|
+
if (date.year.length === 2) {
|
|
179
|
+
year += 2000;
|
|
180
|
+
if (year > new Date().getUTCFullYear() + 50)
|
|
181
|
+
year -= 100;
|
|
182
|
+
}
|
|
183
|
+
const [hours, minutes, seconds] = date.time.split(":").map(Number);
|
|
184
|
+
return Date.UTC(year, month, Number(date.day), hours, minutes, seconds);
|
|
185
|
+
}
|
|
142
186
|
// The error envelope's fields that have the right type: a proxy or gateway
|
|
143
187
|
// in front of the API may answer with any JSON.
|
|
144
188
|
function errorEnvelope(body) {
|
|
@@ -149,10 +193,24 @@ function errorEnvelope(body) {
|
|
|
149
193
|
type: typeof type === "string" ? type : undefined,
|
|
150
194
|
message: typeof message === "string" ? message : undefined,
|
|
151
195
|
request_id: typeof request_id === "string" ? request_id : undefined,
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
196
|
+
details: Array.isArray(details) ? details.map(errorDetail) : undefined,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
// A detail that isn't an object is kept as its text, so currentRevision can
|
|
200
|
+
// read every detail's fields.
|
|
201
|
+
function errorDetail(detail) {
|
|
202
|
+
if (isObject(detail) && !Array.isArray(detail))
|
|
203
|
+
return detail;
|
|
204
|
+
const message = typeof detail === "string" ? detail : JSON.stringify(detail);
|
|
205
|
+
return { code: "unknown", message: String(message) };
|
|
206
|
+
}
|
|
207
|
+
// A redirect's error: following one would send the API key elsewhere, or a
|
|
208
|
+
// write as a GET.
|
|
209
|
+
function redirect(headers) {
|
|
210
|
+
const location = headers.get("location");
|
|
211
|
+
return {
|
|
212
|
+
type: "redirect",
|
|
213
|
+
message: `The API answered with a redirect${location ? ` to ${location}` : ""}; check the base URL`,
|
|
156
214
|
};
|
|
157
215
|
}
|
|
158
|
-
const isObject = (value) => typeof value === "object" && value !== null;
|
|
216
|
+
export const isObject = (value) => typeof value === "object" && value !== null;
|
|
@@ -8,10 +8,6 @@ export declare const OPERATIONS: {
|
|
|
8
8
|
readonly method: "DELETE";
|
|
9
9
|
readonly path: "/v1/assets/{asset_id}";
|
|
10
10
|
};
|
|
11
|
-
readonly assets_download_url: {
|
|
12
|
-
readonly method: "GET";
|
|
13
|
-
readonly path: "/v1/assets/{asset_id}/download-url";
|
|
14
|
-
};
|
|
15
11
|
readonly assets_get: {
|
|
16
12
|
readonly method: "GET";
|
|
17
13
|
readonly path: "/v1/assets/{asset_id}";
|
|
@@ -339,10 +335,6 @@ export interface Operations {
|
|
|
339
335
|
data: T.AssetsDeleteData;
|
|
340
336
|
response: T.AssetsDeleteResponse;
|
|
341
337
|
};
|
|
342
|
-
assets_download_url: {
|
|
343
|
-
data: T.AssetsDownloadUrlData;
|
|
344
|
-
response: T.AssetsDownloadUrlResponse;
|
|
345
|
-
};
|
|
346
338
|
assets_get: {
|
|
347
339
|
data: T.AssetsGetData;
|
|
348
340
|
response: T.AssetsGetResponse;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export const OPERATIONS = {
|
|
2
2
|
assets_complete: { method: "POST", path: "/v1/assets/{asset_id}/complete" },
|
|
3
3
|
assets_delete: { method: "DELETE", path: "/v1/assets/{asset_id}" },
|
|
4
|
-
assets_download_url: { method: "GET", path: "/v1/assets/{asset_id}/download-url" },
|
|
5
4
|
assets_get: { method: "GET", path: "/v1/assets/{asset_id}" },
|
|
6
5
|
assets_list: { method: "GET", path: "/v1/projects/{project_id}/assets" },
|
|
7
6
|
assets_purge: { method: "POST", path: "/v1/assets/{asset_id}/purge" },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export type { Actor, ApiKeyScope, Asset, AssetEventData, AssetField, AssetFieldConfiguration, AssetFieldCreate, AssetKind, AssetList, AssetProperties, AssetUpdate, AssetUploadCreate, AssetUploadTarget, AssetUsage, AssetWithUpload, AuditEvent, AuditEventList, BooleanField, BooleanFieldConfiguration, BooleanFieldCreate, Change, Collection, CollectionCreate, CollectionList, CollectionUpdate, DateField, DateFieldConfiguration, DateFieldCreate, DecimalField, DecimalFieldConfiguration, DecimalFieldCreate,
|
|
1
|
+
export type { Actor, ApiKeyScope, Asset, AssetEventData, AssetField, AssetFieldConfiguration, AssetFieldCreate, AssetKind, AssetList, AssetProperties, AssetUpdate, AssetUploadCreate, AssetUploadTarget, AssetUsage, AssetWithUpload, AuditEvent, AuditEventList, BooleanField, BooleanFieldConfiguration, BooleanFieldCreate, Change, Collection, CollectionCreate, CollectionList, CollectionUpdate, DateField, DateFieldConfiguration, DateFieldCreate, DecimalField, DecimalFieldConfiguration, DecimalFieldCreate, Draft, DraftCreate, DraftEventData, DraftList, DraftSchedule, DraftScheduleUpdate, EnumField, EnumFieldConfiguration, EnumFieldCreate, Error, ErrorDetail, ErrorType, FeatureFlags, Field, FieldAliasEventData, FieldConfiguration, FieldCreate, FieldCreateProperties, FieldDefinition, FieldList, FieldMigration, FieldMigrationCounts, FieldMigrationList, FieldMigrationPreview, FieldMigrationRequest, FieldOrder, FieldProperties, FieldType, FieldUpdate, Health, IntegerField, IntegerFieldConfiguration, IntegerFieldCreate, JsonField, JsonFieldConfiguration, JsonFieldCreate, ListMeta, LongTextField, LongTextFieldConfiguration, LongTextFieldCreate, MergedDraft, NullableTimestamp, Principal, PublishedOnly, RecordBatch, RecordBatchRequest, RecordCount, RecordCreate, RecordData, RecordDiff, RecordEventData, RecordFilterValue, RecordList, RecordPublication, RecordRevision, RecordRevisionDetail, RecordRevisionList, RecordRevisionOperation, RecordRevisionProperties, RecordSchedule, RecordScheduleUpdate, RecordWrite, RelationField, RelationFieldConfiguration, RelationFieldCreate, RequestId, RevertPreview, RichTextField, RichTextFieldConfiguration, RichTextFieldCreate, ScheduledChange, ScheduledChangeList, Schema, SchemaChange, SchemaChangeDetail, SchemaChangeList, SchemaChangeProperties, SchemaDefinition, SchemaEventData, SearchIndex, SearchResults, TextField, TextFieldConfiguration, TextFieldCreate, TimeFilter, Timestamp, Via, Webhook, WebhookCreate, WebhookDelivery, WebhookDeliveryDetail, WebhookDeliveryList, WebhookDeliveryProperties, WebhookEventPayload, WebhookEventType, WebhookList, WebhookProperties, WebhookSecret, WebhookSecretList, WebhookSecretProperties, WebhookSecretWithValue, WebhookSubscription, WebhookTestEventData, WebhookUpdate, WebhookWithSecret, } from "./types.gen.ts";
|