@nohead/sdk 0.0.0 → 0.1.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 +20 -0
- package/LICENSE +21 -0
- package/README.md +352 -3
- package/dist/api-promise.d.ts +50 -0
- package/dist/api-promise.js +77 -0
- package/dist/client.d.ts +47 -0
- package/dist/client.js +48 -0
- package/dist/core.d.ts +70 -0
- package/dist/core.js +290 -0
- package/dist/errors.d.ts +85 -0
- package/dist/errors.js +144 -0
- package/dist/generated/operations.gen.d.ts +502 -0
- package/dist/generated/operations.gen.js +64 -0
- package/dist/generated/schemas.gen.d.ts +1 -0
- package/dist/generated/schemas.gen.js +1 -0
- package/dist/generated/types.gen.d.ts +6806 -0
- package/dist/generated/types.gen.js +2 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +5 -0
- package/dist/resources/assets.d.ts +44 -0
- package/dist/resources/assets.js +86 -0
- package/dist/resources/other.d.ts +20 -0
- package/dist/resources/other.js +24 -0
- package/dist/resources/records.d.ts +52 -0
- package/dist/resources/records.js +78 -0
- package/dist/resources/resource.d.ts +5 -0
- package/dist/resources/resource.js +6 -0
- package/dist/resources/schema.d.ts +64 -0
- package/dist/resources/schema.js +97 -0
- package/dist/resources/webhooks.d.ts +32 -0
- package/dist/resources/webhooks.js +50 -0
- package/dist/types.d.ts +99 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/dist/webhooks.d.ts +47 -0
- package/dist/webhooks.js +70 -0
- package/package.json +58 -3
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { APIPromise, PagePromise } from "./api-promise.ts";
|
|
2
|
+
import { type OperationId, type Operations } from "./generated/operations.gen.ts";
|
|
3
|
+
import type { ListMeta } from "./generated/types.gen.ts";
|
|
4
|
+
export declare const DEFAULT_BASE_URL = "https://api.nohead.io";
|
|
5
|
+
export interface ClientOptions {
|
|
6
|
+
/** A project API key (`sk_live_…`). Defaults to `NOHEAD_API_KEY`. */
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
/** Defaults to `NOHEAD_API_URL`, else `https://api.nohead.io`. */
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
/** The key's project. Looked up once with `GET /v1/me` when omitted. */
|
|
11
|
+
projectId?: string;
|
|
12
|
+
/** Retries of failed requests (see README, "Retries"). Default 2. */
|
|
13
|
+
maxRetries?: number;
|
|
14
|
+
/** Milliseconds per attempt. Default 60 000. */
|
|
15
|
+
timeout?: number;
|
|
16
|
+
/** A `fetch` implementation, for proxies, instrumentation or tests. */
|
|
17
|
+
fetch?: typeof fetch;
|
|
18
|
+
/** Headers added to every request. */
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
/** Log deprecation and plan usage warnings (once each). Default true. */
|
|
21
|
+
warnings?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface RequestOptions {
|
|
24
|
+
/** Sent as `Idempotency-Key` on writes. Generated when omitted. */
|
|
25
|
+
idempotencyKey?: string;
|
|
26
|
+
/** A reason for the change, shown in history (`Nohead-Change-Note`). */
|
|
27
|
+
changeNote?: string;
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
/** Milliseconds per attempt, overriding the client's. */
|
|
30
|
+
timeout?: number;
|
|
31
|
+
/** Retries, overriding the client's. */
|
|
32
|
+
maxRetries?: number;
|
|
33
|
+
headers?: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
/** Options of record writes that accept `If-Match`. */
|
|
36
|
+
export interface ConditionalRequestOptions extends RequestOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Only write if the record is still at this revision; otherwise the API
|
|
39
|
+
* answers `412` (`PreconditionFailedError`). A record works too.
|
|
40
|
+
*/
|
|
41
|
+
ifMatch?: number | {
|
|
42
|
+
revision: number;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export interface RequestInput {
|
|
46
|
+
path?: Record<string, string | number | undefined>;
|
|
47
|
+
query?: object;
|
|
48
|
+
body?: unknown;
|
|
49
|
+
}
|
|
50
|
+
type AllOptions = ConditionalRequestOptions;
|
|
51
|
+
export declare class Core {
|
|
52
|
+
#private;
|
|
53
|
+
constructor(options: ClientOptions);
|
|
54
|
+
/** The API key's project, from `GET /v1/me` the first time. */
|
|
55
|
+
projectId(): Promise<string>;
|
|
56
|
+
request<Op extends OperationId>(operation: Op, input?: RequestInput, options?: AllOptions): APIPromise<Operations[Op]["response"]>;
|
|
57
|
+
/** A list operation as pages of `Item`. */
|
|
58
|
+
paginate<Item, Meta extends ListMeta = ListMeta>(operation: OperationId, input: RequestInput, options?: RequestOptions): PagePromise<Item, Meta>;
|
|
59
|
+
/** `fetch` with this client's timeout and retries, for presigned URLs. */
|
|
60
|
+
fetchWithRetries(url: string, init: RequestInit, options: RequestOptions & {
|
|
61
|
+
retry: boolean;
|
|
62
|
+
}): Promise<Response>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Query parameters as the API reads them: objects become `key[sub]=…`
|
|
66
|
+
* (`filter[status]=published`), arrays are comma-separated
|
|
67
|
+
* (`expand=author,tags`), dates are ISO 8601.
|
|
68
|
+
*/
|
|
69
|
+
export declare function appendQuery(params: URLSearchParams, value: unknown, key?: string): void;
|
|
70
|
+
export {};
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { APIPromise, Page, PagePromise, } from "./api-promise.js";
|
|
2
|
+
import { AbortError, ConnectionError, NoheadError, TimeoutError, apiError, retryAfterSeconds, } from "./errors.js";
|
|
3
|
+
import { OPERATIONS, } from "./generated/operations.gen.js";
|
|
4
|
+
import { VERSION } from "./version.js";
|
|
5
|
+
export const DEFAULT_BASE_URL = "https://api.nohead.io";
|
|
6
|
+
const RETRYABLE_STATUSES = new Set([429, 500, 502, 503, 504]);
|
|
7
|
+
const MAX_RETRY_AFTER_SECONDS = 60;
|
|
8
|
+
const warnedDeprecations = new Set();
|
|
9
|
+
let warnedUsage = false;
|
|
10
|
+
export class Core {
|
|
11
|
+
#apiKey;
|
|
12
|
+
#baseUrl;
|
|
13
|
+
#fetch;
|
|
14
|
+
#headers;
|
|
15
|
+
#maxRetries;
|
|
16
|
+
#timeout;
|
|
17
|
+
#warnings;
|
|
18
|
+
#projectId;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
const env = environment();
|
|
21
|
+
// Empty environment variables count as unset.
|
|
22
|
+
const apiKey = options.apiKey ?? (env.NOHEAD_API_KEY || undefined);
|
|
23
|
+
if (!apiKey) {
|
|
24
|
+
throw new NoheadError("Missing API key: pass `apiKey` or set NOHEAD_API_KEY");
|
|
25
|
+
}
|
|
26
|
+
this.#apiKey = apiKey;
|
|
27
|
+
this.#baseUrl = (options.baseUrl ??
|
|
28
|
+
(env.NOHEAD_API_URL || DEFAULT_BASE_URL)).replace(/\/+$/, "");
|
|
29
|
+
this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
30
|
+
this.#headers = options.headers ?? {};
|
|
31
|
+
this.#maxRetries = options.maxRetries ?? 2;
|
|
32
|
+
this.#timeout = options.timeout ?? 60_000;
|
|
33
|
+
this.#warnings = options.warnings ?? true;
|
|
34
|
+
if (options.projectId)
|
|
35
|
+
this.#projectId = Promise.resolve(options.projectId);
|
|
36
|
+
}
|
|
37
|
+
/** The API key's project, from `GET /v1/me` the first time. */
|
|
38
|
+
projectId() {
|
|
39
|
+
this.#projectId ??= this.request("me_get", {}).then((me) => {
|
|
40
|
+
if (!me.api_key) {
|
|
41
|
+
throw new NoheadError("The credentials are not a project API key");
|
|
42
|
+
}
|
|
43
|
+
return me.api_key.project_id;
|
|
44
|
+
}, (error) => {
|
|
45
|
+
this.#projectId = undefined;
|
|
46
|
+
throw error;
|
|
47
|
+
});
|
|
48
|
+
return this.#projectId;
|
|
49
|
+
}
|
|
50
|
+
request(operation, input = {}, options = {}) {
|
|
51
|
+
return new APIPromise(this.#send(operation, input, options));
|
|
52
|
+
}
|
|
53
|
+
/** A list operation as pages of `Item`. */
|
|
54
|
+
paginate(operation, input, options = {}) {
|
|
55
|
+
const fetchPage = async (cursor) => {
|
|
56
|
+
const query = { ...input.query, ...(cursor ? { cursor } : {}) };
|
|
57
|
+
const result = await this.#send(operation, { ...input, query }, options);
|
|
58
|
+
const list = result.data;
|
|
59
|
+
const page = new Page(list.data, list.meta, (next) => fetchPage(next).then((r) => r.data));
|
|
60
|
+
return { ...result, data: page };
|
|
61
|
+
};
|
|
62
|
+
const first = input.query?.cursor;
|
|
63
|
+
return new PagePromise(fetchPage(first));
|
|
64
|
+
}
|
|
65
|
+
/** `fetch` with this client's timeout and retries, for presigned URLs. */
|
|
66
|
+
async fetchWithRetries(url, init, options) {
|
|
67
|
+
const maxRetries = options.retry
|
|
68
|
+
? (options.maxRetries ?? this.#maxRetries)
|
|
69
|
+
: 0;
|
|
70
|
+
for (let attempt = 0;; attempt++) {
|
|
71
|
+
try {
|
|
72
|
+
const { response } = await this.#attempt(url, init, options, false);
|
|
73
|
+
if (!RETRYABLE_STATUSES.has(response.status) || attempt >= maxRetries) {
|
|
74
|
+
return response;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (!(error instanceof ConnectionError) || attempt >= maxRetries) {
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
await sleep(backoff(attempt), options.signal);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async #send(operation, input, options) {
|
|
86
|
+
const { method, path } = OPERATIONS[operation];
|
|
87
|
+
const url = new URL(this.#baseUrl + (await this.#path(path, input.path)));
|
|
88
|
+
appendQuery(url.searchParams, input.query);
|
|
89
|
+
const headers = {
|
|
90
|
+
Accept: "application/json",
|
|
91
|
+
Authorization: `Bearer ${this.#apiKey}`,
|
|
92
|
+
"Nohead-Client": `sdk-typescript/${VERSION}`,
|
|
93
|
+
...USER_AGENT,
|
|
94
|
+
...this.#headers,
|
|
95
|
+
};
|
|
96
|
+
if (input.body !== undefined)
|
|
97
|
+
headers["Content-Type"] = "application/json";
|
|
98
|
+
if (method !== "GET") {
|
|
99
|
+
headers["Idempotency-Key"] = options.idempotencyKey ?? crypto.randomUUID();
|
|
100
|
+
}
|
|
101
|
+
if (options.ifMatch !== undefined) {
|
|
102
|
+
const revision = typeof options.ifMatch === "object"
|
|
103
|
+
? options.ifMatch.revision
|
|
104
|
+
: options.ifMatch;
|
|
105
|
+
headers["If-Match"] = `"${revision}"`;
|
|
106
|
+
}
|
|
107
|
+
if (options.changeNote)
|
|
108
|
+
headers["Nohead-Change-Note"] = options.changeNote;
|
|
109
|
+
Object.assign(headers, options.headers);
|
|
110
|
+
const init = {
|
|
111
|
+
method,
|
|
112
|
+
headers,
|
|
113
|
+
body: input.body === undefined ? undefined : JSON.stringify(input.body),
|
|
114
|
+
};
|
|
115
|
+
const maxRetries = options.maxRetries ?? this.#maxRetries;
|
|
116
|
+
for (let attempt = 0;; attempt++) {
|
|
117
|
+
let result;
|
|
118
|
+
try {
|
|
119
|
+
result = await this.#attempt(url.toString(), init, options, true);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (!(error instanceof ConnectionError) || attempt >= maxRetries) {
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
await sleep(backoff(attempt), options.signal);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const { response, body } = result;
|
|
129
|
+
this.#warn(operation, response);
|
|
130
|
+
const requestId = response.headers.get("x-request-id") ?? undefined;
|
|
131
|
+
if (response.ok)
|
|
132
|
+
return { data: body, response, requestId };
|
|
133
|
+
const error = apiError(response.status, body, response.headers);
|
|
134
|
+
const delay = retryDelay(error, attempt, maxRetries);
|
|
135
|
+
if (delay === undefined)
|
|
136
|
+
throw error;
|
|
137
|
+
await sleep(delay, options.signal);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async #attempt(url, init, options, readBody) {
|
|
141
|
+
const timeout = options.timeout ?? this.#timeout;
|
|
142
|
+
const timer = AbortSignal.timeout(timeout);
|
|
143
|
+
const signal = options.signal
|
|
144
|
+
? AbortSignal.any([options.signal, timer])
|
|
145
|
+
: timer;
|
|
146
|
+
try {
|
|
147
|
+
const response = await this.#fetch(url, { ...init, signal });
|
|
148
|
+
const body = readBody ? await parseBody(response) : undefined;
|
|
149
|
+
return { response, body };
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (options.signal?.aborted) {
|
|
153
|
+
throw new AbortError("The request was aborted", {
|
|
154
|
+
cause: options.signal.reason,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (timer.aborted) {
|
|
158
|
+
throw new TimeoutError(`The request timed out after ${timeout} ms`);
|
|
159
|
+
}
|
|
160
|
+
throw new ConnectionError(`Could not reach ${new URL(url).host}: ${error.message}`, { cause: error });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async #path(template, values = {}) {
|
|
164
|
+
let path = template;
|
|
165
|
+
for (const [, name] of template.matchAll(/\{(\w+)\}/g)) {
|
|
166
|
+
let value = values[name];
|
|
167
|
+
if (value === undefined && name === "project_id") {
|
|
168
|
+
value = await this.projectId();
|
|
169
|
+
}
|
|
170
|
+
if (value === undefined || value === "") {
|
|
171
|
+
throw new NoheadError(`Missing ${name}`);
|
|
172
|
+
}
|
|
173
|
+
path = path.replace(`{${name}}`, encodeURIComponent(String(value)));
|
|
174
|
+
}
|
|
175
|
+
return path;
|
|
176
|
+
}
|
|
177
|
+
#warn(operation, response) {
|
|
178
|
+
if (!this.#warnings)
|
|
179
|
+
return;
|
|
180
|
+
const deprecation = response.headers.get("deprecation");
|
|
181
|
+
if (deprecation && !warnedDeprecations.has(operation)) {
|
|
182
|
+
warnedDeprecations.add(operation);
|
|
183
|
+
const sunset = response.headers.get("sunset");
|
|
184
|
+
const link = response.headers.get("link")?.match(/<([^>]+)>/)?.[1];
|
|
185
|
+
console.warn(`[nohead] ${operation} is deprecated` +
|
|
186
|
+
(sunset ? ` and will be removed after ${sunset}` : "") +
|
|
187
|
+
(link ? `. See ${link}` : ""));
|
|
188
|
+
}
|
|
189
|
+
const usage = response.headers.get("nohead-usage-warning");
|
|
190
|
+
if (usage && !warnedUsage) {
|
|
191
|
+
warnedUsage = true;
|
|
192
|
+
console.warn(`[nohead] Over a plan limit: ${usage}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** Seconds to wait before retrying `error`, or undefined to throw it. */
|
|
197
|
+
function retryDelay(error, attempt, maxRetries) {
|
|
198
|
+
if (attempt >= maxRetries)
|
|
199
|
+
return undefined;
|
|
200
|
+
const inProgress = error.status === 409 && error.details.some((d) => d.code === "in_progress");
|
|
201
|
+
if (!inProgress && !RETRYABLE_STATUSES.has(error.status))
|
|
202
|
+
return undefined;
|
|
203
|
+
const retryAfter = retryAfterSeconds(error.headers);
|
|
204
|
+
if (retryAfter === undefined)
|
|
205
|
+
return backoff(attempt);
|
|
206
|
+
return retryAfter <= MAX_RETRY_AFTER_SECONDS ? retryAfter : undefined;
|
|
207
|
+
}
|
|
208
|
+
/** Exponential backoff with jitter: about 0.5 s, 1 s, 2 s… up to 8 s. */
|
|
209
|
+
function backoff(attempt) {
|
|
210
|
+
const base = Math.min(0.5 * 2 ** attempt, 8);
|
|
211
|
+
return base * (1 - Math.random() * 0.25);
|
|
212
|
+
}
|
|
213
|
+
function sleep(seconds, signal) {
|
|
214
|
+
return new Promise((resolve, reject) => {
|
|
215
|
+
if (signal?.aborted) {
|
|
216
|
+
reject(new AbortError("The request was aborted", { cause: signal.reason }));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const timer = setTimeout(done, seconds * 1000);
|
|
220
|
+
function done() {
|
|
221
|
+
signal?.removeEventListener("abort", aborted);
|
|
222
|
+
resolve();
|
|
223
|
+
}
|
|
224
|
+
function aborted() {
|
|
225
|
+
clearTimeout(timer);
|
|
226
|
+
reject(new AbortError("The request was aborted", { cause: signal.reason }));
|
|
227
|
+
}
|
|
228
|
+
signal?.addEventListener("abort", aborted, { once: true });
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
async function parseBody(response) {
|
|
232
|
+
const text = await response.text();
|
|
233
|
+
if (!text)
|
|
234
|
+
return undefined;
|
|
235
|
+
if (!response.headers.get("content-type")?.includes("json"))
|
|
236
|
+
return text;
|
|
237
|
+
try {
|
|
238
|
+
return JSON.parse(text);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return text;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Query parameters as the API reads them: objects become `key[sub]=…`
|
|
246
|
+
* (`filter[status]=published`), arrays are comma-separated
|
|
247
|
+
* (`expand=author,tags`), dates are ISO 8601.
|
|
248
|
+
*/
|
|
249
|
+
export function appendQuery(params, value, key) {
|
|
250
|
+
if (value === undefined || value === null)
|
|
251
|
+
return;
|
|
252
|
+
if (value instanceof Date) {
|
|
253
|
+
params.append(key, value.toISOString());
|
|
254
|
+
}
|
|
255
|
+
else if (Array.isArray(value)) {
|
|
256
|
+
if (value.length > 0)
|
|
257
|
+
params.append(key, value.map(scalar).join(","));
|
|
258
|
+
}
|
|
259
|
+
else if (typeof value === "object") {
|
|
260
|
+
for (const [name, inner] of Object.entries(value)) {
|
|
261
|
+
appendQuery(params, inner, key ? `${key}[${name}]` : name);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
params.append(key, scalar(value));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function scalar(value) {
|
|
269
|
+
return value instanceof Date ? value.toISOString() : String(value);
|
|
270
|
+
}
|
|
271
|
+
// `nohead-typescript/0.1.0 node/26.9.0`, or none in a browser, which sends its
|
|
272
|
+
// own (Nohead-Client still names the SDK there).
|
|
273
|
+
const RUNTIME = runtimeVersion();
|
|
274
|
+
const USER_AGENT = {};
|
|
275
|
+
if (RUNTIME)
|
|
276
|
+
USER_AGENT["User-Agent"] = `nohead-typescript/${VERSION} ${RUNTIME}`;
|
|
277
|
+
function runtimeVersion() {
|
|
278
|
+
const g = globalThis;
|
|
279
|
+
if (g.Bun?.version)
|
|
280
|
+
return `bun/${g.Bun.version}`;
|
|
281
|
+
if (g.Deno?.version?.deno)
|
|
282
|
+
return `deno/${g.Deno.version.deno}`;
|
|
283
|
+
if (g.process?.versions?.node)
|
|
284
|
+
return `node/${g.process.versions.node}`;
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
function environment() {
|
|
288
|
+
const process = globalThis.process;
|
|
289
|
+
return process?.env ?? {};
|
|
290
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ErrorDetail, ErrorType } from "./generated/types.gen.ts";
|
|
2
|
+
/** The base class of every error this SDK throws. */
|
|
3
|
+
export declare class NoheadError extends Error {
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
/** The API answered with an error status. Subclasses follow the error `type`. */
|
|
7
|
+
export declare class APIError extends NoheadError {
|
|
8
|
+
name: string;
|
|
9
|
+
/** The HTTP status. */
|
|
10
|
+
readonly status: number;
|
|
11
|
+
/** The error `type`, e.g. `validation_error`; undefined if the body had none. */
|
|
12
|
+
readonly type: ErrorType | undefined;
|
|
13
|
+
/** The request's ID (`req_…`), for support requests and logs. */
|
|
14
|
+
readonly requestId: string | undefined;
|
|
15
|
+
/** Field-level details, e.g. `[{ field: "title", code: "required", … }]`. */
|
|
16
|
+
readonly details: ErrorDetail[];
|
|
17
|
+
readonly headers: Headers;
|
|
18
|
+
/** The parsed response body, or its text when it was not JSON. */
|
|
19
|
+
readonly body: unknown;
|
|
20
|
+
constructor(status: number, body: unknown, headers: Headers);
|
|
21
|
+
}
|
|
22
|
+
export declare class InvalidRequestError extends APIError {
|
|
23
|
+
name: string;
|
|
24
|
+
}
|
|
25
|
+
export declare class AuthenticationError extends APIError {
|
|
26
|
+
name: string;
|
|
27
|
+
}
|
|
28
|
+
export declare class PlanLimitExceededError extends APIError {
|
|
29
|
+
name: string;
|
|
30
|
+
}
|
|
31
|
+
export declare class AuthorizationError extends APIError {
|
|
32
|
+
name: string;
|
|
33
|
+
}
|
|
34
|
+
export declare class NotFoundError extends APIError {
|
|
35
|
+
name: string;
|
|
36
|
+
}
|
|
37
|
+
export declare class ConflictError extends APIError {
|
|
38
|
+
name: string;
|
|
39
|
+
}
|
|
40
|
+
/** An `ifMatch` revision was not the current one: reload and try again. */
|
|
41
|
+
export declare class PreconditionFailedError extends APIError {
|
|
42
|
+
name: string;
|
|
43
|
+
/** The resource's current revision, when the API reports it. */
|
|
44
|
+
get currentRevision(): number | undefined;
|
|
45
|
+
}
|
|
46
|
+
export declare class ValidationError extends APIError {
|
|
47
|
+
name: string;
|
|
48
|
+
}
|
|
49
|
+
export declare class RateLimitError extends APIError {
|
|
50
|
+
name: string;
|
|
51
|
+
/** Seconds to wait before retrying, from `Retry-After`. */
|
|
52
|
+
get retryAfter(): number | undefined;
|
|
53
|
+
}
|
|
54
|
+
export declare class InternalServerError extends APIError {
|
|
55
|
+
name: string;
|
|
56
|
+
}
|
|
57
|
+
export declare class ServiceUnavailableError extends APIError {
|
|
58
|
+
name: string;
|
|
59
|
+
}
|
|
60
|
+
/** The request never got a response: DNS, TLS, a reset connection. */
|
|
61
|
+
export declare class ConnectionError extends NoheadError {
|
|
62
|
+
name: string;
|
|
63
|
+
}
|
|
64
|
+
/** An attempt took longer than the `timeout` option. */
|
|
65
|
+
export declare class TimeoutError extends ConnectionError {
|
|
66
|
+
name: string;
|
|
67
|
+
}
|
|
68
|
+
/** The caller's `signal` aborted the request. */
|
|
69
|
+
export declare class AbortError extends NoheadError {
|
|
70
|
+
name: string;
|
|
71
|
+
}
|
|
72
|
+
/** A presigned upload to storage failed (`assets.upload`). */
|
|
73
|
+
export declare class UploadError extends NoheadError {
|
|
74
|
+
name: string;
|
|
75
|
+
/** Storage's HTTP status, when it answered. */
|
|
76
|
+
readonly status: number | undefined;
|
|
77
|
+
constructor(message: string, status?: number, options?: ErrorOptions);
|
|
78
|
+
}
|
|
79
|
+
/** A webhook request's signature or timestamp did not check out. */
|
|
80
|
+
export declare class WebhookVerificationError extends NoheadError {
|
|
81
|
+
name: string;
|
|
82
|
+
}
|
|
83
|
+
/** The error for a response, by its `type`, or by status when it has none. */
|
|
84
|
+
export declare function apiError(status: number, body: unknown, headers: Headers): APIError;
|
|
85
|
+
export declare function retryAfterSeconds(headers: Headers): number | undefined;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/** The base class of every error this SDK throws. */
|
|
2
|
+
export class NoheadError extends Error {
|
|
3
|
+
name = "NoheadError";
|
|
4
|
+
}
|
|
5
|
+
/** The API answered with an error status. Subclasses follow the error `type`. */
|
|
6
|
+
export class APIError extends NoheadError {
|
|
7
|
+
name = "APIError";
|
|
8
|
+
/** The HTTP status. */
|
|
9
|
+
status;
|
|
10
|
+
/** The error `type`, e.g. `validation_error`; undefined if the body had none. */
|
|
11
|
+
type;
|
|
12
|
+
/** The request's ID (`req_…`), for support requests and logs. */
|
|
13
|
+
requestId;
|
|
14
|
+
/** Field-level details, e.g. `[{ field: "title", code: "required", … }]`. */
|
|
15
|
+
details;
|
|
16
|
+
headers;
|
|
17
|
+
/** The parsed response body, or its text when it was not JSON. */
|
|
18
|
+
body;
|
|
19
|
+
constructor(status, body, headers) {
|
|
20
|
+
const error = errorEnvelope(body);
|
|
21
|
+
super(error?.message ?? `Request failed with status ${status}`);
|
|
22
|
+
this.status = status;
|
|
23
|
+
this.type = error?.type;
|
|
24
|
+
this.requestId =
|
|
25
|
+
error?.request_id ?? headers.get("x-request-id") ?? undefined;
|
|
26
|
+
this.details = error?.details ?? [];
|
|
27
|
+
this.headers = headers;
|
|
28
|
+
this.body = body;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class InvalidRequestError extends APIError {
|
|
32
|
+
name = "InvalidRequestError";
|
|
33
|
+
}
|
|
34
|
+
export class AuthenticationError extends APIError {
|
|
35
|
+
name = "AuthenticationError";
|
|
36
|
+
}
|
|
37
|
+
export class PlanLimitExceededError extends APIError {
|
|
38
|
+
name = "PlanLimitExceededError";
|
|
39
|
+
}
|
|
40
|
+
export class AuthorizationError extends APIError {
|
|
41
|
+
name = "AuthorizationError";
|
|
42
|
+
}
|
|
43
|
+
export class NotFoundError extends APIError {
|
|
44
|
+
name = "NotFoundError";
|
|
45
|
+
}
|
|
46
|
+
export class ConflictError extends APIError {
|
|
47
|
+
name = "ConflictError";
|
|
48
|
+
}
|
|
49
|
+
/** An `ifMatch` revision was not the current one: reload and try again. */
|
|
50
|
+
export class PreconditionFailedError extends APIError {
|
|
51
|
+
name = "PreconditionFailedError";
|
|
52
|
+
/** The resource's current revision, when the API reports it. */
|
|
53
|
+
get currentRevision() {
|
|
54
|
+
return this.details.find((d) => d.code === "revision_mismatch")
|
|
55
|
+
?.current_revision;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export class ValidationError extends APIError {
|
|
59
|
+
name = "ValidationError";
|
|
60
|
+
}
|
|
61
|
+
export class RateLimitError extends APIError {
|
|
62
|
+
name = "RateLimitError";
|
|
63
|
+
/** Seconds to wait before retrying, from `Retry-After`. */
|
|
64
|
+
get retryAfter() {
|
|
65
|
+
return retryAfterSeconds(this.headers);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export class InternalServerError extends APIError {
|
|
69
|
+
name = "InternalServerError";
|
|
70
|
+
}
|
|
71
|
+
export class ServiceUnavailableError extends APIError {
|
|
72
|
+
name = "ServiceUnavailableError";
|
|
73
|
+
}
|
|
74
|
+
/** The request never got a response: DNS, TLS, a reset connection. */
|
|
75
|
+
export class ConnectionError extends NoheadError {
|
|
76
|
+
name = "ConnectionError";
|
|
77
|
+
}
|
|
78
|
+
/** An attempt took longer than the `timeout` option. */
|
|
79
|
+
export class TimeoutError extends ConnectionError {
|
|
80
|
+
name = "TimeoutError";
|
|
81
|
+
}
|
|
82
|
+
/** The caller's `signal` aborted the request. */
|
|
83
|
+
export class AbortError extends NoheadError {
|
|
84
|
+
name = "AbortError";
|
|
85
|
+
}
|
|
86
|
+
/** A presigned upload to storage failed (`assets.upload`). */
|
|
87
|
+
export class UploadError extends NoheadError {
|
|
88
|
+
name = "UploadError";
|
|
89
|
+
/** Storage's HTTP status, when it answered. */
|
|
90
|
+
status;
|
|
91
|
+
constructor(message, status, options) {
|
|
92
|
+
super(message, options);
|
|
93
|
+
this.status = status;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** A webhook request's signature or timestamp did not check out. */
|
|
97
|
+
export class WebhookVerificationError extends NoheadError {
|
|
98
|
+
name = "WebhookVerificationError";
|
|
99
|
+
}
|
|
100
|
+
const CLASSES = {
|
|
101
|
+
invalid_request: InvalidRequestError,
|
|
102
|
+
authentication_error: AuthenticationError,
|
|
103
|
+
plan_limit_exceeded: PlanLimitExceededError,
|
|
104
|
+
authorization_error: AuthorizationError,
|
|
105
|
+
not_found: NotFoundError,
|
|
106
|
+
conflict: ConflictError,
|
|
107
|
+
precondition_failed: PreconditionFailedError,
|
|
108
|
+
validation_error: ValidationError,
|
|
109
|
+
rate_limited: RateLimitError,
|
|
110
|
+
internal_error: InternalServerError,
|
|
111
|
+
service_unavailable: ServiceUnavailableError,
|
|
112
|
+
};
|
|
113
|
+
/** The error for a response, by its `type`, or by status when it has none. */
|
|
114
|
+
export function apiError(status, body, headers) {
|
|
115
|
+
const type = errorEnvelope(body)?.type;
|
|
116
|
+
const Class = (type && CLASSES[type]) ||
|
|
117
|
+
(status === 503
|
|
118
|
+
? ServiceUnavailableError
|
|
119
|
+
: status >= 500
|
|
120
|
+
? InternalServerError
|
|
121
|
+
: APIError);
|
|
122
|
+
return new Class(status, body, headers);
|
|
123
|
+
}
|
|
124
|
+
export function retryAfterSeconds(headers) {
|
|
125
|
+
const value = headers.get("retry-after");
|
|
126
|
+
if (!value)
|
|
127
|
+
return undefined;
|
|
128
|
+
const seconds = Number(value);
|
|
129
|
+
if (Number.isFinite(seconds))
|
|
130
|
+
return Math.max(0, seconds);
|
|
131
|
+
const date = Date.parse(value);
|
|
132
|
+
return Number.isNaN(date)
|
|
133
|
+
? undefined
|
|
134
|
+
: Math.max(0, (date - Date.now()) / 1000);
|
|
135
|
+
}
|
|
136
|
+
function errorEnvelope(body) {
|
|
137
|
+
if (typeof body !== "object" || body === null || !("error" in body)) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
const error = body.error;
|
|
141
|
+
return typeof error === "object" && error !== null
|
|
142
|
+
? error
|
|
143
|
+
: undefined;
|
|
144
|
+
}
|