@nohead/sdk 0.1.0 → 0.3.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 +124 -0
- package/README.md +106 -42
- package/dist/client.d.ts +4 -1
- package/dist/client.js +14 -1
- package/dist/core.d.ts +49 -9
- package/dist/core.js +36 -10
- package/dist/errors.d.ts +5 -2
- package/dist/errors.js +25 -11
- package/dist/generated/operations.gen.d.ts +164 -12
- package/dist/generated/operations.gen.js +26 -7
- package/dist/generated/schemas.gen.d.ts +1 -1
- package/dist/generated/types.gen.d.ts +2467 -2783
- package/dist/index.d.ts +2 -2
- package/dist/resources/assets.d.ts +30 -7
- package/dist/resources/assets.js +48 -7
- package/dist/resources/drafts.d.ts +41 -0
- package/dist/resources/drafts.js +63 -0
- package/dist/resources/other.d.ts +6 -1
- package/dist/resources/other.js +16 -2
- package/dist/resources/records.d.ts +46 -17
- package/dist/resources/records.js +40 -3
- package/dist/resources/resource.d.ts +5 -0
- package/dist/resources/resource.js +14 -0
- package/dist/resources/schema.d.ts +26 -19
- package/dist/resources/schema.js +22 -6
- package/dist/resources/webhooks.d.ts +20 -6
- package/dist/resources/webhooks.js +25 -5
- package/dist/types.d.ts +107 -12
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/webhooks.d.ts +37 -17
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,130 @@ 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.3.0
|
|
9
|
+
|
|
10
|
+
- **Breaking:** 0.1.0 and 0.2.0 no longer work with the API: its paths and
|
|
11
|
+
shapes changed (below), so upgrading is required.
|
|
12
|
+
- `changeNote` is sent percent-encoded (UTF-8), so change notes work in any
|
|
13
|
+
language: a note outside ASCII used to fail the request.
|
|
14
|
+
- Drafts: changes to a record that wait until they are merged into it, now
|
|
15
|
+
or at a set time. `drafts.create`, `list`, `get` (with `expand`, for
|
|
16
|
+
previews), `update`, `discard`, `merge`, `schedule` and `unschedule`, and
|
|
17
|
+
`records.drafts`. Drafts take the type of their data as records do
|
|
18
|
+
(`drafts.get<Post>(id)` is a `NoheadDraft<Post>`, also `Nohead.Draft`),
|
|
19
|
+
and `drafts.create(record)` needs no params to start from the record's
|
|
20
|
+
values. `records.publish` on a live record with a pending draft is a
|
|
21
|
+
`ConflictError` (`draft_pending`). Webhook events gain `DraftEvent`
|
|
22
|
+
(`draft.*`), and a merge's `record.updated` carries its `draft_id`.
|
|
23
|
+
- **Breaking:** request options follow what each operation takes, by type:
|
|
24
|
+
`changeNote` only on writes that change records or the schema
|
|
25
|
+
(`WriteOptions`: record writes, `drafts.merge`, collection and field
|
|
26
|
+
writes), not on assets, webhooks, schedules, search index rebuilds or
|
|
27
|
+
other draft writes, which never kept a note. `ifMatch` on
|
|
28
|
+
`records.update`, `delete`, `restore` (new), `publish`, `unpublish` and
|
|
29
|
+
`revisions.revert` (`ConditionalWriteOptions`) and on `drafts.update`,
|
|
30
|
+
`discard` and `merge`. `ConditionalRequestOptions` no longer includes
|
|
31
|
+
`changeNote`.
|
|
32
|
+
- **Breaking:** `records.schedule` sets the whole schedule: a time left out
|
|
33
|
+
is cleared (it used to keep its value), and `{}` clears both, as
|
|
34
|
+
`unschedule` does.
|
|
35
|
+
- `fields.get()`: one field by ID.
|
|
36
|
+
- **Breaking:** the types for what only the web app can do (organizations,
|
|
37
|
+
projects, members and invitations, billing, API keys, usage, account
|
|
38
|
+
changes and redaction, such as `Organization`, `Project`, `ApiKey`, `Billing`) are gone. Those
|
|
39
|
+
operations need a signed-in person, so an API key could never call them,
|
|
40
|
+
and the API reference no longer lists them.
|
|
41
|
+
- **Breaking:** the API is resource-oriented. Method names stay; what they
|
|
42
|
+
return and take follows it:
|
|
43
|
+
- Records have no `status`, `schedule` or `deleted`: a record is published
|
|
44
|
+
when `published_at` is set, and has `deleted_at` and `purge_after`.
|
|
45
|
+
Filter with `{ published_at: { exists: true } }`. `records.get` returns
|
|
46
|
+
deleted records too (no `include_deleted`).
|
|
47
|
+
- `records.schedule` and `unschedule` return the schedule, and
|
|
48
|
+
`records.getSchedule`, `records.publication` and
|
|
49
|
+
`records.scheduledChanges` are new. `records.bulk` returns a
|
|
50
|
+
`record_batch`: `BulkRecordsResult` and `BulkRecordsRequest` are now
|
|
51
|
+
`RecordBatch` and `RecordBatchRequest`.
|
|
52
|
+
- Project search takes `filter: { published_at: { exists: true } }`
|
|
53
|
+
instead of `filter: { status }`.
|
|
54
|
+
- Collections, fields, assets and webhooks have `deleted_at` and
|
|
55
|
+
`purge_after` instead of `deleted`, and migrations
|
|
56
|
+
`cancel_requested_at`.
|
|
57
|
+
- `assets.createUpload` returns the asset with its `upload`
|
|
58
|
+
(`AssetWithUpload`, was `AssetUpload`).
|
|
59
|
+
- `fields.reorder` returns the `field_order`; `fields.order` reads it.
|
|
60
|
+
- Webhook event types are those of the activity log (`record.restored`,
|
|
61
|
+
`field.updated`, …, and wildcards such as `record.*`), and event
|
|
62
|
+
payloads carry `actor` and `via`. `SchemaChangedEvent`
|
|
63
|
+
(`schema.changed`) is now `SchemaEvent` (`collection.*` and `field.*`,
|
|
64
|
+
with the `collection`, its `object`, the `schema_change` and, for a
|
|
65
|
+
field, its `field_id`), and `FieldAliasRemovedEvent` is new
|
|
66
|
+
(`field.alias_removed`). There is no `record.imported`. The event data
|
|
67
|
+
types are exported too (`RecordEventData`, `DraftEventData`,
|
|
68
|
+
`AssetEventData`, `SchemaEventData`, `FieldAliasEventData`,
|
|
69
|
+
`WebhookTestEventData`).
|
|
70
|
+
- `webhooks.rotateSecret` returns a `webhook_secret`
|
|
71
|
+
(`WebhookSecretWithValue`, was `WebhookWithSecret`); the old secret
|
|
72
|
+
signs for 24 hours more. `webhooks.secrets.list`,
|
|
73
|
+
`webhooks.secrets.retire`, `webhooks.restore` and
|
|
74
|
+
`webhooks.list({ deleted })` are new. `webhooks.deliveries.retry`
|
|
75
|
+
calls the API's new path.
|
|
76
|
+
- Audit events are `object: "event"`, and name what happened in `type`
|
|
77
|
+
(was `action`). `auditEvents.list` filters by `type`, `resource_type`
|
|
78
|
+
(one, or a list of them), `resource_id`, `actor_type`, `actor_id` and
|
|
79
|
+
`created_at: { gte, gt, lte, lt }` (was `action`, `since` and
|
|
80
|
+
`until`); an unknown filter is an `InvalidRequestError`. Field events
|
|
81
|
+
have the resource type `field`.
|
|
82
|
+
- `records.revisions.list` filters `created_at` by range (was `since`
|
|
83
|
+
and `until`), and revisions' `operation` can be `merge` (not `import`).
|
|
84
|
+
- `ScheduledChange` has no `last_error`, and `SchemaChange` no `status`.
|
|
85
|
+
`ChangeVia` is gone: `Via` names the client a change came through.
|
|
86
|
+
- Exported types for the params that were inline:
|
|
87
|
+
`ScheduledChangeListParams`, `DraftScheduleParams`,
|
|
88
|
+
`FieldReorderParams`, and the filters `AuditEventFilter`,
|
|
89
|
+
`RevisionFilter` and `TimeRange`.
|
|
90
|
+
- **Breaking:** the `datetime` field type is now `date`. A plain date field holds
|
|
91
|
+
`YYYY-MM-DD`; with `include_time` it holds a moment, written in the field's
|
|
92
|
+
`time_zone` when it has one. Field migrations take `time_zone`, the zone
|
|
93
|
+
whose day each moment falls on when the time is removed.
|
|
94
|
+
- Boolean fields can't be `required`: a boolean is true or false, and no
|
|
95
|
+
value reads as false.
|
|
96
|
+
- Text fields take a `format` (`email`, `url`, `slug`; slugs are always
|
|
97
|
+
unique) and text and integer fields `unique`; a taken value's error detail
|
|
98
|
+
has the `record_id` that has it (`code: "taken"`).
|
|
99
|
+
- Filters take operators: `{ price: { lt: 50 } }`, with `eq`, `ne`, `gt`,
|
|
100
|
+
`gte`, `lt`, `lte`, `in` (a list) and `exists`, and record lists sort by a
|
|
101
|
+
field's value. Commas and backslashes in list values are escaped, so a
|
|
102
|
+
value can hold a comma.
|
|
103
|
+
- Date filters take `today`, and dates with a time `now`.
|
|
104
|
+
- `assets.usage()`: where an asset is used, as counts of the records that use
|
|
105
|
+
it (and of those, the published ones) and the 10 most recently updated with
|
|
106
|
+
the fields that use it. Needs the `records:read` scope too.
|
|
107
|
+
- `assets.purge()`: permanently deletes a deleted asset now, instead of at
|
|
108
|
+
its `purge_after`, and frees its storage.
|
|
109
|
+
- An error reads only the fields of the API's error envelope that have the
|
|
110
|
+
right type, so JSON from a proxy or gateway that merely looks like one no
|
|
111
|
+
longer gives it a non-string `message`, `type` or `requestId`.
|
|
112
|
+
- `APIError.type` is typed as the known error types or any string: the API
|
|
113
|
+
can add types, and one newer than the SDK is kept as is. Code that checks
|
|
114
|
+
it exhaustively needs a default branch.
|
|
115
|
+
- An error whose type names a built-in property (`constructor`,
|
|
116
|
+
`toString`) is an `APIError`; it used to throw a value that wasn't one.
|
|
117
|
+
- An error's `details` keep only entries that are objects, so
|
|
118
|
+
`currentRevision` no longer throws on a `null` one.
|
|
119
|
+
|
|
120
|
+
## 0.2.0
|
|
121
|
+
|
|
122
|
+
- `onRetry` client option: called before each retry with the attempt, the
|
|
123
|
+
delay, and the status or error that failed, for example to report retries.
|
|
124
|
+
- `assets.upload()` with an `idempotencyKey` no longer fails when it
|
|
125
|
+
completes the upload: the key only starts it. Running the same upload again
|
|
126
|
+
resumes it, and returns an asset that is already `ready` without sending the
|
|
127
|
+
bytes again; once the upload's URL has expired it throws `UploadError`
|
|
128
|
+
(start over with a new key).
|
|
129
|
+
- Types follow the API's current contract. They add types only for
|
|
130
|
+
operations an API key can't call, so no method changed.
|
|
131
|
+
|
|
8
132
|
## 0.1.0
|
|
9
133
|
|
|
10
134
|
The first release.
|
package/README.md
CHANGED
|
@@ -8,19 +8,20 @@ import { Nohead } from "@nohead/sdk"
|
|
|
8
8
|
const nohead = new Nohead() // reads NOHEAD_API_KEY
|
|
9
9
|
|
|
10
10
|
for await (const post of nohead.records.list("posts", {
|
|
11
|
-
filter: {
|
|
11
|
+
filter: { published_at: { exists: true } },
|
|
12
12
|
})) {
|
|
13
13
|
console.log(post.data.title)
|
|
14
14
|
}
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
> **Status:** 0.x
|
|
17
|
+
> **Status:** 0.x until the Nohead API launches.
|
|
18
18
|
|
|
19
19
|
## Contents
|
|
20
20
|
|
|
21
21
|
- [Installation](#installation)
|
|
22
22
|
- [Configuration](#configuration)
|
|
23
23
|
- [Records](#records)
|
|
24
|
+
- [Drafts](#drafts)
|
|
24
25
|
- [Pagination](#pagination)
|
|
25
26
|
- [Errors](#errors)
|
|
26
27
|
- [Retries and idempotency](#retries-and-idempotency)
|
|
@@ -50,32 +51,36 @@ const nohead = new Nohead({
|
|
|
50
51
|
})
|
|
51
52
|
```
|
|
52
53
|
|
|
53
|
-
| Option | Default |
|
|
54
|
-
| ------------ | ---------------------------------------------- |
|
|
55
|
-
| `apiKey` | `NOHEAD_API_KEY` | A project API key (`sk_live_…`). Required.
|
|
56
|
-
| `baseUrl` | `NOHEAD_API_URL`, else `https://api.nohead.io` |
|
|
57
|
-
| `projectId` | the key's project | Looked up once with `GET /v1/me` when omitted.
|
|
58
|
-
| `maxRetries` | `2` | See [retries](#retries-and-idempotency).
|
|
59
|
-
| `timeout` | `60000` | Milliseconds per attempt.
|
|
60
|
-
| `fetch` | `globalThis.fetch` | For proxies, instrumentation and tests.
|
|
61
|
-
| `headers` | `{}` | Added to every request.
|
|
62
|
-
| `warnings` | `true` | Logs deprecation and plan usage warnings, once each.
|
|
54
|
+
| Option | Default | |
|
|
55
|
+
| ------------ | ---------------------------------------------- | ------------------------------------------------------------------ |
|
|
56
|
+
| `apiKey` | `NOHEAD_API_KEY` | A project API key (`sk_live_…`). 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). |
|
|
63
65
|
|
|
64
66
|
API keys belong to a project, so methods like `collections.list()` need no project ID. Collections can be named by ID or slug everywhere.
|
|
65
67
|
|
|
66
68
|
## Records
|
|
67
69
|
|
|
68
70
|
```ts
|
|
69
|
-
const
|
|
71
|
+
const created = await nohead.records.create("posts", {
|
|
70
72
|
data: { title: "Hello", author: "rec_01J9…" },
|
|
71
73
|
})
|
|
72
|
-
const post = await nohead.records.get(
|
|
74
|
+
const post = await nohead.records.get(created.id, { expand: ["author"] })
|
|
73
75
|
await nohead.records.update(post.id, { data: { title: "Hello again" } })
|
|
74
76
|
await nohead.records.publish(post.id)
|
|
77
|
+
await nohead.records.publication(post.id) // since when, by whom, which revision
|
|
75
78
|
await nohead.records.schedule(post.id, { unpublish_at: new Date("2027-01-01") })
|
|
76
79
|
await nohead.records.delete(post.id) // soft delete; records.restore() undoes it
|
|
77
80
|
```
|
|
78
81
|
|
|
82
|
+
`schedule` sets the whole schedule: a time left out is cleared, so `{ unpublish_at }` alone drops a pending publish.
|
|
83
|
+
|
|
79
84
|
Methods return the resource and throw on failure. Field names stay as the API sends them (`published_at`, `data`), so the API reference applies as-is.
|
|
80
85
|
|
|
81
86
|
**Typed data.** Describe a collection's fields and pass the type:
|
|
@@ -94,6 +99,29 @@ The type is a promise you make; nothing is checked at runtime. The record type i
|
|
|
94
99
|
|
|
95
100
|
**More:** `count`, `bulk` (up to 100 records at once), `diff`, `revisions.list`, `revisions.get` and `revisions.revert` (with `{ dry_run: true }` for a preview).
|
|
96
101
|
|
|
102
|
+
## Drafts
|
|
103
|
+
|
|
104
|
+
A draft holds changes to a record until they are merged, now or at a set time, while the record keeps its current content: changes to review before they go live, or changes due later.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
const draft = await nohead.drafts.create(post.id, {
|
|
108
|
+
data: { title: "Hello, revised" },
|
|
109
|
+
})
|
|
110
|
+
await nohead.drafts.update(
|
|
111
|
+
draft.id,
|
|
112
|
+
{ data: { summary: "…" } },
|
|
113
|
+
{ ifMatch: draft }
|
|
114
|
+
)
|
|
115
|
+
const preview = await nohead.drafts.get(draft.id, { expand: ["author"] }) // data as it would read
|
|
116
|
+
await nohead.drafts.merge(draft.id) // or drafts.schedule(draft.id, { merge_at })
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- A record has at most one draft; starting a second is a `ConflictError` (detail code `draft_exists`, with its `draft_id`).
|
|
120
|
+
- A merge changes content only: a live record stays live, an unpublished one unpublished. Publishing a live record with a pending draft is a `ConflictError` (`draft_pending`).
|
|
121
|
+
- `records.drafts(id)` lists a record's draft, and `drafts.list(collection, { record_id })` a collection's (`record_id` takes up to 100 IDs).
|
|
122
|
+
- `drafts.discard` deletes one for good.
|
|
123
|
+
- Drafts take the type of their data as records do: `drafts.get<Post>(id)` is a `Nohead.Draft<Post>` (also exported as `NoheadDraft`).
|
|
124
|
+
|
|
97
125
|
## Pagination
|
|
98
126
|
|
|
99
127
|
List methods return a promise of the first page that you can also loop over:
|
|
@@ -112,14 +140,27 @@ while (page.hasNextPage()) page = await page.getNextPage()
|
|
|
112
140
|
await nohead.records.list("posts", { cursor: page.meta.next_cursor! })
|
|
113
141
|
```
|
|
114
142
|
|
|
115
|
-
Filters
|
|
143
|
+
Filters take a value to equal, or operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in` (a list) and `exists`. They combine with AND. On fields with several values, equal means "contains". Values are strings, numbers, booleans and dates. A `Date` is sent as a moment, for date fields with a time; a plain date field takes its `YYYY-MM-DD` string, and both take `"today"` (dates with a time `"now"` too). Lists sort by a field too, with records that have no value last:
|
|
116
144
|
|
|
117
145
|
```ts
|
|
118
146
|
nohead.records.list("posts", {
|
|
119
|
-
filter: {
|
|
147
|
+
filter: {
|
|
148
|
+
published_at: { exists: true },
|
|
149
|
+
featured: true,
|
|
150
|
+
author: "rec_01J9…",
|
|
151
|
+
},
|
|
120
152
|
sort: "-published_at",
|
|
121
153
|
expand: ["author", "tags"],
|
|
122
154
|
})
|
|
155
|
+
|
|
156
|
+
nohead.records.list("products", {
|
|
157
|
+
filter: {
|
|
158
|
+
price: { lt: 50 },
|
|
159
|
+
category: { in: ["shoes", "bags"] },
|
|
160
|
+
cover: { exists: true },
|
|
161
|
+
},
|
|
162
|
+
sort: "price",
|
|
163
|
+
})
|
|
123
164
|
```
|
|
124
165
|
|
|
125
166
|
## Errors
|
|
@@ -163,6 +204,17 @@ Failed requests are retried twice by default (`maxRetries`), with exponential ba
|
|
|
163
204
|
- what's retried: connection errors, timeouts, 429, 500, 502, 503, 504, and a 409 for a request that is still running
|
|
164
205
|
- `Retry-After` is honored up to 60 seconds; a longer one throws `RateLimitError` straight away
|
|
165
206
|
|
|
207
|
+
To report retries, pass `onRetry`. It gets the retry's `attempt` (1 for the first), `delay` in seconds, `url`, the failed `status` (none when the connection failed) and the `error`:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
const nohead = new Nohead({
|
|
211
|
+
onRetry: ({ attempt, delay, status }) =>
|
|
212
|
+
console.error(
|
|
213
|
+
`Retry ${attempt} in ${delay.toFixed(1)}s (${status ?? "network"})`
|
|
214
|
+
),
|
|
215
|
+
})
|
|
216
|
+
```
|
|
217
|
+
|
|
166
218
|
Every write gets an `Idempotency-Key` that stays the same across its retries, so a retry after a lost response never writes twice. To make a write safe across your own retries (a job that may run twice), pass a key:
|
|
167
219
|
|
|
168
220
|
```ts
|
|
@@ -176,10 +228,10 @@ await nohead.records.create(
|
|
|
176
228
|
Every method takes request options as its last argument:
|
|
177
229
|
|
|
178
230
|
- `idempotencyKey`
|
|
179
|
-
- `ifMatch` (record writes)
|
|
180
|
-
- `changeNote`, a reason shown in history
|
|
181
231
|
- `signal`, `timeout`, `maxRetries` and `headers`
|
|
182
232
|
|
|
233
|
+
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)).
|
|
234
|
+
|
|
183
235
|
## Concurrency
|
|
184
236
|
|
|
185
237
|
Pass the revision you read to make sure nobody changed the record since:
|
|
@@ -195,7 +247,7 @@ try {
|
|
|
195
247
|
}
|
|
196
248
|
```
|
|
197
249
|
|
|
198
|
-
`ifMatch` takes a record or a revision number, on `update`, `delete`, `publish`, `unpublish` and `revisions.revert`.
|
|
250
|
+
`ifMatch` takes a record or a revision number, on `records.update`, `delete`, `restore`, `publish`, `unpublish` and `revisions.revert`, and a draft or its revision on `drafts.update`, `discard` and `merge`.
|
|
199
251
|
|
|
200
252
|
## Assets
|
|
201
253
|
|
|
@@ -223,8 +275,18 @@ const { url } = await nohead.assets.imageUrl(asset.id, {
|
|
|
223
275
|
|
|
224
276
|
**Errors:** `UploadError` if storage refuses the bytes; `ValidationError` if the file fails the checks.
|
|
225
277
|
|
|
278
|
+
**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.
|
|
279
|
+
|
|
226
280
|
**Uploading from a browser:** create the upload on your server with `createUpload`, `PUT` the file from the browser, then `complete` it.
|
|
227
281
|
|
|
282
|
+
**Deleting:** `delete` keeps a deleted asset until its `purge_after` (30 days), and `restore` brings it back. To remove it for good now, `purge` it after the delete. Records that use it keep an ID that no longer resolves, so check `usage` first:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
const usage = await nohead.assets.usage(asset.id)
|
|
286
|
+
usage.records // how many records use it, usage.published of them published
|
|
287
|
+
usage.uses // the 10 most recently updated, with the fields that use it
|
|
288
|
+
```
|
|
289
|
+
|
|
228
290
|
## Search
|
|
229
291
|
|
|
230
292
|
```ts
|
|
@@ -277,14 +339,14 @@ export async function POST(request: Request) {
|
|
|
277
339
|
secret: process.env.NOHEAD_WEBHOOK_SECRET!,
|
|
278
340
|
}
|
|
279
341
|
)
|
|
280
|
-
if (event.type === "record.published") {
|
|
342
|
+
if (event.type === "record.published" && event.data.record) {
|
|
281
343
|
revalidateTag(event.data.record.collection)
|
|
282
344
|
}
|
|
283
345
|
return new Response(null, { status: 204 })
|
|
284
346
|
}
|
|
285
347
|
```
|
|
286
348
|
|
|
287
|
-
`unwrap` checks the signature and the timestamp (Standard Webhooks), and throws `WebhookVerificationError` if either is off. Pass the raw body: parsing and re-serializing JSON changes the bytes.
|
|
349
|
+
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.
|
|
288
350
|
|
|
289
351
|
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.
|
|
290
352
|
|
|
@@ -299,23 +361,25 @@ response.headers.get("RateLimit-Remaining")
|
|
|
299
361
|
|
|
300
362
|
## Reference
|
|
301
363
|
|
|
302
|
-
| Resource | Methods
|
|
303
|
-
| --------------------------- |
|
|
304
|
-
| `records` | `list`, `get`, `create`, `update`, `delete`, `restore`, `publish`, `unpublish`, `schedule`, `unschedule`, `count`, `bulk`, `diff`, `search` |
|
|
305
|
-
| `
|
|
306
|
-
| `
|
|
307
|
-
| `
|
|
308
|
-
| `collections
|
|
309
|
-
| `collections.
|
|
310
|
-
| `
|
|
311
|
-
| `
|
|
312
|
-
| `
|
|
313
|
-
| `
|
|
314
|
-
| `webhooks
|
|
315
|
-
| `
|
|
316
|
-
| `
|
|
317
|
-
| `
|
|
318
|
-
| `
|
|
364
|
+
| Resource | Methods |
|
|
365
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
366
|
+
| `records` | `list`, `get`, `create`, `update`, `delete`, `restore`, `publish`, `unpublish`, `publication`, `getSchedule`, `schedule`, `unschedule`, `scheduledChanges`, `drafts`, `count`, `bulk`, `diff`, `search` |
|
|
367
|
+
| `drafts` | `create`, `list`, `get`, `update`, `discard`, `merge`, `schedule`, `unschedule` |
|
|
368
|
+
| `records.revisions` | `list`, `get`, `revert` |
|
|
369
|
+
| `search` | across the project |
|
|
370
|
+
| `collections` | `list`, `get`, `create`, `update`, `delete`, `restore`, `schema` |
|
|
371
|
+
| `collections.schemaChanges` | `list`, `get` |
|
|
372
|
+
| `collections.searchIndex` | `get`, `rebuild` |
|
|
373
|
+
| `fields` | `list`, `get`, `create`, `update`, `delete`, `restore`, `order`, `reorder`, `removeAlias`, `migrate` |
|
|
374
|
+
| `migrations` | `list`, `get`, `cancel` |
|
|
375
|
+
| `assets` | `upload`, `createUpload`, `complete`, `list`, `get`, `delete`, `restore`, `purge`, `usage`, `imageUrl`, `downloadUrl` |
|
|
376
|
+
| `webhooks` | `list`, `get`, `create`, `update`, `delete`, `restore`, `rotateSecret`, `test`, `unwrap` |
|
|
377
|
+
| `webhooks.secrets` | `list`, `retire` |
|
|
378
|
+
| `webhooks.deliveries` | `list`, `get`, `retry` |
|
|
379
|
+
| `auditEvents` | `list` |
|
|
380
|
+
| `featureFlags` | `list` |
|
|
381
|
+
| `me` | `get` |
|
|
382
|
+
| `health` | `check` |
|
|
319
383
|
|
|
320
384
|
The SDK covers every operation an API key can call. Organizations, projects, members and API keys are managed in the web app. The full API is documented at [docs.nohead.io](https://docs.nohead.io).
|
|
321
385
|
|
|
@@ -336,7 +400,7 @@ NOHEAD_API_URL=http://localhost:3000 NOHEAD_API_KEY=sk_live_… npm run smoke
|
|
|
336
400
|
- the types, in `src/generated/types.gen.ts` (hey-api)
|
|
337
401
|
- the operation table, in `src/generated/operations.gen.ts`
|
|
338
402
|
- The methods in `src/resources` are written by hand.
|
|
339
|
-
- `test/contract.test.ts` calls every public method. It fails when an API-key operation in the contract has no method, or when a request doesn't match its operation.
|
|
403
|
+
- `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.
|
|
340
404
|
|
|
341
405
|
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.
|
|
342
406
|
|
|
@@ -344,9 +408,9 @@ The smoke test (`smoke/smoke.ts`) runs the core flow against a real API, using t
|
|
|
344
408
|
|
|
345
409
|
1. Bump the version in `package.json` and `src/version.ts`.
|
|
346
410
|
2. Add a section for it to `CHANGELOG.md` (`## 1.2.3`), which becomes the release's notes.
|
|
347
|
-
3. Merge to `main`.
|
|
348
|
-
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`.
|
|
349
|
-
5. The tag starts `.github/workflows/release.yml`.
|
|
411
|
+
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.
|
|
412
|
+
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.
|
|
413
|
+
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.
|
|
350
414
|
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.
|
|
351
415
|
|
|
352
416
|
## License
|
package/dist/client.d.ts
CHANGED
|
@@ -2,11 +2,12 @@ import type { PagePromise } from "./api-promise.ts";
|
|
|
2
2
|
import { type ClientOptions, type RequestOptions } from "./core.ts";
|
|
3
3
|
import type * as G from "./generated/types.gen.ts";
|
|
4
4
|
import { Assets } from "./resources/assets.ts";
|
|
5
|
+
import { Drafts } from "./resources/drafts.ts";
|
|
5
6
|
import { AuditEvents, FeatureFlags, Health, Me } from "./resources/other.ts";
|
|
6
7
|
import { Records } from "./resources/records.ts";
|
|
7
8
|
import { Collections, Fields, Migrations } from "./resources/schema.ts";
|
|
8
9
|
import { Webhooks } from "./resources/webhooks.ts";
|
|
9
|
-
import type { NoheadRecord, ProjectSearchParams, RecordData, SearchMeta } from "./types.ts";
|
|
10
|
+
import type { NoheadDraft, NoheadRecord, ProjectSearchParams, RecordData, SearchMeta } from "./types.ts";
|
|
10
11
|
/**
|
|
11
12
|
* A client for the Nohead API, authenticated with a project API key.
|
|
12
13
|
*
|
|
@@ -18,6 +19,7 @@ import type { NoheadRecord, ProjectSearchParams, RecordData, SearchMeta } from "
|
|
|
18
19
|
export declare class Nohead {
|
|
19
20
|
#private;
|
|
20
21
|
readonly records: Records;
|
|
22
|
+
readonly drafts: Drafts;
|
|
21
23
|
readonly collections: Collections;
|
|
22
24
|
readonly fields: Fields;
|
|
23
25
|
readonly migrations: Migrations;
|
|
@@ -37,6 +39,7 @@ export declare class Nohead {
|
|
|
37
39
|
/** The main types, also reachable as `Nohead.Record`, `Nohead.Collection`… */
|
|
38
40
|
export declare namespace Nohead {
|
|
39
41
|
type Record<D = RecordData> = NoheadRecord<D>;
|
|
42
|
+
type Draft<D = RecordData> = NoheadDraft<D>;
|
|
40
43
|
type Collection = G.Collection;
|
|
41
44
|
type Field = G.Field;
|
|
42
45
|
type Asset = G.Asset;
|
package/dist/client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Core } from "./core.js";
|
|
2
2
|
import { Assets } from "./resources/assets.js";
|
|
3
|
+
import { Drafts } from "./resources/drafts.js";
|
|
3
4
|
import { AuditEvents, FeatureFlags, Health, Me } from "./resources/other.js";
|
|
4
5
|
import { Records } from "./resources/records.js";
|
|
5
6
|
import { Collections, Fields, Migrations } from "./resources/schema.js";
|
|
@@ -14,6 +15,7 @@ import { Webhooks } from "./resources/webhooks.js";
|
|
|
14
15
|
*/
|
|
15
16
|
export class Nohead {
|
|
16
17
|
records;
|
|
18
|
+
drafts;
|
|
17
19
|
collections;
|
|
18
20
|
fields;
|
|
19
21
|
migrations;
|
|
@@ -28,6 +30,7 @@ export class Nohead {
|
|
|
28
30
|
const core = new Core(options);
|
|
29
31
|
this.#core = core;
|
|
30
32
|
this.records = new Records(core);
|
|
33
|
+
this.drafts = new Drafts(core);
|
|
31
34
|
this.collections = new Collections(core);
|
|
32
35
|
this.fields = new Fields(core);
|
|
33
36
|
this.migrations = new Migrations(core);
|
|
@@ -43,6 +46,16 @@ export class Nohead {
|
|
|
43
46
|
* collections), most relevant first, through the first 1,000 hits.
|
|
44
47
|
*/
|
|
45
48
|
search(query, params = {}, options) {
|
|
46
|
-
|
|
49
|
+
const { collections, filter, ...rest } = params;
|
|
50
|
+
return this.#core.paginate("projects_search", {
|
|
51
|
+
query: {
|
|
52
|
+
q: query,
|
|
53
|
+
...rest,
|
|
54
|
+
filter: {
|
|
55
|
+
...filter,
|
|
56
|
+
...(collections ? { collection: { in: collections } } : {}),
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}, options);
|
|
47
60
|
}
|
|
48
61
|
}
|
package/dist/core.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { APIPromise, PagePromise } from "./api-promise.ts";
|
|
2
|
+
import { NoheadError } from "./errors.ts";
|
|
2
3
|
import { type OperationId, type Operations } from "./generated/operations.gen.ts";
|
|
3
4
|
import type { ListMeta } from "./generated/types.gen.ts";
|
|
4
5
|
export declare const DEFAULT_BASE_URL = "https://api.nohead.io";
|
|
@@ -19,12 +20,25 @@ export interface ClientOptions {
|
|
|
19
20
|
headers?: Record<string, string>;
|
|
20
21
|
/** Log deprecation and plan usage warnings (once each). Default true. */
|
|
21
22
|
warnings?: boolean;
|
|
23
|
+
/** Called before each retry, for example to report it. */
|
|
24
|
+
onRetry?: (retry: RetryEvent) => void;
|
|
25
|
+
}
|
|
26
|
+
/** A retry about to happen (`onRetry`). */
|
|
27
|
+
export interface RetryEvent {
|
|
28
|
+
/** 1 for the first retry of the request, 2 for the second… */
|
|
29
|
+
attempt: number;
|
|
30
|
+
/** Seconds the client waits before it. */
|
|
31
|
+
delay: number;
|
|
32
|
+
/** The URL being retried. */
|
|
33
|
+
url: string;
|
|
34
|
+
/** The status that failed, or undefined when the connection did. */
|
|
35
|
+
status?: number;
|
|
36
|
+
/** What failed: an `APIError` or a `ConnectionError` (none for storage). */
|
|
37
|
+
error?: NoheadError;
|
|
22
38
|
}
|
|
23
39
|
export interface RequestOptions {
|
|
24
40
|
/** Sent as `Idempotency-Key` on writes. Generated when omitted. */
|
|
25
41
|
idempotencyKey?: string;
|
|
26
|
-
/** A reason for the change, shown in history (`Nohead-Change-Note`). */
|
|
27
|
-
changeNote?: string;
|
|
28
42
|
signal?: AbortSignal;
|
|
29
43
|
/** Milliseconds per attempt, overriding the client's. */
|
|
30
44
|
timeout?: number;
|
|
@@ -32,28 +46,53 @@ export interface RequestOptions {
|
|
|
32
46
|
maxRetries?: number;
|
|
33
47
|
headers?: Record<string, string>;
|
|
34
48
|
}
|
|
35
|
-
/** Options of
|
|
49
|
+
/** Options of writes that change records or the schema. */
|
|
50
|
+
export interface WriteOptions extends RequestOptions {
|
|
51
|
+
/**
|
|
52
|
+
* A reason for the change, kept with the revision, schema change or
|
|
53
|
+
* migration it makes (`Nohead-Change-Note`).
|
|
54
|
+
*/
|
|
55
|
+
changeNote?: string;
|
|
56
|
+
}
|
|
57
|
+
/** Options of draft writes that accept `If-Match`. */
|
|
36
58
|
export interface ConditionalRequestOptions extends RequestOptions {
|
|
37
59
|
/**
|
|
38
|
-
* Only write if the record is still at this revision;
|
|
39
|
-
* answers `412` (`PreconditionFailedError`). A record
|
|
60
|
+
* Only write if the record (or the draft) is still at this revision;
|
|
61
|
+
* otherwise the API answers `412` (`PreconditionFailedError`). A record
|
|
62
|
+
* or a draft works too.
|
|
40
63
|
*/
|
|
41
64
|
ifMatch?: number | {
|
|
42
65
|
revision: number;
|
|
43
66
|
};
|
|
44
67
|
}
|
|
68
|
+
/** Options of record writes, which accept `If-Match` and a change note. */
|
|
69
|
+
export interface ConditionalWriteOptions extends WriteOptions, ConditionalRequestOptions {
|
|
70
|
+
}
|
|
71
|
+
/** The request headers an operation declares. */
|
|
72
|
+
type HeadersOf<Op extends OperationId> = Operations[Op]["data"] extends {
|
|
73
|
+
headers?: infer H;
|
|
74
|
+
} ? keyof NonNullable<H> : never;
|
|
75
|
+
/**
|
|
76
|
+
* The options an operation takes: `ifMatch` and `changeNote` only where it
|
|
77
|
+
* declares `If-Match` and `Nohead-Change-Note`, so a method can't offer them
|
|
78
|
+
* elsewhere.
|
|
79
|
+
*/
|
|
80
|
+
export type OptionsOf<Op extends OperationId> = RequestOptions & ("If-Match" extends HeadersOf<Op> ? Pick<ConditionalRequestOptions, "ifMatch"> : {
|
|
81
|
+
ifMatch?: never;
|
|
82
|
+
}) & ("Nohead-Change-Note" extends HeadersOf<Op> ? Pick<WriteOptions, "changeNote"> : {
|
|
83
|
+
changeNote?: never;
|
|
84
|
+
});
|
|
45
85
|
export interface RequestInput {
|
|
46
86
|
path?: Record<string, string | number | undefined>;
|
|
47
87
|
query?: object;
|
|
48
88
|
body?: unknown;
|
|
49
89
|
}
|
|
50
|
-
type AllOptions = ConditionalRequestOptions;
|
|
51
90
|
export declare class Core {
|
|
52
91
|
#private;
|
|
53
92
|
constructor(options: ClientOptions);
|
|
54
93
|
/** The API key's project, from `GET /v1/me` the first time. */
|
|
55
94
|
projectId(): Promise<string>;
|
|
56
|
-
request<Op extends OperationId>(operation: Op, input?: RequestInput, options?:
|
|
95
|
+
request<Op extends OperationId>(operation: Op, input?: RequestInput, options?: OptionsOf<Op>): APIPromise<Operations[Op]["response"]>;
|
|
57
96
|
/** A list operation as pages of `Item`. */
|
|
58
97
|
paginate<Item, Meta extends ListMeta = ListMeta>(operation: OperationId, input: RequestInput, options?: RequestOptions): PagePromise<Item, Meta>;
|
|
59
98
|
/** `fetch` with this client's timeout and retries, for presigned URLs. */
|
|
@@ -63,8 +102,9 @@ export declare class Core {
|
|
|
63
102
|
}
|
|
64
103
|
/**
|
|
65
104
|
* Query parameters as the API reads them: objects become `key[sub]=…`
|
|
66
|
-
* (`filter[
|
|
67
|
-
*
|
|
105
|
+
* (`filter[price][lt]=50`), arrays are comma-separated (`expand=author,tags`)
|
|
106
|
+
* with `\,` for a comma inside a value and `\\` for a backslash, dates are
|
|
107
|
+
* ISO 8601.
|
|
68
108
|
*/
|
|
69
109
|
export declare function appendQuery(params: URLSearchParams, value: unknown, key?: string): void;
|
|
70
110
|
export {};
|