@relyio/sdk 0.4.0 → 0.6.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 +193 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/{chunk-43VPZT2G.js → chunk-7ABNXVWL.js} +464 -200
- package/dist/{chunk-HAQELDZN.cjs → chunk-DY2XT7OF.cjs} +2 -2
- package/dist/{chunk-XS32U2UP.cjs → chunk-GCMQUM6N.cjs} +466 -199
- package/dist/{chunk-XY5KFEFY.js → chunk-XJVY2CLY.js} +2 -2
- package/dist/client/index.cjs +3 -3
- package/dist/client/index.d.cts +5 -4
- package/dist/client/index.d.ts +5 -4
- package/dist/client/index.js +1 -1
- package/dist/define-content-type-CDB3kyeA.d.cts +1173 -0
- package/dist/define-content-type-CDB3kyeA.d.ts +1173 -0
- package/dist/{define-schema-DzYXk4HL.d.ts → define-schema-1kr2HIzN.d.cts} +25 -347
- package/dist/{define-schema-2xbHgtK-.d.cts → define-schema-ChvAzY8H.d.ts} +25 -347
- package/dist/{index-BzOFJJKS.d.ts → index-Df1VsfJR.d.cts} +334 -72
- package/dist/{index-BvO6HDtY.d.cts → index-XqoGUaAY.d.ts} +334 -72
- package/dist/{index-DFehx5sI.d.cts → index-d1Kz1e19.d.cts} +191 -29
- package/dist/{index-FDak5ofk.d.ts → index-yPbhZc6q.d.ts} +191 -29
- package/dist/index.cjs +161 -68
- package/dist/index.d.cts +302 -8
- package/dist/index.d.ts +302 -8
- package/dist/index.js +90 -3
- package/dist/schema/index.cjs +35 -35
- package/dist/schema/index.d.cts +11 -7
- package/dist/schema/index.d.ts +11 -7
- package/dist/schema/index.js +1 -1
- package/dist/types/index.d.cts +2 -2
- package/dist/types/index.d.ts +2 -2
- package/package.json +3 -3
- package/dist/define-content-type-CBfnTWSS.d.cts +0 -716
- package/dist/define-content-type-CBfnTWSS.d.ts +0 -716
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,198 @@
|
|
|
1
1
|
# @relyio/sdk
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- af38235: `relyio.changesHead()` returns the changes feed's current position without waiting for a change. Take it before you read what you are going to follow, and start the feed from it, so a write that lands between your read and the feed's start is replayed rather than missed:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
const since = await relyio.changesHead()
|
|
11
|
+
const post = await relyio.content('post').findById(id).execute()
|
|
12
|
+
for await (const change of relyio.changes({ since, ids: [id] })) {
|
|
13
|
+
// …
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`RELYIO_RESYNC_EVENT` (`'relyio:resync'`) is the event a preview surface dispatches on `window` when the feed lost its place and it re-read everything, so anything on the page may be stale. Its `detail`, `RelyIOResyncDetail`, says why: `{ cause: 'expired', reason }` or `{ cause: 'no-cursor' }`. It is never dispatched as a `relyio:change`, so a listener that reads a change's ids never receives one without them.
|
|
18
|
+
|
|
19
|
+
- 80cb307: `fetchOne()` and `fetchOneWithTag()` take the headers one read adds, and `findById()` has a matching `.headers()` step. A name the client already sends is replaced for that request only, so this reads an entry from another dataset your key can see without changing the client:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
const draft = await relyio.content('post').fetchOne('welcome', {
|
|
23
|
+
headers: { 'X-RelyIO-Dataset': 'staging' },
|
|
24
|
+
})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- 1fa0ba4: `followChanges()` follows the changes feed for as long as you need it, and reconnects for you. Hand it where the changes come from and what to do with each one:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { createClient, followChanges } from '@relyio/sdk'
|
|
31
|
+
|
|
32
|
+
const relyio = createClient({ apiKey: process.env.RELYIO_API_KEY! })
|
|
33
|
+
const controller = new AbortController()
|
|
34
|
+
|
|
35
|
+
const end = await followChanges(from => relyio.changes({ ...from, types: ['post'] }), {
|
|
36
|
+
signal: controller.signal,
|
|
37
|
+
onChange: change => refetch(change.contentType, change.entryId),
|
|
38
|
+
onResync: () => refetchEverything(),
|
|
39
|
+
onState: state => console.info(state.kind),
|
|
40
|
+
})
|
|
41
|
+
if (end.kind === 'stopped') console.error(end.error)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- Changes are handed to `onChange` one at a time, in order, and the next is not taken until the one before it has settled.
|
|
45
|
+
- When the feed fails with something retrying can fix, it waits and reopens from the last change it delivered, so nothing is skipped. The wait starts at up to one second and doubles to at most thirty, shortened by a random amount of up to half so that many clients do not reconnect at the same instant, and it is never shorter than a `Retry-After` the service sent. A delivered change resets it.
|
|
46
|
+
- `onResync` is called when what you derived from the feed may be stale: the cursor could no longer be replayed, or the feed failed before any change arrived, so there was nothing to resume from. Re-read what you hold there.
|
|
47
|
+
- Anything retrying cannot fix, such as a revoked key, ends it with `{ kind: 'stopped', error }`. Aborting the signal ends it with `{ kind: 'aborted', since }`, where `since` is the cursor to start a later follow from. It never rejects.
|
|
48
|
+
- `onState` reports `following`, `reconnecting` (with the error and the wait), `resynced` and `stopped`. `classifyFollowFailure()` is the default decision between retrying and stopping, and `FOLLOW_RETRY_BASE_MS` and `FOLLOW_RETRY_MAX_MS` are the wait's bounds.
|
|
49
|
+
- The first argument can be any source of changes, not only `relyio.changes()`. A source you write can call the `opened()` it is handed once its connection is up; the next time that connection drops, the wait starts again from one second even though no change arrived.
|
|
50
|
+
|
|
51
|
+
- 028739b: `createClient()` no longer throws when `apiKey` is missing or empty. The key is now required by each request instead: a client created without one sends nothing, and every read or write it makes rejects with `MissingApiKeyError` naming `apiKey`. A client created at the top of a module, such as `createClient({ apiKey: process.env.RELYIO_API_KEY! })`, can now be loaded by a build that has no keys set, such as `next build` in CI. Under `relyio dev` the sandbox's key is used, so `RELYIO_API_KEY` no longer needs to be set there either.
|
|
52
|
+
|
|
53
|
+
A key that is only whitespace is now refused the same way, before the request is sent. Before, it was sent and the API refused it.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { createClient, MissingApiKeyError } from '@relyio/sdk'
|
|
57
|
+
|
|
58
|
+
const relyio = createClient({ apiKey: process.env.RELYIO_API_KEY! }) // no longer throws when unset
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await relyio.content('post').findById('welcome').execute()
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error instanceof MissingApiKeyError) console.error('Set RELYIO_API_KEY')
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- 2f3e7e1: `isPublishableKey(apiKey)` says whether a key is a publishable key (`rly_pub_…`), the one kind of key that may sit in a browser. It reads the key's prefix only; whether the key is valid, active or allowed from the page's origin is still the API's answer. `PUBLISHABLE_KEY_PREFIX` is the prefix it checks.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { isPublishableKey } from '@relyio/sdk'
|
|
71
|
+
|
|
72
|
+
isPublishableKey(import.meta.env.VITE_RELYIO_PUBLISHABLE_KEY) // true
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- 7719c0a: The list, search, similar and ad-hoc query reads take the headers one read adds, so each can read another dataset your key can see without changing the client: `content(type).query().headers(…)`, `search(q).headers(…)`, `similar(id).headers(…)`, and a fourth `{ headers }` argument to `query.execute()`.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const drafts = await relyio
|
|
79
|
+
.search('welcome')
|
|
80
|
+
.across('*')
|
|
81
|
+
.headers({ 'X-RelyIO-Dataset': 'staging' })
|
|
82
|
+
.execute()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`search()` gains `.sort(field, direction)`, which sends the route's `sort` array (relevance orders the results otherwise), and `similar()` gains `.across(contentTypes | '*')` to find similar entries across several content types or all of them.
|
|
86
|
+
|
|
87
|
+
**Aggregate metrics are now the expression the service reads.** `AggregateMetric` was `{ fn, field? }`, a shape `POST /v1/query` refuses on every request — its validator tests each metric against `<field>.<fn>` and read the object as `[object Object]`. It is now that string, e.g. `'id.count'` or `'price.avg'`, and `AggregateBlock` gains the `where` the service already applies before grouping (in the Query DSL's bare spelling, `{ price: { gte: 10 } }`):
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
await relyio.query.aggregate('product', { count: 'id.count', avgPrice: 'price.avg' }, 'status')
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- 1772f21: Entries are typed field by field. Once `relyio generate-types` has registered your schema — or you pass `schema:` — every field of an entry has the type a read returns, where it used to be `unknown`:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const post = await relyio.content('post').fetchOne(id)
|
|
97
|
+
|
|
98
|
+
post?.title // string | undefined
|
|
99
|
+
post?.status // 'draft' | 'published' | undefined — an enum is the union of its options
|
|
100
|
+
post?.author // string | null | Author | undefined — the author's id, or the author once `include` names it
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
| Field type | Read as |
|
|
104
|
+
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
105
|
+
| `shortText`, `longText`, `email`, `url`, `slug` | `string` |
|
|
106
|
+
| `richText` | `string` — the stored HTML |
|
|
107
|
+
| `integer`, `decimal` | `number` |
|
|
108
|
+
| `boolean` | `boolean` |
|
|
109
|
+
| `enum` | the union of its option values; an array of them with `multi: true` |
|
|
110
|
+
| `date`, `dateTime` | `string` — `YYYY-MM-DD` and ISO 8601; the client does not convert them to `Date` |
|
|
111
|
+
| `reference` | the target's id, or the target entry once `include` expands it; `null` after its target was deleted with `onTargetDelete: 'nullify'`; an array of either with `multi: true` |
|
|
112
|
+
| `tags` | `string[]` |
|
|
113
|
+
| `list` | objects of its subfields, or `string[]` when it declares none |
|
|
114
|
+
| `component(…)` | an object of the component's fields |
|
|
115
|
+
|
|
116
|
+
- Every field is optional: a read leaves out a field with no value — a draft, a `fields` projection, a locale with nothing to fall back to.
|
|
117
|
+
- A localized field is the value for the locale you read, like any other field.
|
|
118
|
+
- `where()` is typed by the same fields: each key is one of the content type's fields and each value one of that field's values — a list field matches one of its items, a reference the target's id, an array means "any of". With no schema, `where()` now accepts any field name; before, it accepted only `_metadata`.
|
|
119
|
+
- `search()` results are typed by the schema: every content type's entries until `.in('post')` or `.across(['post', 'page'])` narrows them, and `.in()` takes only your content types' names.
|
|
120
|
+
- `InferContentType`, `InferSingleton`, `InferAssetType` and `InferSettingsGroup` take the schema as an optional second argument — `InferContentType<typeof post, typeof schema>` — which is what types an expanded reference as its entry and a component as its fields. Without it, those two are typed as an entry or an object whose fields are not known.
|
|
121
|
+
- `defineComponent()` keeps its fields' types, as `defineContentType()` does.
|
|
122
|
+
- New types: `SchemaEntry`, `SchemaEntryOf`, `FieldValue`, `FieldValues`, `UnresolvedEntry`, `UnresolvedComponent`, `SearchEntry`, `SearchTypeName`, `WhereScalar` and `WhereFieldCondition`.
|
|
123
|
+
|
|
124
|
+
Code that relied on a field being `unknown` may now fail to compile where it passes a value the field cannot hold — `create({ price: 'free' })` for a `decimal`, `where({ status: 'archived' })` when `archived` is not an option. Nothing changes at runtime.
|
|
125
|
+
|
|
126
|
+
- 88b004e: `create()` and `update()` take the headers one write adds, and a batch has a matching `.headers()` step. A name the client already sends is replaced for that request only, so this writes to another dataset your key can write without changing the client:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
await relyio.content('post').update(
|
|
130
|
+
'welcome',
|
|
131
|
+
{ title: 'Hello' },
|
|
132
|
+
{
|
|
133
|
+
locale: 'de-DE',
|
|
134
|
+
headers: { 'X-RelyIO-Dataset': 'staging' },
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`ifMatch` and `idempotencyKey` still decide `If-Match` and `Idempotency-Key`, whatever the headers name.
|
|
140
|
+
|
|
141
|
+
### Patch Changes
|
|
142
|
+
|
|
143
|
+
- ec18021: `browser: { includeCredentials: false }` now keeps the browser's cookies off requests to another origin. It used to send them exactly as `true` does. In a web page the client now sends `credentials: 'same-origin'` for `false` and `credentials: 'include'` for `true` or when the option is left unset, which is still the default. Outside a web page (on a server, in an edge runtime or in a Web Worker) the client sets no `credentials` mode at all, as before. The RelyIO API authenticates with your API key and reads no cookie, so turning cookies off changes nothing it answers:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
const relyio = createClient({
|
|
147
|
+
apiKey: 'rly_pub_…',
|
|
148
|
+
browser: { includeCredentials: false },
|
|
149
|
+
})
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- 88b004e: `REQUEST_INVALID`, `CONTENT_INVALID` and `REQUEST_TIMEOUT` refusals now keep the body the API sent on `error.context.response`, as every other refusal already did, so every message and `details` the API returned can be read — not only the first message the error class keeps. `InvalidPayloadError`, `ValidationError` and `TimeoutError` take an optional `context` as their last constructor argument, and a `REQUEST_TIMEOUT` refusal's `TimeoutError` now carries its status, `408`.
|
|
153
|
+
- d99bece: A client from `withLocale()` now reads in its locale everywhere, not only through `content()` and `asset()`. These reads asked for the default locale, and now ask for the client's:
|
|
154
|
+
|
|
155
|
+
- `singleton(key)`: `fetch()`, `fetchWithTag()` and `exists()`
|
|
156
|
+
- `settings(group)`: `fetch()`, `fetchWithTag()` and `exists()`
|
|
157
|
+
- `search()`, `similar()` and `autocomplete()`
|
|
158
|
+
- `content(type).children(id)`
|
|
159
|
+
- `executeQuery(name, params)`
|
|
160
|
+
- the ad-hoc queries, `adhoc()` and every `query` method, which send it as the `locale` parameter
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
const fr = relyio.withLocale('fr')
|
|
164
|
+
|
|
165
|
+
await fr.singleton('home').fetch() // French; before, the default locale
|
|
166
|
+
await fr.search('chaussures').execute() // French; before, the default locale
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
A locale you name on the call still wins over the client's: `.locale('de')`, `fetch({ locale: 'de' })`, `children(id, { locale: 'de' })`, or a `locale` parameter (for `executeQuery()`, `locales` as well). `updateConfig({ locale })` now reaches these reads too. A client with no locale sends none, as before.
|
|
170
|
+
|
|
171
|
+
Version history (`versions()`, `version()`) and writes are unchanged.
|
|
172
|
+
|
|
173
|
+
## 0.5.0
|
|
174
|
+
|
|
175
|
+
### Minor Changes
|
|
176
|
+
|
|
177
|
+
- c59912d: `client.sandbox` reports the `relyio dev` sandbox the client resolved, `client.project()` describes the project your key belongs to, and `@relyio/sdk` exports the `SandboxState`, `Project` and `RelyIOChangeDetail` types.
|
|
178
|
+
|
|
179
|
+
`client.project()` calls `GET /v1/project` and returns the organization, project, dataset and branch, each with its `id` and `name`, plus `studioUrl`, the address of the project's Studio. `dataset` is the one your requests read, which may be a sandbox rather than the project's default, so add it to a Studio link as `?dataset=`. `studioUrl` is `null` when the service has no Dashboard address configured.
|
|
180
|
+
|
|
181
|
+
`client.sandbox` is one of five states:
|
|
182
|
+
|
|
183
|
+
- `{ state: 'active', datasetId, branchId, expiresAt, source }` — the client is reading the sandbox. `source` is `'env'` under `relyio dev -- <command>` and `'file'` when it read `.relyio/dev.json`. `branchId` is `null` from the environment, which names no branch. `expiresAt` is `null` for a sandbox with no deadline.
|
|
184
|
+
- `{ state: 'expired', expiredAt }` or `{ state: 'unreadable', reason }` — a sandbox was found and ignored, so the client reads the dataset and key you configured. The console says so, as before.
|
|
185
|
+
- `{ state: 'absent' }` — no sandbox was found. A client created in the browser always reports this, because the browser cannot see the environment or the file.
|
|
186
|
+
- `{ state: 'disabled' }` — the client did not look, because you passed `dev: false`, or `NODE_ENV` is `production` and you did not pass `dev: true`.
|
|
187
|
+
|
|
188
|
+
It is plain JSON with no API key in it, so a Next.js server component can pass it to a client component as a prop:
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
<SandboxBadge sandbox={relyio.sandbox} />
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`RelyIOChangeDetail` is the `detail` a `RELYIO_CHANGE_EVENT` dispatch carries: the same `{ entryId, contentType, locale, kind, cursor }` as a `Change` from `relyio.changes()`.
|
|
195
|
+
|
|
3
196
|
## 0.4.0
|
|
4
197
|
|
|
5
198
|
### Minor Changes
|