@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 +91 -0
- package/README.md +72 -24
- package/dist/client.d.ts +10 -2
- package/dist/client.js +14 -2
- package/dist/core.d.ts +48 -9
- package/dist/core.js +167 -46
- package/dist/errors.d.ts +13 -1
- package/dist/errors.js +74 -16
- package/dist/generated/operations.gen.d.ts +2 -0
- package/dist/generated/operations.gen.js +24 -0
- package/dist/generated/types.gen.d.ts +172 -37
- 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 +5 -1
- package/dist/resources/assets.js +23 -14
- package/dist/resources/drafts.d.ts +9 -9
- package/dist/resources/records.d.ts +14 -14
- package/dist/types.d.ts +7 -0
- 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/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
|
|
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
|
|
50
|
-
|
|
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`
|
|
57
|
-
| `baseUrl` | `
|
|
58
|
-
| `projectId` | the key's project
|
|
59
|
-
| `maxRetries` | `2`
|
|
60
|
-
| `timeout` | `60000`
|
|
61
|
-
| `fetch` | `globalThis.fetch`
|
|
62
|
-
| `headers` | `{}`
|
|
63
|
-
| `warnings` | `true`
|
|
64
|
-
| `onRetry` |
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
**
|
|
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
|
|
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
|
|
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`.
|
|
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
|
|
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?:
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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 `
|
|
80
|
-
* declares `If-Match
|
|
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:
|
|
101
|
-
/**
|
|
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
|
/**
|