@nohead/sdk 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +352 -3
- package/dist/api-promise.d.ts +50 -0
- package/dist/api-promise.js +77 -0
- package/dist/client.d.ts +47 -0
- package/dist/client.js +48 -0
- package/dist/core.d.ts +70 -0
- package/dist/core.js +290 -0
- package/dist/errors.d.ts +85 -0
- package/dist/errors.js +144 -0
- package/dist/generated/operations.gen.d.ts +502 -0
- package/dist/generated/operations.gen.js +64 -0
- package/dist/generated/schemas.gen.d.ts +1 -0
- package/dist/generated/schemas.gen.js +1 -0
- package/dist/generated/types.gen.d.ts +6806 -0
- package/dist/generated/types.gen.js +2 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +5 -0
- package/dist/resources/assets.d.ts +44 -0
- package/dist/resources/assets.js +86 -0
- package/dist/resources/other.d.ts +20 -0
- package/dist/resources/other.js +24 -0
- package/dist/resources/records.d.ts +52 -0
- package/dist/resources/records.js +78 -0
- package/dist/resources/resource.d.ts +5 -0
- package/dist/resources/resource.js +6 -0
- package/dist/resources/schema.d.ts +64 -0
- package/dist/resources/schema.js +97 -0
- package/dist/resources/webhooks.d.ts +32 -0
- package/dist/resources/webhooks.js +50 -0
- package/dist/types.d.ts +99 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/dist/webhooks.d.ts +47 -0
- package/dist/webhooks.js +70 -0
- package/package.json +58 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Changes to `@nohead/sdk` that you can notice. Versions follow
|
|
4
|
+
[Semantic Versioning](https://semver.org): additive API changes are minor
|
|
5
|
+
releases; a change that could break your code is a major one. Each release's
|
|
6
|
+
section is its GitHub release's notes.
|
|
7
|
+
|
|
8
|
+
## 0.1.0
|
|
9
|
+
|
|
10
|
+
The first release.
|
|
11
|
+
|
|
12
|
+
- `new Nohead()`, with methods for every operation an API key can call:
|
|
13
|
+
records (with revisions, scheduling, bulk changes and search), collections,
|
|
14
|
+
fields and migrations, assets, webhooks and their deliveries, the audit
|
|
15
|
+
log, feature flags.
|
|
16
|
+
- Lists you can `await` for one page or `for await` across every page.
|
|
17
|
+
- Typed errors per API error type, retries with idempotency keys, `ifMatch`
|
|
18
|
+
and change notes, `withResponse()`.
|
|
19
|
+
- `assets.upload()` in one call, and webhook verification
|
|
20
|
+
(`webhooks.unwrap()`, or `@nohead/sdk/webhooks` without a client).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nohead
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,5 +1,354 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Nohead TypeScript SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The official TypeScript client for the [Nohead](https://nohead.io) API: typed records, pagination you can loop over, retries that are safe for writes, one-call uploads and webhook verification. No runtime dependencies.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```ts
|
|
6
|
+
import { Nohead } from "@nohead/sdk"
|
|
7
|
+
|
|
8
|
+
const nohead = new Nohead() // reads NOHEAD_API_KEY
|
|
9
|
+
|
|
10
|
+
for await (const post of nohead.records.list("posts", {
|
|
11
|
+
filter: { status: "published" },
|
|
12
|
+
})) {
|
|
13
|
+
console.log(post.data.title)
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
> **Status:** 0.x, not yet published to npm. Until it is, install from GitHub: `npm install github:nohead-io/nohead-typescript`.
|
|
18
|
+
|
|
19
|
+
## Contents
|
|
20
|
+
|
|
21
|
+
- [Installation](#installation)
|
|
22
|
+
- [Configuration](#configuration)
|
|
23
|
+
- [Records](#records)
|
|
24
|
+
- [Pagination](#pagination)
|
|
25
|
+
- [Errors](#errors)
|
|
26
|
+
- [Retries and idempotency](#retries-and-idempotency)
|
|
27
|
+
- [Concurrency](#concurrency)
|
|
28
|
+
- [Assets](#assets)
|
|
29
|
+
- [Search](#search)
|
|
30
|
+
- [Schema](#schema)
|
|
31
|
+
- [Webhooks](#webhooks)
|
|
32
|
+
- [Raw responses](#raw-responses)
|
|
33
|
+
- [Reference](#reference)
|
|
34
|
+
- [Development](#development)
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install @nohead/sdk
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
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.
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const nohead = new Nohead({
|
|
48
|
+
apiKey: process.env.NOHEAD_API_KEY, // default: NOHEAD_API_KEY
|
|
49
|
+
baseUrl: "https://api.nohead.io", // default: NOHEAD_API_URL, else production
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
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. |
|
|
63
|
+
|
|
64
|
+
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
|
+
|
|
66
|
+
## Records
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const draft = await nohead.records.create("posts", {
|
|
70
|
+
data: { title: "Hello", author: "rec_01J9…" },
|
|
71
|
+
})
|
|
72
|
+
const post = await nohead.records.get(draft.id, { expand: ["author"] })
|
|
73
|
+
await nohead.records.update(post.id, { data: { title: "Hello again" } })
|
|
74
|
+
await nohead.records.publish(post.id)
|
|
75
|
+
await nohead.records.schedule(post.id, { unpublish_at: new Date("2027-01-01") })
|
|
76
|
+
await nohead.records.delete(post.id) // soft delete; records.restore() undoes it
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
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
|
+
|
|
81
|
+
**Typed data.** Describe a collection's fields and pass the type:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
interface Post {
|
|
85
|
+
title: string
|
|
86
|
+
author: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const post = await nohead.records.get<Post>(id) // post.data.title: string
|
|
90
|
+
await nohead.records.update<Post>(id, { data: { title: null } }) // null clears
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The type is a promise you make; nothing is checked at runtime. The record type itself is `Nohead.Record<Post>` (also exported as `NoheadRecord`).
|
|
94
|
+
|
|
95
|
+
**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
|
+
|
|
97
|
+
## Pagination
|
|
98
|
+
|
|
99
|
+
List methods return a promise of the first page that you can also loop over:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
// Every record, fetching pages as needed
|
|
103
|
+
for await (const record of nohead.records.list("posts")) { … }
|
|
104
|
+
|
|
105
|
+
// One page at a time
|
|
106
|
+
let page = await nohead.records.list("posts", { limit: 100 })
|
|
107
|
+
page.data // this page's records
|
|
108
|
+
page.meta // { next_cursor, has_more }
|
|
109
|
+
while (page.hasNextPage()) page = await page.getNextPage()
|
|
110
|
+
|
|
111
|
+
// Resume from a saved cursor
|
|
112
|
+
await nohead.records.list("posts", { cursor: page.meta.next_cursor! })
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Filters are equality filters (for fields with several values: "contains"), and accept strings, numbers, booleans and dates:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
nohead.records.list("posts", {
|
|
119
|
+
filter: { status: "published", featured: true, author: "rec_01J9…" },
|
|
120
|
+
sort: "-published_at",
|
|
121
|
+
expand: ["author", "tags"],
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Errors
|
|
126
|
+
|
|
127
|
+
Every error is a `NoheadError`. API errors are `APIError`s with `status`, `type`, `message`, `requestId`, `details` and `headers`, in a class per type:
|
|
128
|
+
|
|
129
|
+
| Class | Status |
|
|
130
|
+
| ------------------------- | ----------------------- |
|
|
131
|
+
| `InvalidRequestError` | 400 |
|
|
132
|
+
| `AuthenticationError` | 401 |
|
|
133
|
+
| `PlanLimitExceededError` | 402 |
|
|
134
|
+
| `AuthorizationError` | 403 |
|
|
135
|
+
| `NotFoundError` | 404 |
|
|
136
|
+
| `ConflictError` | 409 |
|
|
137
|
+
| `PreconditionFailedError` | 412 (`currentRevision`) |
|
|
138
|
+
| `ValidationError` | 422 |
|
|
139
|
+
| `RateLimitError` | 429 (`retryAfter`) |
|
|
140
|
+
| `InternalServerError` | 500 and other 5xx |
|
|
141
|
+
| `ServiceUnavailableError` | 503 |
|
|
142
|
+
|
|
143
|
+
Other errors: `ConnectionError`, `TimeoutError` (a `ConnectionError`), `AbortError` (your `signal` fired), `UploadError` and `WebhookVerificationError`.
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { ValidationError } from "@nohead/sdk"
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
await nohead.records.create("posts", { data: {} })
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error instanceof ValidationError) {
|
|
152
|
+
console.log(error.details) // [{ field: "title", code: "required", message: "…" }]
|
|
153
|
+
} else {
|
|
154
|
+
throw error
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Retries and idempotency
|
|
160
|
+
|
|
161
|
+
Failed requests are retried twice by default (`maxRetries`), with exponential backoff:
|
|
162
|
+
|
|
163
|
+
- what's retried: connection errors, timeouts, 429, 500, 502, 503, 504, and a 409 for a request that is still running
|
|
164
|
+
- `Retry-After` is honored up to 60 seconds; a longer one throws `RateLimitError` straight away
|
|
165
|
+
|
|
166
|
+
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
|
+
|
|
168
|
+
```ts
|
|
169
|
+
await nohead.records.create(
|
|
170
|
+
"posts",
|
|
171
|
+
{ data },
|
|
172
|
+
{ idempotencyKey: `import-${row.id}` }
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Every method takes request options as its last argument:
|
|
177
|
+
|
|
178
|
+
- `idempotencyKey`
|
|
179
|
+
- `ifMatch` (record writes)
|
|
180
|
+
- `changeNote`, a reason shown in history
|
|
181
|
+
- `signal`, `timeout`, `maxRetries` and `headers`
|
|
182
|
+
|
|
183
|
+
## Concurrency
|
|
184
|
+
|
|
185
|
+
Pass the revision you read to make sure nobody changed the record since:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
const post = await nohead.records.get(id)
|
|
189
|
+
try {
|
|
190
|
+
await nohead.records.update(id, { data: { title } }, { ifMatch: post })
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (error instanceof PreconditionFailedError) {
|
|
193
|
+
// changed since: reload, and merge or ask
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`ifMatch` takes a record or a revision number, on `update`, `delete`, `publish`, `unpublish` and `revisions.revert`.
|
|
199
|
+
|
|
200
|
+
## Assets
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
import { openAsBlob } from "node:fs"
|
|
204
|
+
|
|
205
|
+
const asset = await nohead.assets.upload(await openAsBlob("cover.jpg"), {
|
|
206
|
+
filename: "cover.jpg",
|
|
207
|
+
})
|
|
208
|
+
await nohead.records.update(id, { data: { cover: asset.id } })
|
|
209
|
+
|
|
210
|
+
const { url } = await nohead.assets.imageUrl(asset.id, {
|
|
211
|
+
width: 1200,
|
|
212
|
+
format: "webp",
|
|
213
|
+
})
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**What `upload` accepts:** a `Blob`, `File`, `ArrayBuffer`, `Uint8Array`, or a `ReadableStream` with `byte_size`.
|
|
217
|
+
|
|
218
|
+
**What it does:**
|
|
219
|
+
|
|
220
|
+
1. Creates the upload.
|
|
221
|
+
2. Sends the bytes straight to storage.
|
|
222
|
+
3. Completes the upload, which checks the file, and returns the `ready` asset.
|
|
223
|
+
|
|
224
|
+
**Errors:** `UploadError` if storage refuses the bytes; `ValidationError` if the file fails the checks.
|
|
225
|
+
|
|
226
|
+
**Uploading from a browser:** create the upload on your server with `createUpload`, `PUT` the file from the browser, then `complete` it.
|
|
227
|
+
|
|
228
|
+
## Search
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
// One collection
|
|
232
|
+
for await (const hit of nohead.records.search("posts", "content model")) { … }
|
|
233
|
+
|
|
234
|
+
// Across the project
|
|
235
|
+
const results = await nohead.search("content model", { collections: ["posts", "pages"] })
|
|
236
|
+
results.meta.total_estimate
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Search needs `search_enabled` collections and the `search:read` scope. It pages through the first 1,000 hits.
|
|
240
|
+
|
|
241
|
+
## Schema
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
const posts = await nohead.collections.create({
|
|
245
|
+
name: "Posts",
|
|
246
|
+
slug: "posts",
|
|
247
|
+
fields: [{ name: "Title", api_key: "title", type: "text", required: true }],
|
|
248
|
+
})
|
|
249
|
+
await nohead.fields.create("posts", {
|
|
250
|
+
name: "Summary",
|
|
251
|
+
api_key: "summary",
|
|
252
|
+
type: "long_text",
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
// Changes that rewrite records go through a migration; preview first
|
|
256
|
+
const preview = await nohead.fields.migrate("fld_…", {
|
|
257
|
+
type: "long_text",
|
|
258
|
+
dry_run: true,
|
|
259
|
+
})
|
|
260
|
+
const migration = await nohead.fields.migrate("fld_…", { type: "long_text" })
|
|
261
|
+
await nohead.migrations.get(migration.id)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
For schema as code, see the `nohead` CLI (`nohead schema pull/diff/push`).
|
|
265
|
+
|
|
266
|
+
## Webhooks
|
|
267
|
+
|
|
268
|
+
Verify a webhook request, then use its event:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
// e.g. a Next.js route handler
|
|
272
|
+
export async function POST(request: Request) {
|
|
273
|
+
const event = await nohead.webhooks.unwrap(
|
|
274
|
+
await request.text(),
|
|
275
|
+
request.headers,
|
|
276
|
+
{
|
|
277
|
+
secret: process.env.NOHEAD_WEBHOOK_SECRET!,
|
|
278
|
+
}
|
|
279
|
+
)
|
|
280
|
+
if (event.type === "record.published") {
|
|
281
|
+
revalidateTag(event.data.record.collection)
|
|
282
|
+
}
|
|
283
|
+
return new Response(null, { status: 204 })
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
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.
|
|
288
|
+
|
|
289
|
+
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
|
+
|
|
291
|
+
## Raw responses
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
const { data, response, requestId } = await nohead.records
|
|
295
|
+
.get(id)
|
|
296
|
+
.withResponse()
|
|
297
|
+
response.headers.get("RateLimit-Remaining")
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Reference
|
|
301
|
+
|
|
302
|
+
| Resource | Methods |
|
|
303
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
304
|
+
| `records` | `list`, `get`, `create`, `update`, `delete`, `restore`, `publish`, `unpublish`, `schedule`, `unschedule`, `count`, `bulk`, `diff`, `search` |
|
|
305
|
+
| `records.revisions` | `list`, `get`, `revert` |
|
|
306
|
+
| `search` | across the project |
|
|
307
|
+
| `collections` | `list`, `get`, `create`, `update`, `delete`, `restore`, `schema` |
|
|
308
|
+
| `collections.schemaChanges` | `list`, `get` |
|
|
309
|
+
| `collections.searchIndex` | `get`, `rebuild` |
|
|
310
|
+
| `fields` | `list`, `create`, `update`, `delete`, `restore`, `reorder`, `removeAlias`, `migrate` |
|
|
311
|
+
| `migrations` | `list`, `get`, `cancel` |
|
|
312
|
+
| `assets` | `upload`, `createUpload`, `complete`, `list`, `get`, `delete`, `restore`, `imageUrl`, `downloadUrl` |
|
|
313
|
+
| `webhooks` | `list`, `get`, `create`, `update`, `delete`, `rotateSecret`, `test`, `unwrap` |
|
|
314
|
+
| `webhooks.deliveries` | `list`, `get`, `retry` |
|
|
315
|
+
| `auditEvents` | `list` |
|
|
316
|
+
| `featureFlags` | `list` |
|
|
317
|
+
| `me` | `get` |
|
|
318
|
+
| `health` | `check` |
|
|
319
|
+
|
|
320
|
+
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
|
+
|
|
322
|
+
## Development
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
npm install # also builds dist/
|
|
326
|
+
npm test # unit and contract tests
|
|
327
|
+
npm run lint && npm run typecheck && npm run format:check
|
|
328
|
+
npm run generate # after updating openapi.json
|
|
329
|
+
npm run samples # after changing test/calls.ts (the docs' code samples)
|
|
330
|
+
NOHEAD_API_URL=http://localhost:3000 NOHEAD_API_KEY=sk_live_… npm run smoke
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**How the code is organized:**
|
|
334
|
+
|
|
335
|
+
- `openapi.json` is the API's published contract. `npm run generate` derives two things from it:
|
|
336
|
+
- the types, in `src/generated/types.gen.ts` (hey-api)
|
|
337
|
+
- the operation table, in `src/generated/operations.gen.ts`
|
|
338
|
+
- 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.
|
|
340
|
+
|
|
341
|
+
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
|
+
|
|
343
|
+
## Releasing
|
|
344
|
+
|
|
345
|
+
1. Bump the version in `package.json` and `src/version.ts`.
|
|
346
|
+
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`. It checks the version and its notes, tests, and stages the package on npm with provenance through trusted publishing (no token). Then it creates a draft GitHub release.
|
|
350
|
+
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
|
+
|
|
352
|
+
## License
|
|
353
|
+
|
|
354
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ListMeta } from "./generated/types.gen.ts";
|
|
2
|
+
/** A response's data with the raw `Response`, from `withResponse()`. */
|
|
3
|
+
export interface APIResponse<T> {
|
|
4
|
+
data: T;
|
|
5
|
+
response: Response;
|
|
6
|
+
/** The `X-Request-Id` header (`req_…`). */
|
|
7
|
+
requestId: string | undefined;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* What every method returns: a promise of the resource, which also offers
|
|
11
|
+
* `withResponse()` for the raw response (headers, status) alongside it.
|
|
12
|
+
*
|
|
13
|
+
* const record = await nohead.records.get(id)
|
|
14
|
+
* const { data, response } = await nohead.records.get(id).withResponse()
|
|
15
|
+
*/
|
|
16
|
+
export declare class APIPromise<T> implements PromiseLike<T> {
|
|
17
|
+
#private;
|
|
18
|
+
constructor(raw: Promise<APIResponse<T>>);
|
|
19
|
+
/** The data together with the `Response` it came from. */
|
|
20
|
+
withResponse(): Promise<APIResponse<T>>;
|
|
21
|
+
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
|
|
22
|
+
catch<TResult = never>(onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null): Promise<T | TResult>;
|
|
23
|
+
finally(onfinally?: (() => void) | null): Promise<T>;
|
|
24
|
+
get [Symbol.toStringTag](): string;
|
|
25
|
+
}
|
|
26
|
+
/** One page of a list, iterable across the pages that follow it. */
|
|
27
|
+
export declare class Page<T, Meta extends ListMeta = ListMeta> implements AsyncIterable<T> {
|
|
28
|
+
#private;
|
|
29
|
+
/** The items on this page. */
|
|
30
|
+
readonly data: T[];
|
|
31
|
+
/** `next_cursor` and `has_more` (and `total_estimate` for search). */
|
|
32
|
+
readonly meta: Meta;
|
|
33
|
+
constructor(data: T[], meta: Meta, fetch: (cursor: string) => Promise<Page<T, Meta>>);
|
|
34
|
+
hasNextPage(): boolean;
|
|
35
|
+
/** The next page; throws when there is none (check `hasNextPage()`). */
|
|
36
|
+
getNextPage(): Promise<Page<T, Meta>>;
|
|
37
|
+
/** Every item from this page on, fetching later pages as needed. */
|
|
38
|
+
[Symbol.asyncIterator](): AsyncIterator<T>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* What list methods return. Awaiting it gives the first page; iterating it
|
|
42
|
+
* walks every item across pages.
|
|
43
|
+
*
|
|
44
|
+
* const page = await nohead.records.list("posts")
|
|
45
|
+
* for await (const record of nohead.records.list("posts")) { … }
|
|
46
|
+
*/
|
|
47
|
+
export declare class PagePromise<T, Meta extends ListMeta = ListMeta> extends APIPromise<Page<T, Meta>> implements AsyncIterable<T> {
|
|
48
|
+
[Symbol.asyncIterator](): AsyncIterator<T>;
|
|
49
|
+
get [Symbol.toStringTag](): string;
|
|
50
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { NoheadError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* What every method returns: a promise of the resource, which also offers
|
|
4
|
+
* `withResponse()` for the raw response (headers, status) alongside it.
|
|
5
|
+
*
|
|
6
|
+
* const record = await nohead.records.get(id)
|
|
7
|
+
* const { data, response } = await nohead.records.get(id).withResponse()
|
|
8
|
+
*/
|
|
9
|
+
export class APIPromise {
|
|
10
|
+
#raw;
|
|
11
|
+
constructor(raw) {
|
|
12
|
+
this.#raw = raw;
|
|
13
|
+
}
|
|
14
|
+
/** The data together with the `Response` it came from. */
|
|
15
|
+
withResponse() {
|
|
16
|
+
return this.#raw;
|
|
17
|
+
}
|
|
18
|
+
then(onfulfilled, onrejected) {
|
|
19
|
+
return this.#raw.then((r) => r.data).then(onfulfilled, onrejected);
|
|
20
|
+
}
|
|
21
|
+
catch(onrejected) {
|
|
22
|
+
return this.then(undefined, onrejected);
|
|
23
|
+
}
|
|
24
|
+
finally(onfinally) {
|
|
25
|
+
return this.then().finally(onfinally);
|
|
26
|
+
}
|
|
27
|
+
get [Symbol.toStringTag]() {
|
|
28
|
+
return "APIPromise";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** One page of a list, iterable across the pages that follow it. */
|
|
32
|
+
export class Page {
|
|
33
|
+
/** The items on this page. */
|
|
34
|
+
data;
|
|
35
|
+
/** `next_cursor` and `has_more` (and `total_estimate` for search). */
|
|
36
|
+
meta;
|
|
37
|
+
#fetch;
|
|
38
|
+
constructor(data, meta, fetch) {
|
|
39
|
+
this.data = data;
|
|
40
|
+
this.meta = meta;
|
|
41
|
+
this.#fetch = fetch;
|
|
42
|
+
}
|
|
43
|
+
hasNextPage() {
|
|
44
|
+
return this.meta.has_more && this.meta.next_cursor !== null;
|
|
45
|
+
}
|
|
46
|
+
/** The next page; throws when there is none (check `hasNextPage()`). */
|
|
47
|
+
getNextPage() {
|
|
48
|
+
if (!this.hasNextPage()) {
|
|
49
|
+
return Promise.reject(new NoheadError("There is no next page"));
|
|
50
|
+
}
|
|
51
|
+
return this.#fetch(this.meta.next_cursor);
|
|
52
|
+
}
|
|
53
|
+
/** Every item from this page on, fetching later pages as needed. */
|
|
54
|
+
async *[Symbol.asyncIterator]() {
|
|
55
|
+
yield* this.data;
|
|
56
|
+
let next = this.hasNextPage() ? await this.getNextPage() : undefined;
|
|
57
|
+
while (next) {
|
|
58
|
+
yield* next.data;
|
|
59
|
+
next = next.hasNextPage() ? await next.getNextPage() : undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* What list methods return. Awaiting it gives the first page; iterating it
|
|
65
|
+
* walks every item across pages.
|
|
66
|
+
*
|
|
67
|
+
* const page = await nohead.records.list("posts")
|
|
68
|
+
* for await (const record of nohead.records.list("posts")) { … }
|
|
69
|
+
*/
|
|
70
|
+
export class PagePromise extends APIPromise {
|
|
71
|
+
async *[Symbol.asyncIterator]() {
|
|
72
|
+
yield* await this;
|
|
73
|
+
}
|
|
74
|
+
get [Symbol.toStringTag]() {
|
|
75
|
+
return "PagePromise";
|
|
76
|
+
}
|
|
77
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { PagePromise } from "./api-promise.ts";
|
|
2
|
+
import { type ClientOptions, type RequestOptions } from "./core.ts";
|
|
3
|
+
import type * as G from "./generated/types.gen.ts";
|
|
4
|
+
import { Assets } from "./resources/assets.ts";
|
|
5
|
+
import { AuditEvents, FeatureFlags, Health, Me } from "./resources/other.ts";
|
|
6
|
+
import { Records } from "./resources/records.ts";
|
|
7
|
+
import { Collections, Fields, Migrations } from "./resources/schema.ts";
|
|
8
|
+
import { Webhooks } from "./resources/webhooks.ts";
|
|
9
|
+
import type { NoheadRecord, ProjectSearchParams, RecordData, SearchMeta } from "./types.ts";
|
|
10
|
+
/**
|
|
11
|
+
* A client for the Nohead API, authenticated with a project API key.
|
|
12
|
+
*
|
|
13
|
+
* import { Nohead } from "@nohead/sdk"
|
|
14
|
+
*
|
|
15
|
+
* const nohead = new Nohead() // NOHEAD_API_KEY
|
|
16
|
+
* for await (const post of nohead.records.list("posts")) console.log(post.data.title)
|
|
17
|
+
*/
|
|
18
|
+
export declare class Nohead {
|
|
19
|
+
#private;
|
|
20
|
+
readonly records: Records;
|
|
21
|
+
readonly collections: Collections;
|
|
22
|
+
readonly fields: Fields;
|
|
23
|
+
readonly migrations: Migrations;
|
|
24
|
+
readonly assets: Assets;
|
|
25
|
+
readonly webhooks: Webhooks;
|
|
26
|
+
readonly auditEvents: AuditEvents;
|
|
27
|
+
readonly featureFlags: FeatureFlags;
|
|
28
|
+
readonly me: Me;
|
|
29
|
+
readonly health: Health;
|
|
30
|
+
constructor(options?: ClientOptions);
|
|
31
|
+
/**
|
|
32
|
+
* Full-text search across the key's project (or some of its
|
|
33
|
+
* collections), most relevant first, through the first 1,000 hits.
|
|
34
|
+
*/
|
|
35
|
+
search<D = RecordData>(query: string, params?: ProjectSearchParams, options?: RequestOptions): PagePromise<NoheadRecord<D>, SearchMeta>;
|
|
36
|
+
}
|
|
37
|
+
/** The main types, also reachable as `Nohead.Record`, `Nohead.Collection`… */
|
|
38
|
+
export declare namespace Nohead {
|
|
39
|
+
type Record<D = RecordData> = NoheadRecord<D>;
|
|
40
|
+
type Collection = G.Collection;
|
|
41
|
+
type Field = G.Field;
|
|
42
|
+
type Asset = G.Asset;
|
|
43
|
+
type Webhook = G.Webhook;
|
|
44
|
+
type Schema = G.Schema;
|
|
45
|
+
type FieldMigration = G.FieldMigration;
|
|
46
|
+
type RecordRevision = G.RecordRevision;
|
|
47
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Core } from "./core.js";
|
|
2
|
+
import { Assets } from "./resources/assets.js";
|
|
3
|
+
import { AuditEvents, FeatureFlags, Health, Me } from "./resources/other.js";
|
|
4
|
+
import { Records } from "./resources/records.js";
|
|
5
|
+
import { Collections, Fields, Migrations } from "./resources/schema.js";
|
|
6
|
+
import { Webhooks } from "./resources/webhooks.js";
|
|
7
|
+
/**
|
|
8
|
+
* A client for the Nohead API, authenticated with a project API key.
|
|
9
|
+
*
|
|
10
|
+
* import { Nohead } from "@nohead/sdk"
|
|
11
|
+
*
|
|
12
|
+
* const nohead = new Nohead() // NOHEAD_API_KEY
|
|
13
|
+
* for await (const post of nohead.records.list("posts")) console.log(post.data.title)
|
|
14
|
+
*/
|
|
15
|
+
export class Nohead {
|
|
16
|
+
records;
|
|
17
|
+
collections;
|
|
18
|
+
fields;
|
|
19
|
+
migrations;
|
|
20
|
+
assets;
|
|
21
|
+
webhooks;
|
|
22
|
+
auditEvents;
|
|
23
|
+
featureFlags;
|
|
24
|
+
me;
|
|
25
|
+
health;
|
|
26
|
+
#core;
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
const core = new Core(options);
|
|
29
|
+
this.#core = core;
|
|
30
|
+
this.records = new Records(core);
|
|
31
|
+
this.collections = new Collections(core);
|
|
32
|
+
this.fields = new Fields(core);
|
|
33
|
+
this.migrations = new Migrations(core);
|
|
34
|
+
this.assets = new Assets(core);
|
|
35
|
+
this.webhooks = new Webhooks(core);
|
|
36
|
+
this.auditEvents = new AuditEvents(core);
|
|
37
|
+
this.featureFlags = new FeatureFlags(core);
|
|
38
|
+
this.me = new Me(core);
|
|
39
|
+
this.health = new Health(core);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Full-text search across the key's project (or some of its
|
|
43
|
+
* collections), most relevant first, through the first 1,000 hits.
|
|
44
|
+
*/
|
|
45
|
+
search(query, params = {}, options) {
|
|
46
|
+
return this.#core.paginate("projects_search", { query: { q: query, ...params } }, options);
|
|
47
|
+
}
|
|
48
|
+
}
|