@relyio/sdk 0.6.0 → 0.8.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +400 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/{chunk-DY2XT7OF.cjs → chunk-72UK5U7T.cjs} +70 -60
  4. package/dist/{chunk-I3ESNANB.js → chunk-H6IBQSMM.js} +2 -2
  5. package/dist/{chunk-7ABNXVWL.js → chunk-QFHVF3YF.js} +1617 -404
  6. package/dist/{chunk-HT6NFRM2.cjs → chunk-TZL374LS.cjs} +2 -2
  7. package/dist/{chunk-GCMQUM6N.cjs → chunk-XIE7WJV3.cjs} +1621 -407
  8. package/dist/{chunk-XJVY2CLY.js → chunk-YEJUMW4X.js} +70 -60
  9. package/dist/client/index.cjs +4 -3
  10. package/dist/client/index.d.cts +5 -4
  11. package/dist/client/index.d.ts +5 -4
  12. package/dist/client/index.js +2 -1
  13. package/dist/{index-d1Kz1e19.d.cts → define-content-type-cEGjIwT5.d.cts} +1859 -733
  14. package/dist/{index-yPbhZc6q.d.ts → define-content-type-cEGjIwT5.d.ts} +1859 -733
  15. package/dist/{define-schema-1kr2HIzN.d.cts → define-schema-BPj2srgh.d.ts} +189 -85
  16. package/dist/{define-schema-ChvAzY8H.d.ts → define-schema-DgbJ5Kwn.d.cts} +189 -85
  17. package/dist/index-B_rIWO_l.d.cts +860 -0
  18. package/dist/index-Cv4tmcrw.d.ts +860 -0
  19. package/dist/{index-Df1VsfJR.d.cts → index-DBoPPlT1.d.cts} +684 -277
  20. package/dist/{index-XqoGUaAY.d.ts → index-yRJ2IqM_.d.ts} +684 -277
  21. package/dist/index.cjs +148 -124
  22. package/dist/index.d.cts +174 -64
  23. package/dist/index.d.ts +174 -64
  24. package/dist/index.js +43 -18
  25. package/dist/schema/index.cjs +35 -35
  26. package/dist/schema/index.d.cts +13 -11
  27. package/dist/schema/index.d.ts +13 -11
  28. package/dist/schema/index.js +1 -1
  29. package/dist/types/index.d.cts +2 -2
  30. package/dist/types/index.d.ts +2 -2
  31. package/dist/utils/index.cjs +19 -19
  32. package/dist/utils/index.d.cts +10 -1
  33. package/dist/utils/index.d.ts +10 -1
  34. package/dist/utils/index.js +1 -1
  35. package/package.json +2 -1
  36. package/dist/define-content-type-CDB3kyeA.d.cts +0 -1173
  37. package/dist/define-content-type-CDB3kyeA.d.ts +0 -1173
