@nohead/sdk 0.8.0 → 0.10.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 CHANGED
@@ -5,6 +5,97 @@ Changes to `@nohead/sdk` that you can notice. Versions follow
5
5
  releases; a change that could break your code is a major one. Each release's
6
6
  section is its GitHub release's notes.
7
7
 
8
+ ## 0.10.0
9
+
10
+ - Rich text reads as HTML or Markdown: `richText: "html"` (or `"markdown"`)
11
+ on the client, or in a request's options, sends the API's `rich_text`
12
+ query parameter on the methods that return records or drafts, so a rich
13
+ text field's value in `data` is a string instead of the document. A
14
+ request's `richText` overrides the client's, and `withOptions` can change
15
+ it. Writing Markdown already worked: `data` is sent as it is.
16
+
17
+ ## 0.9.0
18
+
19
+ - **Breaking:** the client no longer reads `NOHEAD_API_URL`: a stray value
20
+ sent requests, with the API key, to another host. The base URL is
21
+ `baseUrl`, else `https://api.nohead.io`; pass `baseUrl` to use another
22
+ API, as in `new Nohead({ baseUrl: process.env.NOHEAD_API_URL })`.
23
+
24
+ - An `APIError`'s `message` names the status, the error type and the
25
+ request ID along with the API's message, as in the Python and Ruby SDKs:
26
+ `422 validation_error: Title is required (request req_…)`, where it was
27
+ only `Title is required`. The API's own text is `apiMessage`.
28
+
29
+ - `unwrapWebhook` and `webhooks.unwrap` throw a `WebhookVerificationError`
30
+ for an empty secret (`""` or `"whsec_"`, as from an unset environment
31
+ variable), not the platform's `DOMException`.
32
+
33
+ - `DEFAULT_BASE_URL` is exported: the API's address (`https://api.nohead.io`)
34
+ that a client uses when `baseUrl` isn't set.
35
+
36
+ - `timeout` is now the longest an attempt may go without progress: to get
37
+ the response, then between chunks of its body. A large response that keeps
38
+ arriving no longer times out; one that stalls still does. An upload's bytes
39
+ get as long as sending them at 64 KiB/s takes, if that is longer.
40
+
41
+ - `withOptions({ timeout, maxRetries, headers, onRetry })` copies a client
42
+ with other settings; the copy shares the key's project lookup. `headers`
43
+ are added to the client's.
44
+
45
+ - Redirects are no longer followed, which could send the API key to another
46
+ host or a write as a GET: a `3xx` throws an `APIError` with `type`
47
+ `redirect` and the message
48
+ `301 redirect: The API answered with a redirect to <Location>; check the base URL`.
49
+ A redirect from storage fails an upload with `UploadError`.
50
+
51
+ - Headers are matched by name in any case, and each is sent once. A call's
52
+ `headers` replace the client's, which replace the SDK's own; the headers
53
+ the request options set (`Idempotency-Key`, `If-Match`,
54
+ `Nohead-Change-Note`, `Content-Type`) always win. A client header
55
+ `authorization` was sent along with the SDK's `Authorization`.
56
+
57
+ - Empty options count as unset: `apiKey`, `baseUrl` and `projectId` of `""`
58
+ fall back to `NOHEAD_API_KEY` and the defaults, an `idempotencyKey` of
59
+ `""` gets a generated key, an upload's `filename` and `content_type` of
60
+ `""` their defaults, and `imageUrl`'s empty options are left out.
61
+
62
+ - A `baseUrl` that isn't an `http` or `https` URL throws a `NoheadError`
63
+ when the client is made, not on its first request.
64
+
65
+ - A path value of `.` or `..` (`records.get("..")`) throws a `NoheadError`,
66
+ `Invalid record_id: ".."`, before anything is sent: URLs resolve them.
67
+
68
+ - `ifMatch` that isn't a revision number, or an object with one, throws a
69
+ `TypeError` before anything is sent, where an object without one sent
70
+ `If-Match: "undefined"`.
71
+
72
+ - `NaN` and `Infinity` in a body throw a `TypeError`, where they were sent as
73
+ `null`.
74
+
75
+ - A list whose response isn't the API's list (a proxy's page, say) throws a
76
+ `NoheadError`, `The API's response could not be read (HTTP 200)`, not a
77
+ `TypeError` or a page without items.
78
+
79
+ - `Retry-After` is read as a number of seconds (`120`, `1.5`) or an HTTP
80
+ date, in all three of its forms, in GMT. Anything else (`1e3`, `0x10`,
81
+ `-1`, an ISO 8601 time) is ignored and the usual backoff applies.
82
+
83
+ - An error's details that aren't objects are kept, as
84
+ `{ code: "unknown", message: <the value as text> }`, rather than dropped.
85
+
86
+ - `assets.upload` sends a stream with `Content-Length: byte_size`, which the
87
+ upload URL is signed for; storage could refuse it without one. Given a string,
88
+ it throws a `TypeError`: pass `await fs.openAsBlob(path)` for a file. Its
89
+ `UploadError` messages name the filename you passed (or `upload`).
90
+
91
+ - `unwrapWebhook` and `webhooks.unwrap` take an unset `secret` (`undefined`,
92
+ as from a missing environment variable) and throw `WebhookVerificationError`
93
+ `The webhook secret is empty`, not a `TypeError`.
94
+
95
+ - `imageUrl` throws a `TypeError` for an unknown `fit`
96
+ (`fit must be one of contain, cover, fill`), where it wrote `rs:undefined`
97
+ into the URL.
98
+
8
99
  ## 0.8.0
9
100
 
10
101
  - `Asset` has `file_url`, the file as uploaded, which downloads under its
package/README.md CHANGED
@@ -22,6 +22,7 @@ for await (const post of nohead.records.list("posts", {
22
22
  - [Configuration](#configuration)
23
23
  - [Records](#records)
24
24
  - [Drafts](#drafts)
25
+ - [Rich text](#rich-text)
25
26
  - [Pagination](#pagination)
26
27
  - [Errors](#errors)
27
28
  - [Retries and idempotency](#retries-and-idempotency)
@@ -40,28 +41,40 @@ for await (const post of nohead.records.list("posts", {
40
41
  npm install @nohead/sdk
41
42
  ```
42
43
 
43
- The package is ESM; Node 22+ can also `require()` it. It runs on Node 22, 24 and 26, Bun, Deno, Cloudflare Workers and other runtimes with `fetch` and Web Crypto. Use it on servers: API keys are secrets.
44
+ The package is ESM; Node 22+ can also `require()` it. It runs on Node 22, 24 and 26, Bun, Deno, Cloudflare Workers and other runtimes with `fetch`, Web Crypto and ES2024. Use it on servers: API keys are secrets.
45
+
46
+ Until 1.0, any release may break your code: pin a version, and read the changelog's **Breaking** entries before upgrading.
44
47
 
45
48
  ## Configuration
46
49
 
47
50
  ```ts
48
51
  const nohead = new Nohead({
49
- apiKey: process.env.NOHEAD_API_KEY, // default: NOHEAD_API_KEY
50
- baseUrl: "https://api.nohead.io", // default: NOHEAD_API_URL, else production
52
+ apiKey: process.env.NOHEAD_API_KEY, // the default
53
+ timeout: 10_000,
54
+ maxRetries: 3,
51
55
  })
52
56
  ```
53
57
 
54
- | Option | Default | |
55
- | ------------ | ---------------------------------------------- | ---------------------------------------------------------------------------------- |
56
- | `apiKey` | `NOHEAD_API_KEY` | A project API key (`nohead_sk_…`, or `nohead_pk_…` when published-only). Required. |
57
- | `baseUrl` | `NOHEAD_API_URL`, else `https://api.nohead.io` | |
58
- | `projectId` | the key's project | Looked up once with `GET /v1/me` when omitted. |
59
- | `maxRetries` | `2` | See [retries](#retries-and-idempotency). |
60
- | `timeout` | `60000` | Milliseconds per attempt. |
61
- | `fetch` | `globalThis.fetch` | For proxies, instrumentation and tests. |
62
- | `headers` | `{}` | Added to every request. |
63
- | `warnings` | `true` | Logs deprecation and plan usage warnings, once each. |
64
- | `onRetry` | | Called before each retry. See [retries](#retries-and-idempotency). |
58
+ | Option | Default | |
59
+ | ------------ | ----------------------- | ----------------------------------------------------------------------------------------- |
60
+ | `apiKey` | `NOHEAD_API_KEY` | A project API key (`nohead_sk_…`, or `nohead_pk_…` when published-only). Required. |
61
+ | `baseUrl` | `https://api.nohead.io` | For a proxy, or a mock server in tests. |
62
+ | `projectId` | the key's project | Looked up once with `GET /v1/me` when omitted. |
63
+ | `maxRetries` | `2` | See [retries](#retries-and-idempotency). |
64
+ | `timeout` | `60000` | Milliseconds an attempt may go without progress. See [retries](#retries-and-idempotency). |
65
+ | `fetch` | `globalThis.fetch` | For proxies, instrumentation and tests. |
66
+ | `headers` | `{}` | Added to every request. |
67
+ | `warnings` | `true` | Logs deprecation and plan usage warnings, once each. |
68
+ | `onRetry` | | Called before each retry. See [retries](#retries-and-idempotency). |
69
+ | `richText` | `"document"` | How rich text reads: `"html"` or `"markdown"`. See [rich text](#rich-text). |
70
+
71
+ An empty option counts as unset, as an empty environment variable does: `apiKey: ""` falls back to `NOHEAD_API_KEY`, and `baseUrl: ""` to `https://api.nohead.io`. The base URL comes only from `baseUrl`, never from the environment. A `baseUrl` that isn't an `http` or `https` URL throws.
72
+
73
+ `withOptions` copies a client with another `timeout`, `maxRetries`, `onRetry` or `richText`, or more `headers` (added to the client's). The copy shares the key's project, so it doesn't look it up again:
74
+
75
+ ```ts
76
+ const patient = nohead.withOptions({ timeout: 120_000, maxRetries: 5 })
77
+ ```
65
78
 
66
79
  API keys belong to a project, so methods like `collections.list()` need no project ID. Collections can be named by ID or slug everywhere.
67
80
 
@@ -131,6 +144,24 @@ await nohead.drafts.merge(draft.id) // or drafts.schedule(draft.id, { merge_at }
131
144
  - `drafts.discard` deletes one for good.
132
145
  - Drafts take the type of their data as records do: `drafts.get<Post>(id)` is a `Nohead.Draft<Post>` (also exported as `NoheadDraft`).
133
146
 
147
+ ## Rich text
148
+
149
+ A rich text field takes Markdown or a ProseMirror document, and reads as the document. To read it as HTML or Markdown, set `richText` on the client, or on one request:
150
+
151
+ ```ts
152
+ await nohead.records.update(id, { data: { body: "Hello **world**" } })
153
+
154
+ const site = nohead.withOptions({ richText: "html" })
155
+ const post = await site.records.get<{ body: string }>(id)
156
+ const html = `<article>${post.data.body}</article>` // safe to put in a page as it is
157
+
158
+ await nohead.drafts.get(draftId, {}, { richText: "markdown" })
159
+ ```
160
+
161
+ - A rich text field's value is then a string: type it as one in the data's type, as above.
162
+ - It applies to the methods that return records or drafts, and to the records they embed with `expand`; a request's `richText` overrides the client's. History (`revisions`) and webhooks keep documents.
163
+ - A string you write is read as Markdown, so write back Markdown or the document, never the HTML: with `richText: "html"` on the client, writes answer with HTML too.
164
+
134
165
  ## Pagination
135
166
 
136
167
  List methods return a promise of the first page that you can also loop over:
@@ -190,6 +221,10 @@ Every error is a `NoheadError`. API errors are `APIError`s with `status`, `type`
190
221
  | `InternalServerError` | 500 and other 5xx |
191
222
  | `ServiceUnavailableError` | 503 |
192
223
 
224
+ An API error's `message` names the status, the type and the request ID along with the API's message, so logging it is enough to look the request up: `422 validation_error: Title is required (request req_…)`. The API's own text is `apiMessage`, to show a person.
225
+
226
+ The client follows no redirects: a `3xx` is an `APIError` (`type` `redirect`) that names where it pointed, which usually means the base URL is wrong.
227
+
193
228
  Other errors: `ConnectionError`, `TimeoutError` (a `ConnectionError`), `AbortError` (your `signal` fired), `UploadError` and `WebhookVerificationError`.
194
229
 
195
230
  ```ts
@@ -241,6 +276,10 @@ Every method takes request options as its last argument:
241
276
 
242
277
  Writes that change records or the schema also take `changeNote`, a reason kept with the revision, schema change or migration they make. Record and draft writes take `ifMatch` (see [concurrency](#concurrency)).
243
278
 
279
+ Headers are matched by name in any case, and each is sent once: a call's `headers` replace the client's, which replace the SDK's own (`Authorization`, `User-Agent`…). The headers the request options set (`Idempotency-Key`, `If-Match`, `Nohead-Change-Note`, and `Content-Type` for a body) always win.
280
+
281
+ **Timeouts:** `timeout` is the longest an attempt may go without progress: to connect and get the response, then between chunks of its body. A response that keeps arriving never times out; one that stalls for `timeout` throws `TimeoutError`, and is retried. fetch shows no progress while it sends an upload's bytes, so storage gets as long as sending them at 64 KiB/s takes, when that is longer than `timeout`.
282
+
244
283
  ## Concurrency
245
284
 
246
285
  Pass the revision you read to make sure nobody changed the record since:
@@ -302,11 +341,11 @@ Thumbnails: an image's `preview_url` fits in 640×640, as WebP.
302
341
  2. Sends the bytes straight to storage.
303
342
  3. Completes the upload, which checks the file, and returns the `ready` asset.
304
343
 
305
- **Errors:** `UploadError` if storage refuses the bytes; `ValidationError` if the file fails the checks.
344
+ A string is neither a path nor the content: it throws `TypeError` (use `await openAsBlob(path)` from `node:fs`). A stream is sent with `Content-Length: byte_size`, which the upload URL is signed for.
306
345
 
307
- **Resuming:** pass an `idempotencyKey` (`upload(file, {}, { idempotencyKey })`) and running the same upload again resumes it: an asset that is already `ready` comes back without sending the bytes again. Once the upload's URL has expired (its `upload.expires_at`), it throws `UploadError`: start over with a new key.
346
+ **Errors:** `UploadError` whenever the bytes don't reach storage: `Storage refused the upload of cover.jpg (403)` when it refuses them, with its `status`, or `Could not upload cover.jpg: …` when it can't be reached or times out, with that error as the `cause`. `ValidationError` if the file fails the checks.
308
347
 
309
- **Uploading from a browser:** create the upload on your server with `createUpload`, `PUT` the file from the browser, then `complete` it.
348
+ **Resuming:** pass an `idempotencyKey` (`upload(file, {}, { idempotencyKey })`) and running the same upload again resumes it: an asset that is already `ready` comes back without sending the bytes again. Once the upload's URL has expired (its `upload.expires_at`), it throws `UploadError`: start over with a new key.
310
349
 
311
350
  **Renaming and alt text:** `update` changes an asset's `filename` and `alt` text, the default wherever the asset is used (`expand` returns it with the asset). The file itself never changes.
312
351
 
@@ -379,7 +418,7 @@ export async function POST(request: Request) {
379
418
  await request.text(),
380
419
  request.headers,
381
420
  {
382
- secret: process.env.NOHEAD_WEBHOOK_SECRET!,
421
+ secret: process.env.NOHEAD_WEBHOOK_SECRET,
383
422
  }
384
423
  )
385
424
  if (event.type === "record.published" && event.data.record) {
@@ -391,7 +430,7 @@ export async function POST(request: Request) {
391
430
 
392
431
  With `"max"`, Next.js serves the old version to the next request while it fetches the new one; pass `{ expire: 0 }` instead to drop it at once.
393
432
 
394
- Event types are those of the activity log (`record.published`, `record.restored`, `field.updated`, `asset.purged`, …), and a webhook subscribes to types or wildcards (`record.*`, `*`). Narrowing by `type` types `event.data`, and `unwrap<Post>` types the records and drafts in it. `unwrap` checks the signature and the timestamp (Standard Webhooks), and throws `WebhookVerificationError` if either is off. After `rotateSecret`, requests carry a signature for the old secret and the new one for 24 hours, so switch the receiver's secret within that day (or end it sooner with `webhooks.secrets.retire`). Pass the raw body: parsing and re-serializing JSON changes the bytes.
433
+ Event types are those of the activity log (`record.published`, `record.restored`, `field.updated`, `asset.purged`, …), and a webhook subscribes to types or wildcards (`record.*`, `*`). Narrowing by `type` types `event.data`, and `unwrap<Post>` types the records and drafts in it. `unwrap` checks the signature and the timestamp (Standard Webhooks), and throws `WebhookVerificationError` if either is off, or if the secret is empty or unset (`The webhook secret is empty`). After `rotateSecret`, requests carry a signature for the old secret and the new one for 24 hours, so switch the receiver's secret within that day (or end it sooner with `webhooks.secrets.retire`). Pass the raw body: parsing and re-serializing JSON changes the bytes.
395
434
 
396
435
  A receiver that never calls the API can use `unwrapWebhook` from `@nohead/sdk/webhooks` without a client. Events can arrive more than once, so deduplicate by the `webhook-id` header.
397
436
 
@@ -434,28 +473,37 @@ The SDK covers every operation an API key can call. Organizations, projects, mem
434
473
  ```bash
435
474
  npm install # also builds dist/
436
475
  npm test # unit and contract tests
437
- npm run lint && npm run typecheck && npm run format:check
476
+ npm run lint && npm run typecheck && npm run typecheck:smoke && npm run format:check
438
477
  npm run generate # after updating openapi.json
439
478
  npm run samples # after changing test/calls.ts (the docs' code samples)
440
479
  NOHEAD_API_URL=http://localhost:3000 NOHEAD_API_KEY=nohead_sk_… npm run smoke
441
480
  ```
442
481
 
482
+ Before pushing, also check that the generated code and the samples are current, as CI does:
483
+
484
+ ```bash
485
+ npm run generate && npm run samples && git diff --exit-code src/generated samples.json
486
+ ```
487
+
488
+ The unit tests' mock client (`mockClient` in `test/helpers.ts`) checks every request to the API, and every reply to one, against `openapi.json`, and fails the test on a mismatch; build replies from the contract's examples (`exampleOf`), and mark one that breaks the contract on purpose with `raw()`.
489
+
443
490
  **How the code is organized:**
444
491
 
445
- - `openapi.json` is the API's published contract. `npm run generate` derives two things from it:
492
+ - `openapi.json` is the API's published contract. `npm run generate` derives three things from it:
446
493
  - the types, in `src/generated/types.gen.ts` (hey-api)
447
494
  - the operation table, in `src/generated/operations.gen.ts`
495
+ - the schema types the SDK exports, in `src/generated/schemas.gen.ts`
448
496
  - The methods in `src/resources` are written by hand.
449
497
  - `test/contract.test.ts` calls every public method. It fails when an API-key operation in the contract has no method, when a query parameter is never sent (no method takes it, or no call passes it), or when a request doesn't match its operation (method, path, query parameters and JSON body, against their schemas). Each call gets an example of its operation's response, built from the contract's schema, and must return it.
450
498
 
451
- The smoke test (`smoke/smoke.ts`) runs the core flow against a real API, using the built package. Nohead's own CI runs it on every API contract change.
499
+ The smoke test (`smoke/smoke.ts`) runs the core flow against a real API, using the built package. The Nohead API repository runs it in its end-to-end tests and before every release.
452
500
 
453
501
  ## Releasing
454
502
 
455
- 1. Bump the version in `package.json` and `src/version.ts`.
503
+ 1. Bump the version in `package.json` and `src/version.ts`, then `npm install --package-lock-only`, so `package-lock.json` records it.
456
504
  2. Add a section for it to `CHANGELOG.md` (`## 1.2.3`), which becomes the release's notes.
457
505
  3. Merge to `main`. Its ruleset requires the **CI passed** check, so the commit goes through a pull request or a branch whose CI passed, and force pushes are refused.
458
- 4. Run the **SDK release** workflow in the Nohead API repository. It runs this commit's smoke test against the API and pushes the tag `v1.2.3`. Nobody else can push `v*` tags: a tag ruleset lets only that workflow's deploy key through.
506
+ 4. Run the **SDK release** workflow in the Nohead API repository. It runs this commit's smoke test against the API and pushes the tag `v1.2.3`. A tag ruleset lets only deploy keys push `v*` tags.
459
507
  5. The tag starts `.github/workflows/release.yml`. A build job checks the tag is on `main` and matches the version and its notes, and packs the package, with no publishing credential. Its publishing job runs in the `release` environment, which only `v*` tags can use, and the registry's trusted publisher accepts only that environment. It stages that package on npm with provenance through trusted publishing (no token), then creates a draft GitHub release.
460
508
  6. Approve the staged version with 2FA, on npmjs.com, or with `npm stage list @nohead/sdk` and `npm stage approve <stage-id>`. Only then is it installable. Publish the draft GitHub release.
461
509
 
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { PagePromise } from "./api-promise.ts";
2
- import { type ClientOptions, type RequestOptions } from "./core.ts";
2
+ import { type ClientOptions, type CopyOptions, type RichTextOptions } from "./core.ts";
3
3
  import type * as G from "./generated/types.gen.ts";
4
4
  import { Assets } from "./resources/assets.ts";
5
5
  import { Drafts } from "./resources/drafts.ts";
@@ -31,11 +31,19 @@ export declare class Nohead {
31
31
  readonly me: Me;
32
32
  readonly health: Health;
33
33
  constructor(options?: ClientOptions);
34
+ /**
35
+ * A copy of the client with another `timeout`, `maxRetries`, `onRetry` or
36
+ * `richText`, or more `headers` (added to this client's). It shares the key's project,
37
+ * so it doesn't look it up again.
38
+ *
39
+ * await nohead.withOptions({ timeout: 5_000 }).records.get(id)
40
+ */
41
+ withOptions(options: CopyOptions): Nohead;
34
42
  /**
35
43
  * Full-text search across the key's project (or some of its
36
44
  * collections), most relevant first, through the first 1,000 hits.
37
45
  */
38
- search<D = RecordData>(query: string, params?: ProjectSearchParams, options?: RequestOptions): PagePromise<NoheadRecord<D>, SearchMeta>;
46
+ search<D = RecordData>(query: string, params?: ProjectSearchParams, options?: RichTextOptions): PagePromise<NoheadRecord<D>, SearchMeta>;
39
47
  }
40
48
  /** The main types, also reachable as `Nohead.Record`, `Nohead.Collection`… */
41
49
  export declare namespace Nohead {
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Core } from "./core.js";
1
+ import { Core, } from "./core.js";
2
2
  import { Assets } from "./resources/assets.js";
3
3
  import { Drafts } from "./resources/drafts.js";
4
4
  import { AuditEvents, FeatureFlags, Health, Me } from "./resources/other.js";
@@ -28,7 +28,7 @@ export class Nohead {
28
28
  health;
29
29
  #core;
30
30
  constructor(options = {}) {
31
- const core = new Core(options);
31
+ const core = options[COPY_OF] ?? new Core(options);
32
32
  this.#core = core;
33
33
  this.records = new Records(core);
34
34
  this.revisions = new RevisionsById(core);
@@ -43,6 +43,16 @@ export class Nohead {
43
43
  this.me = new Me(core);
44
44
  this.health = new Health(core);
45
45
  }
46
+ /**
47
+ * A copy of the client with another `timeout`, `maxRetries`, `onRetry` or
48
+ * `richText`, or more `headers` (added to this client's). It shares the key's project,
49
+ * so it doesn't look it up again.
50
+ *
51
+ * await nohead.withOptions({ timeout: 5_000 }).records.get(id)
52
+ */
53
+ withOptions(options) {
54
+ return new Nohead({ [COPY_OF]: this.#core.withOptions(options) });
55
+ }
46
56
  /**
47
57
  * Full-text search across the key's project (or some of its
48
58
  * collections), most relevant first, through the first 1,000 hits.
@@ -61,3 +71,5 @@ export class Nohead {
61
71
  }, options);
62
72
  }
63
73
  }
74
+ // The core a copy is made with (`withOptions`), out of ClientOptions' sight.
75
+ const COPY_OF = Symbol("copyOf");
package/dist/core.d.ts CHANGED
@@ -2,6 +2,7 @@ import { APIPromise, PagePromise } from "./api-promise.ts";
2
2
  import { NoheadError } from "./errors.ts";
3
3
  import { type OperationId, type Operations } from "./generated/operations.gen.ts";
4
4
  import type { ListMeta } from "./generated/types.gen.ts";
5
+ import type { RichText } from "./types.ts";
5
6
  export declare const DEFAULT_BASE_URL = "https://api.nohead.io";
6
7
  export interface ClientOptions {
7
8
  /**
@@ -9,13 +10,19 @@ export interface ClientOptions {
9
10
  * Defaults to `NOHEAD_API_KEY`.
10
11
  */
11
12
  apiKey?: string;
12
- /** Defaults to `NOHEAD_API_URL`, else `https://api.nohead.io`. */
13
+ /**
14
+ * The API's address. Defaults to `https://api.nohead.io`; the environment
15
+ * isn't read for it, so the key only goes where the code says.
16
+ */
13
17
  baseUrl?: string;
14
18
  /** The key's project. Looked up once with `GET /v1/me` when omitted. */
15
19
  projectId?: string;
16
20
  /** Retries of failed requests (see README, "Retries"). Default 2. */
17
21
  maxRetries?: number;
18
- /** Milliseconds per attempt. Default 60 000. */
22
+ /**
23
+ * Milliseconds an attempt may go without progress (connecting, waiting for
24
+ * the response, then between chunks of its body). Default 60 000.
25
+ */
19
26
  timeout?: number;
20
27
  /** A `fetch` implementation, for proxies, instrumentation or tests. */
21
28
  fetch?: typeof fetch;
@@ -25,7 +32,15 @@ export interface ClientOptions {
25
32
  warnings?: boolean;
26
33
  /** Called before each retry, for example to report it. */
27
34
  onRetry?: (retry: RetryEvent) => void;
35
+ /**
36
+ * How rich text fields read in the records and drafts returned: as HTML
37
+ * or Markdown strings, or as the stored document (the API's default). A
38
+ * request's `richText` overrides it.
39
+ */
40
+ richText?: RichText;
28
41
  }
42
+ /** What `withOptions` changes in a client's copy. */
43
+ export type CopyOptions = Pick<ClientOptions, "timeout" | "maxRetries" | "headers" | "onRetry" | "richText">;
29
44
  /** A retry about to happen (`onRetry`). */
30
45
  export interface RetryEvent {
31
46
  /** 1 for the first retry of the request, 2 for the second… */
@@ -43,12 +58,21 @@ export interface RequestOptions {
43
58
  /** Sent as `Idempotency-Key` on writes. Generated when omitted. */
44
59
  idempotencyKey?: string;
45
60
  signal?: AbortSignal;
46
- /** Milliseconds per attempt, overriding the client's. */
61
+ /** Milliseconds without progress, overriding the client's `timeout`. */
47
62
  timeout?: number;
48
63
  /** Retries, overriding the client's. */
49
64
  maxRetries?: number;
50
65
  headers?: Record<string, string>;
51
66
  }
67
+ /** Options of operations that return records or drafts. */
68
+ export interface RichTextOptions extends RequestOptions {
69
+ /**
70
+ * How rich text fields read in the records or drafts returned (and in
71
+ * records they embed with `expand`), overriding the client's `richText`:
72
+ * `"html"` or `"markdown"` strings, or the stored `"document"`.
73
+ */
74
+ richText?: RichText;
75
+ }
52
76
  /** Options of writes that change records or the schema. */
53
77
  export interface WriteOptions extends RequestOptions {
54
78
  /**
@@ -75,15 +99,21 @@ export interface ConditionalWriteOptions extends WriteOptions, ConditionalReques
75
99
  type HeadersOf<Op extends OperationId> = Operations[Op]["data"] extends {
76
100
  headers?: infer H;
77
101
  } ? keyof NonNullable<H> : never;
102
+ /** The query parameters an operation declares. */
103
+ type QueryOf<Op extends OperationId> = Operations[Op]["data"] extends {
104
+ query?: infer Q;
105
+ } ? keyof NonNullable<Q> : never;
78
106
  /**
79
- * The options an operation takes: `ifMatch` and `changeNote` only where it
80
- * declares `If-Match` and `Nohead-Change-Note`, so a method can't offer them
81
- * elsewhere.
107
+ * The options an operation takes: `ifMatch`, `changeNote` and `richText`
108
+ * only where it declares `If-Match`, `Nohead-Change-Note` and `rich_text`,
109
+ * so a method can't offer them elsewhere.
82
110
  */
83
111
  export type OptionsOf<Op extends OperationId> = RequestOptions & ("If-Match" extends HeadersOf<Op> ? Pick<ConditionalRequestOptions, "ifMatch"> : {
84
112
  ifMatch?: never;
85
113
  }) & ("Nohead-Change-Note" extends HeadersOf<Op> ? Pick<WriteOptions, "changeNote"> : {
86
114
  changeNote?: never;
115
+ }) & ("rich_text" extends QueryOf<Op> ? Pick<RichTextOptions, "richText"> : {
116
+ richText?: never;
87
117
  });
88
118
  export interface RequestInput {
89
119
  path?: Record<string, string | number | undefined>;
@@ -92,15 +122,24 @@ export interface RequestInput {
92
122
  }
93
123
  export declare class Core {
94
124
  #private;
95
- constructor(options: ClientOptions);
125
+ constructor(options: ClientOptions, project?: {
126
+ id?: Promise<string>;
127
+ });
128
+ /** A copy with other settings, sharing the project lookup. */
129
+ withOptions(options: CopyOptions): Core;
96
130
  /** The API key's project, from `GET /v1/me` the first time. */
97
131
  projectId(): Promise<string>;
98
132
  request<Op extends OperationId>(operation: Op, input?: RequestInput, options?: OptionsOf<Op>): APIPromise<Operations[Op]["response"]>;
99
133
  /** A list operation as pages of `Item`. */
100
- paginate<Item, Meta extends ListMeta = ListMeta>(operation: OperationId, input: RequestInput, options?: RequestOptions): PagePromise<Item, Meta>;
101
- /** `fetch` with this client's timeout and retries, for presigned URLs. */
134
+ paginate<Item, Meta extends ListMeta = ListMeta, Op extends OperationId = OperationId>(operation: Op, input: RequestInput, options?: OptionsOf<Op>): PagePromise<Item, Meta>;
135
+ /**
136
+ * `fetch` with this client's retries, for an upload's presigned URL. fetch
137
+ * shows no upload progress, so each attempt may take as long as sending
138
+ * `byteSize` at 64 KiB/s, if that is longer than the timeout.
139
+ */
102
140
  fetchWithRetries(url: string, init: RequestInit, options: RequestOptions & {
103
141
  retry: boolean;
142
+ byteSize: number;
104
143
  }): Promise<Response>;
105
144
  }
106
145
  /**