@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/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
- #projectId;
26
- constructor(options) {
27
- const env = environment();
28
- // Empty environment variables count as unset.
29
- const apiKey = options.apiKey ?? (env.NOHEAD_API_KEY || undefined);
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 = withoutTrailingSlashes(options.baseUrl ?? (env.NOHEAD_API_URL || DEFAULT_BASE_URL));
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
- this.#projectId = Promise.resolve(options.projectId);
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.#projectId ??= this.request("me_get", {}).then((me) => {
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.#projectId = undefined;
62
+ this.#project.id = undefined;
53
63
  throw error;
54
64
  });
55
- return this.#projectId;
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
- /** `fetch` with this client's timeout and retries, for presigned URLs. */
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
- const headers = {
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
- ...this.#headers,
105
- };
128
+ });
129
+ setHeaders(headers, this.#headers);
130
+ setHeaders(headers, options.headers);
131
+ const own = {};
106
132
  if (input.body !== undefined)
107
- headers["Content-Type"] = "application/json";
133
+ own["Content-Type"] = "application/json";
108
134
  if (method !== "GET") {
109
- headers["Idempotency-Key"] = options.idempotencyKey ?? crypto.randomUUID();
135
+ own["Idempotency-Key"] = options.idempotencyKey || crypto.randomUUID();
110
136
  }
111
- if (options.ifMatch !== undefined) {
112
- const revision = typeof options.ifMatch === "object"
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
- headers["Nohead-Change-Note"] = encodeURIComponent(wellFormed(options.changeNote));
121
- Object.assign(headers, options.headers);
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 ? undefined : JSON.stringify(input.body),
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 = AbortSignal.timeout(timeout);
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
- const response = await this.#fetch(url, { ...init, signal });
164
- const body = readBody ? await parseBody(response) : undefined;
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 [, name] of template.matchAll(/\{(\w+)\}/g)) {
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.text();
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; a loop, as a regex backtracks on long runs of them. */
312
- function withoutTrailingSlashes(url) {
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
- return url.slice(0, end);
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
- /** The API answered with an error status. Subclasses follow the error `type`. */
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
- /** The API answered with an error status. Subclasses follow the error `type`. */
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
- const error = errorEnvelope(body);
24
- super(error?.message ?? `Request failed with status ${status}`);
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
- const seconds = Number(value);
135
- if (Number.isFinite(seconds))
136
- return Math.max(0, seconds);
137
- const date = Date.parse(value);
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
- // Only objects: currentRevision reads each detail's fields.
153
- details: Array.isArray(details)
154
- ? details.filter(isObject)
155
- : undefined,
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, DownloadUrl, 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";
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";