package/CHANGELOG.md CHANGED
@@ -1,5 +1,405 @@
1
1
  # @relyio/sdk
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5207d3f: **`liveChanges()` follows a dataset's published changes at RelyIO's live edge, with a publishable key.** It opens one WebSocket to the edge and yields each change as ids — `{ entryId, contentType, locale, kind, cursor, fresh }` — the same `FeedChange` `changes()` now yields. Where a socket cannot open (a network that blocks sockets, or Node 18 and 20, which have no global `WebSocket`) it asks the edge for changes about every fifteen seconds instead, and tries a socket again about once a minute. Hand it to `followChanges()`, which reconnects from the last change after a dropped connection — the edge drops every connection when it deploys — with a random, growing wait:
8
+
9
+ ```ts
10
+ import { createClient, followChanges } from '@relyio/sdk'
11
+
12
+ const relyio = createClient({ apiKey: 'rly_pub_…' })
13
+ const controller = new AbortController()
14
+ await followChanges(from => relyio.liveChanges({ ...from, types: ['post'] }), {
15
+ signal: controller.signal,
16
+ onChange: change =>
17
+ relyio.content(change.contentType).findById(change.entryId).fresh(change.fresh).execute(),
18
+ onResync: resync =>
19
+ refetchEverything(resync.cause === 'expired' ? resync.expired.fresh : undefined),
20
+ })
21
+ ```
22
+
23
+ **Refetch with `fresh`.** Every change `changes()` and `liveChanges()` deliver carries `fresh`, the feed's position at or after the change: the change's own cursor on `changes()`, the cursor of its batch at the edge. `findById()`, `query()` and `search()` take it as `.fresh(position)`, and a singleton's `fetch()` as `{ fresh }`; the read is sent with `?_fresh=…`, which the Content API accepts on every read its edge cache may keep and which changes nothing about the answer. Your reads go through that edge and through the browser's own cache, and a change can reach you a moment before its old answer has left them; a request naming a position nobody asked for before the change is one no cache holds an answer to, so it is answered after the change, and every visitor refetching for the same change shares that answer. Use `fresh`, not `cursor` — at the edge an earlier change of a batch carries the position before its batch as `cursor`. An expired cursor's `ChangesExpiredError` carries `fresh` too: the position the feed started again from, for refetching everything. The iterators' element type is now `FeedChange`, the document's `Change` plus `fresh`; `followChanges()` and `ChangeSource` take the change type as a parameter, so a source of plain `Change`s still follows as before.
24
+
25
+ `onExpired` and `signal` mean what they mean for `changes()`. Only a publishable key is sent to the edge: with any other key, `liveChanges()` sends nothing and ends with a `LiveEdgeError`. On a server, keep following `changes()` with a secret or preview key.
26
+
27
+ **Every other end is a `LiveEdgeError` that says what to do next**, and `followChanges()`'s default classifier reads it: `stop` for a refusal the same request would meet again (an unknown or revoked key, an origin the key does not list, a dataset it does not reach), `retry` for a dropped connection, and `back-off` when the project, or the visitor's address, already holds as many live connections as it may — a wait of at least a minute, longer each time it repeats, up to fifteen, without polling meanwhile. `FollowVerdict` gains `backOffMs` for that schedule, and `FOLLOW_BACK_OFF_MAX_MS` is its cap.
28
+
29
+ **Two new options.** `liveUrl` is the live edge; with the default `baseUrl` it is `https://live.relyio.com` and need not be set, and for any other `baseUrl` it must be. `webSocket` is the `WebSocket` class to open the socket with; omitted, the runtime's global one.
30
+
31
+ `ChangesExpiredError.reason` can now also be `ahead-of-head`, `foreign-cursor` or `too-far-behind`, the edge's reasons to refetch everything. A `followChanges()` source may now call `reached(cursor)` with a position it holds without a change, so a connection dropped before its first change reopens from there rather than re-reading everything.
32
+
33
+ ## 0.7.0
34
+
35
+ ### Minor Changes
36
+
37
+ - 4e50077: **`relyio validate` refuses what deploy refuses.** It used to check only that a schema parses; deploy then runs the full validation — reference targets, component field types, asset types, roles, queries — so a schema could validate cleanly and be refused by `relyio deploy`. `relyio init --example blog`'s old template did exactly that: no error from `validate`, six from deploy. `validate` (and the AI assistant's validation) now runs the same validation deploy runs, with the same settings, and prints its warnings too. What it still cannot check offline is a change against the schema your branch has deployed, such as a field whose type changed; `relyio status` checks those.
38
+
39
+ **A schema written in YAML and the same schema written in TypeScript deploy as the same schema version.** They used to differ in small ways, so moving a project from YAML to a `schema.ts` pulled with `relyio generate-types` deployed a new version with no change in it:
40
+
41
+ - a YAML item no longer sends its `id` inside the item, and a TypeScript one no longer sends its `name` — the key it is declared under is its id;
42
+ - a TypeScript schema with no asset types sends `assetTypes: {}`, as YAML does;
43
+ - the SDK's builders no longer write settings you did not: `hierarchical: false` on a content type or singleton, `deprecated: false` on a component, `requiredApprovals: 1` on a workflow stage, `enabled: false` and `stages: []` on a workflow. Each one you write is kept. Each was already the platform's default, so nothing behaves differently;
44
+ - the builders keep an empty list you write: a field's `rules: []`, an asset type's `accessTags: []`, and `defineContentTypeWorkflow({ stages: [] })`.
45
+
46
+ In the SDK's types, `ContentTypeDefinition.hierarchical`, `SingletonContentTypeDefinition.hierarchical`, `ComponentDefinition.deprecated`, `WorkflowStageDefinition.requiredApprovals` and `WorkflowDefinition`'s `enabled` and `stages` are now optional, since a definition carries them only when you set them.
47
+
48
+ **`defineSchema()` takes a schema with no content types**, as deploy does — `relyio init`'s bare template is one — and `relyio generate-types` writes one as a `schema.ts` that compiles. `contentTypes` itself is still required.
49
+
50
+ **A `schema.ts` section of the wrong shape is refused**, as it is in YAML. A `roles` written as an object rather than a list used to be dropped, and the schema deployed without its roles.
51
+
52
+ - 3ccd298: **Requests from a web page no longer send cookies to another origin by default.** `browser.includeCredentials` now defaults to `false`, so the client sends each request from a page with `credentials: 'same-origin'` unless you set the option to `true`. The RelyIO API authenticates with your API key and reads no cookie, so nothing it answers changes. A server that answers CORS with `Access-Control-Allow-Origin: *`, which a browser refuses for a request that carries cookies, now serves reads made with the default. If a server of your own on another origin sits in front of the API and reads cookies, opt back in:
53
+
54
+ ```ts
55
+ const relyio = createClient({
56
+ apiKey: 'rly_pub_…',
57
+ browser: { includeCredentials: true },
58
+ })
59
+ ```
60
+
61
+ Leaving the option out and writing `includeCredentials: undefined` both mean `false`. On a server, in an edge runtime or in a Web Worker the client still sets no `credentials` mode.
62
+
63
+ **An option passed to `createClient()` as `undefined` now takes its default.** The client used to copy `undefined` over the default, so `createClient({ apiKey, baseUrl: process.env.RELYIO_BASE_URL })` with the variable unset sent every request to `undefined/v1/…`. Now `undefined` means the same as leaving the option out, for every top-level option and for every field in `browser`, `cache`, `realtime` and `debug`:
64
+
65
+ ```ts
66
+ const relyio = createClient({
67
+ apiKey: process.env.RELYIO_API_KEY!,
68
+ baseUrl: process.env.RELYIO_BASE_URL, // unset: https://api.relyio.com
69
+ })
70
+ ```
71
+
72
+ A header given in `headers` as `undefined` is no longer sent. It used to go out with the string `undefined` as its value.
73
+
74
+ `client.getConfig()` now types every option that has a default as present, including each field of `browser`, `cache`, `realtime` and `debug` except `onCorsError`, `wsUrl` and `logger`. If you construct `HttpClient` yourself, its configuration must supply those fields too.
75
+
76
+ - f16fb55: **`_metadata` on an entry, a singleton and a settings group is now typed as the API sends it.** The types are read from the API's published document instead of being written out by hand, and the hand-written ones had drifted from what the API sends:
77
+
78
+ - `createdAt` and `updatedAt` are ISO 8601 strings, not `Date`s. The API has always sent strings, and the client never turned them into dates.
79
+ - An entry's `_metadata` now declares `availableLocales`, `defaultLocale`, `slug`, `refs`, `publishedAt`, `firstPublishedAt`, and, when the request asked for them, `requestedLocale`, `localeFallbacksApplied` and `localizations`. A single-entry read also declares `versionCount` and `versions`. `localeHasDraft`, `draftLocales` and `hasDraft` are always sent, so they are no longer optional, and `entryType` is one of `'content'`, `'asset'`, `'singleton'` or `'settings'`.
80
+ - `parentId` and `sourceDatasetId` are removed from an entry's `_metadata`. No read sends them.
81
+ - `SingletonMetadata` identifies the singleton by `id` and `type`, and `key` and `singletonType` are removed. It now declares `status`, the per-locale publication fields, `hasDraft`, `versionCount`, `versions`, `publishedAt` and `firstPublishedAt`.
82
+ - `SettingsGroupMetadata` identifies the group by `id` and `groupId`, and `key`, `groupType` and `createdAt` are removed. It now declares `locale`, the per-locale publication fields, `versionCount` and `versions`.
83
+ - `LocaleStatus`, one row of `availableLocales`, always has a `version`. `LocaleMetadata`, the fields `localeHelpers` read, declares `locale`, `defaultLocale` and `availableLocales` as always present.
84
+ - `SingletonContent` is now the singleton body exactly as the API sends it.
85
+
86
+ ```ts
87
+ const post = await relyio.content('post').findById(id).execute()
88
+ if (post) {
89
+ const updated = new Date(post._metadata.updatedAt) // a string, which the old type called a Date
90
+ const locales = post._metadata.availableLocales.map(locale => locale.code)
91
+ }
92
+ ```
93
+
94
+ Code that used `_metadata.createdAt` or `updatedAt` as a `Date`, or that builds a `_metadata` object by hand, such as a test fixture, may stop compiling. Wrap the string in `new Date(…)`, and give a fixture every field the type declares. Nothing the client sends or receives changes.
95
+
96
+ - c77bf58: **`AuthenticationError`, `SchemaValidationError`, `RequiredFieldError` and `InvalidFieldTypeError` are removed.** The SDK never threw them, so a `catch` that tested for one could never match. A key the API will not accept arrives as `MissingApiKeyError` or `InvalidApiKeyError`, and content the schema refuses as `ValidationError`, whose `violations` name each field and its problem:
97
+
98
+ ```ts
99
+ try {
100
+ await relyio.content('product').create({ title: '' })
101
+ } catch (error) {
102
+ if (error instanceof MissingApiKeyError || error instanceof InvalidApiKeyError) {
103
+ // check the key
104
+ } else if (error instanceof ValidationError) {
105
+ console.log(error.getViolationsForField('title'))
106
+ }
107
+ }
108
+ ```
109
+
110
+ **`waitUntilReady()` and `upload().waitForReady()` reject with an error you can branch on.** Both threw a plain `Error`. A file whose processing failed is now a `FileUploadError` whose `code` is the reason the service stored — `UNSUPPORTED_CODEC`, `CORRUPT_FILE` and so on — or `PROCESSING_FAILED` where it stored none this SDK knows, with what it stored in `context.processingError` and its `retriable` hint as `retryable`. A wait that runs past `timeoutMs` is a `TimeoutError`: nothing failed, and the file may still become ready. `FileUploadError['code']` is now typed `FileUploadErrorCode`, which is `FileErrorCode` plus `PROCESSING_FAILED`.
111
+
112
+ ```ts
113
+ try {
114
+ await relyio.asset('productPhoto').file(id).waitUntilReady()
115
+ } catch (error) {
116
+ if (error instanceof FileUploadError) console.log(`Processing failed: ${error.code}`)
117
+ else if (error instanceof TimeoutError) console.log('Still processing')
118
+ else throw error
119
+ }
120
+ ```
121
+
122
+ **`retryAttempts` must be a whole number of at least 1.** It is the most attempts one request makes, the first included — `1` turns retrying off — as the configuration page has said. `createClient()` and `updateConfig()` used to accept `0`, which made one attempt like `1`, and `NaN`, `Infinity` or a fraction, of which the first two never stopped retrying a failure the client retries; they now throw.
123
+
124
+ **`SearchClient.global()` is removed.** `createClient()` offered no way to reach it. Search every content type with `across('*')`:
125
+
126
+ ```ts
127
+ const results = await relyio.search('sony').across('*').limit(20).execute()
128
+ ```
129
+
130
+ - 62909ac: **Field builder options are spelled as the schema declares them.** Deploy stores a field's `config` as you write it, so an option the schema does not declare is accepted and then read by nothing. Three builders took such options, and the enum builder did not require a label the schema requires:
131
+
132
+ | Field | Was | Now |
133
+ | ------------------ | ---------------------------- | ------------------------------------ |
134
+ | `boolean` | `trueLabel`, `falseLabel` | `labelTrue`, `labelFalse` |
135
+ | `dateTime` | `timezone: boolean` | `timeZoneHandling: 'UTC' \| 'local'` |
136
+ | `date`, `dateTime` | `config.format` | `ui.options.format` |
137
+ | `enum` | an option's `label` optional | required |
138
+
139
+ ```ts
140
+ fields.boolean('featured', { config: { labelTrue: 'Featured', labelFalse: 'Not featured' } })
141
+ fields.dateTime('startsAt', {
142
+ config: { timeZoneHandling: 'UTC' },
143
+ ui: { displayName: 'Starts at', options: { format: 'YYYY-MM-DD HH:mm' } },
144
+ })
145
+ fields.enum('status', {
146
+ config: {
147
+ options: [
148
+ { value: 'draft', label: 'Draft' },
149
+ { value: 'live', label: 'Live' },
150
+ ],
151
+ },
152
+ })
153
+ ```
154
+
155
+ The old spellings stop compiling. An enum option without a `label` already failed at `relyio deploy`, which refuses it; it now fails where you write it.
156
+
157
+ `defaultValue` now takes what the field type's schema accepts, where it took anything: a string for text, slug, email, URL, date and dateTime fields, a number for integer and decimal, a boolean for boolean, and a string or a list of strings for enum and reference. Tags and list fields still take any value.
158
+
159
+ - a949208: **`updateConfig()` can clear an option: pass it as `null`.** Passing `undefined` keeps the current value, so there was no way left to drop a `locale` or `dataset` you had set short of creating a new client. Now `null` clears every option that has no default — `locale`, `dataset`, `apiKey`, `dev`, `fetch`, `browser.onCorsError`, `realtime.wsUrl` and `debug.logger` — and the client runs as though it had never been given one: reads name no locale of the client's, no `X-RelyIO-Dataset` header is sent, requests go through the runtime's own `fetch`, and so on. An option that has a default takes no `null` in TypeScript; from JavaScript, `null` gives it its default back, where it used to leave the option empty (`baseUrl: null` sent requests to `undefined/v1/…`).
160
+
161
+ ```ts
162
+ relyio.updateConfig({ locale: null }) // reads name no locale of the client's
163
+ relyio.updateConfig({ dataset: null }) // requests go to the key's default dataset
164
+ ```
165
+
166
+ **A sub-client you took before `updateConfig()` now sends through the update.** `content()`, `asset()`, `singleton()`, `settings()` and `query` clients kept the connection and the locale they were taken with, and so did a builder — `query()`, `findById()`, `file()`, `upload()`, `search()`, `batch()`, `adhoc()` — so after `updateConfig({ baseUrl, locale })` they went on sending to the old address, with the old headers, in the old locale, and `locale: null` never cleared theirs. Now every request after the call is sent as the update says — address, headers, key, dataset, locale, timeout, retries, `fetch` and logging — whichever client makes it. A locale the call names, a fluent `.locale()` and a sub-client's own `withLocale()` still win over the client's; a client made by `withLocale()` is a client of its own, which an update of its parent does not reach. An `upload()` stays in the locale it started in until it finishes. `getHttpClient()` returns the same client before and after an update.
167
+
168
+ ```ts
169
+ const posts = relyio.content('post')
170
+ relyio.updateConfig({ baseUrl: 'https://proxy.example.com', locale: 'fr' })
171
+ await posts.fetchOne(id) // sent to https://proxy.example.com, with ?locale=fr
172
+ ```
173
+
174
+ **Header names are matched without regard to case.** A header you pass — in `headers`, to `updateConfig()`, per request, or to `setDefaultHeaders()` — replaces the client's header of the same name however either is spelled, instead of being sent beside it, which `fetch` joined into one value (`main, staging`). `updateConfig({ headers: { 'X-Tenant': null } })` now stops sending a header configured as `x-tenant`.
175
+
176
+ **The key the client holds is the key it sends.** When a key is given both as `apiKey` and as an `X-API-Key` header, the client sends `apiKey` — or, under `relyio dev`, the sandbox's key — where it used to send the header, or both. A key in `headers` is sent only when the client holds no `apiKey`. `setApiKey()` and `updateConfig({ apiKey })` both remove a key given in `headers`, so a later `updateConfig()` no longer sends the old key beside the new one.
177
+
178
+ - 166a4ff: **An include's object form is `IncludeSelection`.** `IncludePath` — `{ path, include: string[], where }`
179
+ — described a subset of it and is removed: every value it typed is an `IncludeSelection`. Its `where`,
180
+ and a nested include's, is a `WhereCondition`, as a fetch's is, so an object naming no operator is a
181
+ type error, as the service refuses it.
182
+
183
+ ```ts
184
+ // before
185
+ import type { IncludePath } from '@relyio/sdk/schema'
186
+ const include: IncludePath = { path: 'items.product', include: ['brand'] }
187
+
188
+ // after
189
+ import type { IncludeSelection } from '@relyio/sdk/schema'
190
+ const include: IncludeSelection = { path: 'items.product', include: ['brand'] }
191
+ ```
192
+
193
+ - 1142d78: **A query's operator and include types match what deploy accepts and the Content API runs.**
194
+
195
+ - `WhereOperator` takes `eq`, which the API always ran, and every operator takes a number, string or boolean; `in`, `nin`, `exists` and `notExists` also take a `${param}`.
196
+ - `filters` take every `where` operator: `FilterOperator` is `WhereOperator`. It held six, each a `${param}` string, so `contains`, `startsWith` and `exists` filters — which the API runs — did not type-check.
197
+ - `AggregateBlock.having` takes the new `HavingOperator`: the same operators, each with a literal. It was `WhereOperator`, whose parameters `having` never reads.
198
+ - `IncludeRelationship.aggregate` is `'count'`. `sum`, `avg`, `min` and `max` computed nothing and deploy now refuses them.
199
+ - The ad-hoc client's `FilterValue` is a parameter string or a `FilterOperator`. A bare number or boolean, which it took, is not read as a filter.
200
+
201
+ ```ts
202
+ // before: a type error, though the API ran it
203
+ defineQuery('onSale', {
204
+ endpoint: '/api/on-sale',
205
+ fetch: { contentType: 'product' },
206
+ // @ts-expect-error
207
+ filters: { discount: { exists: true } },
208
+ })
209
+
210
+ // after
211
+ defineQuery('onSale', {
212
+ endpoint: '/api/on-sale',
213
+ fetch: { contentType: 'product' },
214
+ filters: { discount: { exists: true } },
215
+ })
216
+ ```
217
+
218
+ - cd52b9c: **A query's types match what deploy accepts and the Content API runs.**
219
+
220
+ - `defineQuery()` no longer takes a `similar` block, and `SimilarBlock`, `AdhocSimilarBlock` and `AdhocQueryBuilder.similar()` are removed. Deploy refused a query built on `similar` alone, and the API ran none. Find similar content with `relyio.similar(uid)`, which is unchanged.
221
+ - An aggregate block's metrics are `metrics`. `AggregateBlock.aggregations`, which these types called the main spelling, is removed: deploy refused it.
222
+ - `AggregateMetric` takes `first` and `last`, which the API runs.
223
+
224
+ ```ts
225
+ // before
226
+ defineQuery('totals', {
227
+ endpoint: '/api/totals',
228
+ aggregate: { source: 'order', aggregations: { total: 'amount.sum' } },
229
+ })
230
+
231
+ // after
232
+ defineQuery('totals', {
233
+ endpoint: '/api/totals',
234
+ aggregate: { source: 'order', metrics: { total: 'amount.sum' } },
235
+ })
236
+ ```
237
+
238
+ - adb19cb: **Every setting the schema declares can be written with the SDK, and each is deployed as written.**
239
+
240
+ The builders now take settings the schema declares and they refused:
241
+
242
+ | Where | Now takes |
243
+ | -------------------- | ---------------------------------------------------------------------------------------------------- |
244
+ | `fieldset()` | `collapsedByDefault` |
245
+ | `fields.component()` | `config` — `required`, `unique`, `defaultValue` — which validation reads as it does any field's |
246
+ | `defineQuery()` | `type` and `fullText`; a cache policy object without `policy` (it defaults to `public`) |
247
+ | a query's `include` | the schema's selection form, `{ path, fields, include, where }`, one level deep as the schema has it |
248
+ | a view filter | the schema's operators and values |
249
+
250
+ ```ts
251
+ fieldset('seo', {
252
+ displayName: 'SEO',
253
+ collapsedByDefault: true,
254
+ fields: [
255
+ /* … */
256
+ ],
257
+ })
258
+ fields.component('seo', 'seoMetadata', { config: { required: true } })
259
+ defineQuery('searchPosts', {
260
+ endpoint: '/api/search',
261
+ fetch: { contentType: 'blogPost' },
262
+ fullText: { fields: ['title', 'body'], query: '${q}' },
263
+ })
264
+ ```
265
+
266
+ `defineSingleton()` keeps the `workflow` it takes; it used to drop it. It no longer takes `hierarchical: true`, which deploy refuses for a singleton and it wrote as `false`.
267
+
268
+ A view filter's operators are now the schema's: `notContains`, `notIncludes`, `isOn`, `isBetween`, `thisYear` and `hasValue` are new, and `doesNotContain`, `isNotEmpty`, `isOnOrBefore`, `isOnOrAfter`, `tomorrow`, `lastWeek`, `lastMonth`, `includesAll`, `includesAny` and `excludes` stop compiling — deploy refused each of them. A filter's `value` is typed as the schema's.
269
+
270
+ `defineView()` no longer makes up a slug when you leave it out. It wrote `published-posts` for `publishedPosts`, while the platform gives a view without a slug its own, so the same view had a different URL written in TypeScript than in YAML. `ViewDefinition.slug` is optional.
271
+
272
+ `defineLocale()`, `defineRole()`, `defineWorkflowStage()`, `defineContentType()`, `defineSingleton()`, `defineSettingsGroup()` and `defineSchema()` write each setting you give them. They dropped `direction: 'ltr'`, `active: true` and empty lists — `fallback`, `accessTags`, `includes`, `permissions`, `allowedRoles`, `roles`, `views` — and an empty `description`. An empty `locales` is no longer dropped: it declares no default locale, and is refused at load as it is in YAML.
273
+
274
+ - d797377: **A search hit is typed as the hit the API sends, not as an entry.** `search()`, `similar()` and `autocomplete()` results are now read from the API's published document:
275
+
276
+ - A search or similar hit's `_metadata` is the new `SearchHitMetadata`: `id`, `type`, `slug`, `status`, the per-locale fields (`version`, `locale`, `localeStatus`, `localeHasDraft`, `draftLocales`), `hasDraft`, `createdAt`, `updatedAt` and `publishedAt`. It has no `entryType`, `defaultLocale`, `availableLocales`, `refs` or `firstPublishedAt`, which the old type promised and no hit carried. Read the entry for those: `relyio.content(hit._metadata.type).findById(hit._metadata.id)`.
277
+ - A hit's fields keep their types, and `_score`, `_scoreBreakdown` (`keyword`, `semantic`, `combined`) and `_highlights` (Atlas Search's `{ path, score, texts }` list) are typed as sent. `SimilarResult` is the same hit.
278
+ - `AutocompleteSuggestion` is `{ text, score, id, highlight? }`. The old type named `highlighted`, `entryId` and `contentType`, which the API never sent.
279
+ - `localeHelpers` no longer accept a search hit, which has no `availableLocales` for them to read: passing one is a compile error rather than an empty answer.
280
+ - `SettingsGroup` is removed. It described a body no method returns.
281
+
282
+ **A condition on `_metadata` is refused before it is sent.** The API filters on the fields a content type declares and on nothing under `_metadata`: `where({ _metadata: { status: 'active' } })` was answered with a 500, and `where({ '_metadata.status': 'active' })` with an empty page. `where()` on `content().query()`, `search()`, `autocomplete()` and `similar()` now throws a `ValidationError` naming the key, and sends nothing; a client typed by a schema is refused at compile time.
283
+
284
+ ```ts
285
+ const hits = await relyio.search('shoes').in('product').items()
286
+ for (const hit of hits) {
287
+ hit._metadata.localeStatus // as before
288
+ // hit._metadata.availableLocales — a compile error now: read the entry for its locales
289
+ }
290
+ ```
291
+
292
+ `@relyio/agents`: `searchEntries`' `entries` are typed as entries on a list read and as hits on a search or similar read.
293
+
294
+ - c34fa5e: `setApiKey()` now changes the key the client sends. It used to add an `Authorization: Bearer` header and leave `X-API-Key` as it was, and the API reads `X-API-Key` first, so a client created with a key kept sending the old one. A client created without a key kept refusing every request with `MissingApiKeyError`. Now:
295
+
296
+ - every later request sends the new key as `X-API-Key`, and no `Authorization` header, including requests from a `content()` or other sub-client you took before the call
297
+ - `getConfig().apiKey` returns the new key
298
+ - an empty or blank key counts as no key, just as it does when passed to `createClient`: the next request rejects with `MissingApiKeyError` and nothing is sent
299
+ - under `relyio dev`, the sandbox's key is still used, as it is over the key passed to `createClient`
300
+
301
+ ```ts
302
+ const relyio = createClient({ apiKey: process.env.RELYIO_API_KEY! })
303
+ relyio.setApiKey(rotatedKey)
304
+ await relyio.content('post').fetchOne(id) // sent with X-API-Key: rotatedKey
305
+ ```
306
+
307
+ **Breaking:** `HttpClient.setAuthToken()` is removed. The `Authorization` header it set had no effect: the API ignores it when `X-API-Key` is present, and the client does not send a request that has no `X-API-Key`. Use the client's `setApiKey()` instead.
308
+
309
+ The client now decides whether it is running in a browser with `isBrowser()`, which requires both a `window` and a `document`. Before, a `window` alone was enough. So in a runtime whose global `window` has no `document`, such as Deno 1.x, the client now behaves as it does on a server: it sends its `User-Agent`, sets no `credentials` mode, does not call `browser.onCorsError`, and no longer removes headers that only a browser forbids, such as `Origin`. Browsers, Node, edge runtimes and Web Workers behave as before.
310
+
311
+ - fda485b: `similar()` no longer has a `where()` method. Similar content is ranked from the source entry's stored vector, and that index can narrow by content type and locale only — never by an entry field — so a `where` sent to `POST /v1/search/similar` was accepted and ignored. The API now refuses it with `REQUEST_INVALID`, and the builder no longer offers it.
312
+
313
+ ```ts
314
+ // before: the filter was sent and silently ignored
315
+ await relyio.similar('entry_abc123').in('product').where({ category: 'electronics' }).execute()
316
+
317
+ // after: narrow by type and locale
318
+ await relyio.similar('entry_abc123').in('product').locale('fr-FR').limit(5).execute()
319
+ ```
320
+
321
+ - d9b076c: **A client with no schema registered can write, sort and select any field.** Without generated types, `create()` and `update()` accepted no fields at all, so the way the documentation writes content without codegen was a type error:
322
+
323
+ ```ts
324
+ const relyio = createClient({ apiKey })
325
+
326
+ await relyio.content('post').create({ title: 'Hello' }) // compiles; before, TS2353 on `title`
327
+ await relyio.content('post').update(id, { title: 'Hello again' })
328
+ await relyio.asset('productPhoto').create({ alt: 'Hero' }, { fileName: 'hero.jpg' })
329
+ await relyio.content('post').query().sort({ title: 'asc' }).select(['title']).execute()
330
+ ```
331
+
332
+ The same holds for every `batch()` operation's `content`, which no schema types for any client, and for `sort({ … })` and `select([…])`, which took only `_metadata` from a client with no schema. The API still checks what you send against the deployed schema. A client typed by a schema, registered or passed as `schema:`, still accepts only the fields its content types declare, typed as they declare them.
333
+
334
+ A condition or a sort on an asset client (`asset(type).query().where(…)`, `.sort({ … })`, `fetchFirst(…)`) now names the asset type's fields. A client with no schema accepts any field there too, where before it accepted only `file` and `urls`. A typed client no longer offers `file` and `urls` as conditions or sort keys: they are the blocks a read carries beside the fields, not fields, and a condition took no value for them besides an operator such as `$exists`. Code that named `file` or `urls` there stops compiling; `select()` still offers them. `ContentQueryBuilder` and `QueryOptions` take an optional second type parameter, the shape a condition and a sort name, which defaults to the first, so code that names one type parameter is unchanged. Nothing the client sends or receives changes.
335
+
336
+ - 8e77037: **`updateConfig()` now changes only what you pass it.** It used to replace each option you named whole. Naming one field of `browser`, `cache`, `realtime` or `debug` replaced the whole group: a configured `onCorsError` or `logger` was dropped and the other fields went back to their defaults. `headers` replaced every header the client sent, and an option passed as `undefined` went back to its default. Now:
337
+
338
+ - a group you name keeps every field you leave out
339
+ - `headers` adds or replaces the headers you name and keeps sending the rest; pass a header as `null` to stop sending it
340
+ - an option passed as `undefined` keeps its current value, the same as leaving it out
341
+
342
+ ```ts
343
+ const relyio = createClient({ apiKey, headers: { 'X-Tenant': 'acme' }, browser: { onCorsError } })
344
+
345
+ relyio.updateConfig({ headers: { 'X-Trace': traceId } }) // X-Tenant is still sent
346
+ relyio.updateConfig({ browser: { includeCredentials: true } }) // onCorsError is kept
347
+ relyio.updateConfig({ headers: { 'X-Tenant': null } }) // X-Tenant is no longer sent
348
+ ```
349
+
350
+ `updateConfig({ locale: undefined })` used to clear the client's locale, and `updateConfig({ dataset: undefined })` its dataset. Both now keep the current value; pass `null` to clear an option instead (see the note on clearing options). The argument's type is exported as `RelyIOConfigUpdate`. `createClient()` has nothing to clear, so its options still take no `null`. From JavaScript, a header passed to `createClient()` as `null` is left out instead of being sent as the string `null`.
351
+
352
+ Under `relyio dev`, `updateConfig()` checks for the sandbox again, as `createClient()` does. While the sandbox is active, the client still sends its key and dataset. If the sandbox has ended, the client goes back to the dataset and key you configured, including a key you passed to `setApiKey()` while the sandbox was active. It used to keep the sandbox's dataset and key while `client.sandbox` reported the sandbox as gone. A client from `withLocale()` behaves the same way.
353
+
354
+ - 62a189d: **`waitUntilReady()` reports a failed upload to a production key.** An asset is a draft until its file is ready and stays one when its file fails, so a key that reads published content found no entry, and the wait ran out its whole limit as a `TimeoutError` on a file that had already failed. When the entry read finds nothing, the wait now reads the file's binding and rejects with the `FileUploadError` the service stored — `UNSUPPORTED_MIME`, `CORRUPT_FILE` and so on. A file that is ready in an asset the key never sees (created with `publish: false`, or awaiting a workflow) still ends in a `TimeoutError`, whose message now says so; a read-only key, which cannot read the binding, is refused with `ReadOnlyKeyError` instead of waiting.
355
+
356
+ The binding read is also yours to call: `relyio.asset(type).file(id).status({ locale })` answers `{ status, processing? }` — the processing stage, progress and error — from the new `GET /v1/{assetType}/{id}/file`, whatever the asset is published in, and never the entry's fields. It takes the capability the upload calls take. The result type is exported as `FileStatusResult`.
357
+
358
+ - 54c0fad: **Writes and version history now follow a `withLocale()` client's locale.** These calls wrote to, or read the history of, the default locale, and now use the client's:
359
+
360
+ - `content(type)`: `create()`, `update()`, `versions()` and `version()`
361
+ - `asset(type)`: `create()` and `update()`, and the multipart calls `file(id).signPart()`, `listParts()`, `completeMultipart()` and `abortMultipart()`
362
+ - `singleton(key)`: `update()`, `versions()` and `version()`
363
+ - `settings(group)`: `update()`, `reset()`, `versions()` and `version()`
364
+ - every create, update, replace and singleton operation that `batch()` builds, including `bulkCreate()` and `bulkUpdate()`
365
+
366
+ ```ts
367
+ const fr = relyio.withLocale('fr')
368
+
369
+ await fr.content('post').update(id, { title: 'Bonjour' }) // writes French; before, the default locale
370
+ await fr.singleton('home').versions() // French history; before, the default locale's
371
+ await fr
372
+ .batch()
373
+ .create('post', { content: { title: 'Salut' } })
374
+ .execute() // a French entry
375
+ ```
376
+
377
+ A locale you name on the call still wins over the client's: the `locale` option or argument, and for singletons and settings the fluent `.locale()` too, which `versions()` and `version()` now also read. `updateConfig({ locale })` now reaches `batch()`. A client with no locale sends none, as before. A delete names no locale, `restore()` still takes the locale it restores, and an operation added with `batch().raw()` is sent exactly as you wrote it.
378
+
379
+ `settings(group).update()` now sends its fields as `{ content, locale }`, the body the API accepts. It used to send them at the top level, which the API refused with `REQUEST_INVALID`. `settings(group).reset(locale)` now sends the locale as `?locale=`, where the API reads it. It used to send it in the body, so the reset applied to the default locale.
380
+
381
+ ### Patch Changes
382
+
383
+ - d30ac92: **An asset's `file` is exactly the documented block.** `AssetFileBlock`, `AssetFileExtracted` and `FileStatusResult` are now read from the API's committed document (`AssetFile`, `FileStatus`) instead of being written out by hand, and the API now serves only what those declare. Until now every read of an asset returned the stored binding whole — `objectKey`, `storageVersion`, `streamVideoId`, `processing.autoPublish`, `processing.startedAt` and `processing.completedAt`, and, while a file was processing, `processing.claimedBy` — although these types said they were redacted.
384
+
385
+ The members the three types name are unchanged; they are type aliases now rather than interfaces, so a module augmentation of one no longer merges. `extracted` stays open for the keys enrichment adds.
386
+
387
+ - 95725c9: **`defineAssetType` takes an asset type with neither `fields` nor `fieldsets`.** The schema makes both optional — a file with no metadata of its own is an ordinary asset type — but `defineAssetType` required one of them in its types and threw `must have either fields or fieldsets` when both were absent. `AssetTypeConfig` gains the `AssetTypeConfigWithoutFields` member.
388
+
389
+ ```ts
390
+ const brandGuidelines = defineAssetType('brandGuidelines', {
391
+ displayName: 'Brand Guidelines',
392
+ file: { upload: { allowedMimeTypes: ['application/pdf'] } },
393
+ })
394
+ ```
395
+
396
+ - 98899df: **In a web page, the client now leaves out every header the browser sets itself, however you spell it.** It used to leave out `origin` and `ORIGIN` but send `Origin`, and the same for `User-Agent`, `Referer`, `Host`, `Connection` and `Upgrade`, so a header written the usual way reached `fetch`, which the browser then refused or ignored. On a server, in an edge runtime or in a Web Worker these headers are still sent as you wrote them.
397
+
398
+ `mountToolbar()` now asks the SDK's `isBrowser()` whether there is a page to mount into, as the rest of the SDK does. Where there is a global `document` but no `window` with one, it returns `{ state: 'disabled', reason: 'no-document' }` instead of mounting — and, where `window` could not take event listeners, instead of throwing.
399
+
400
+ - 81c6aa2: **`timeout` must be a number of milliseconds from 1 to 2147483647.** `createClient()` and `updateConfig()` accepted `0`, `NaN`, `Infinity` and any number too large for a timer, and none of them waits: a timer given one fires at once, so every request the client made ended as a `TimeoutError` on the next tick, whatever the server did. They now throw, naming the option. No value turns the timeout off; set one as long as the slowest request you expect. A fraction from 1 up is accepted.
401
+ - 62a189d: **`waitUntilReady()` refuses a `timeoutMs` or `pollIntervalMs` it cannot honour.** `timeoutMs: NaN` made a deadline no time reaches, so the wait never ended; `pollIntervalMs` of `0`, `NaN`, a fraction below 1, `Infinity` or more than 2147483647 went to a timer that fires after about 1 ms, so the wait read the asset about a thousand times a second. Both are now checked before the first read — and by the upload builder's `.waitForReady(options)` before anything is uploaded — and throw naming the option. `pollIntervalMs` takes the rule `timeout` does, 1 to 2147483647 ms; `timeoutMs` takes 0 to 2147483647 ms, and `0` still reads the file once and rejects with a `TimeoutError` if it is not ready.
402
+
3
403
  ## 0.6.0
4
404
 
5
405
  ### Minor Changes