@relyio/sdk 0.12.0 → 0.14.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 (48) hide show
  1. package/CHANGELOG.md +228 -0
  2. package/dist/{chunk-3DRDHXLY.js → chunk-5UMY6CS4.js} +435 -23
  3. package/dist/{chunk-FRJEUAAF.js → chunk-DBBZVZOS.js} +968 -2670
  4. package/dist/{chunk-DXCVO6M5.cjs → chunk-GUPV2M3X.cjs} +969 -2680
  5. package/dist/{chunk-POEEDYFM.js → chunk-PCFTJLEP.js} +543 -260
  6. package/dist/{chunk-FI5VIF4M.cjs → chunk-RSHMA4KT.cjs} +442 -22
  7. package/dist/{chunk-3MSFWXQL.cjs → chunk-X4KSS7PP.cjs} +542 -260
  8. package/dist/client/index.cjs +3 -3
  9. package/dist/client/index.d.cts +4 -5
  10. package/dist/client/index.d.ts +4 -5
  11. package/dist/client/index.js +1 -1
  12. package/dist/{include-paths-CL4gHv6I.d.cts → define-content-type-BUZeWtpk.d.cts} +4458 -1312
  13. package/dist/{include-paths-CrJNTsMl.d.ts → define-content-type-DOrZb_a_.d.ts} +4458 -1312
  14. package/dist/define-schema-5aHmsA8j.d.ts +699 -0
  15. package/dist/define-schema-nCfZeboB.d.cts +699 -0
  16. package/dist/index-B97ZDj44.d.ts +507 -0
  17. package/dist/index-C-pcllqY.d.cts +507 -0
  18. package/dist/index-DOhFQbv7.d.ts +3065 -0
  19. package/dist/index-DgVV5Nj7.d.cts +3065 -0
  20. package/dist/index.cjs +114 -147
  21. package/dist/index.d.cts +49 -87
  22. package/dist/index.d.ts +49 -87
  23. package/dist/index.js +5 -29
  24. package/dist/markdown/index.cjs +13750 -0
  25. package/dist/markdown/index.d.cts +111 -0
  26. package/dist/markdown/index.d.ts +111 -0
  27. package/dist/markdown/index.js +13742 -0
  28. package/dist/rich-text/index.cjs +26 -6
  29. package/dist/rich-text/index.d.cts +3 -148
  30. package/dist/rich-text/index.d.ts +3 -148
  31. package/dist/rich-text/index.js +1 -1
  32. package/dist/rich-text.d-BWc-5gX1.d.cts +326 -0
  33. package/dist/rich-text.d-BWc-5gX1.d.ts +326 -0
  34. package/dist/schema/index.cjs +33 -37
  35. package/dist/schema/index.d.cts +37 -191
  36. package/dist/schema/index.d.ts +37 -191
  37. package/dist/schema/index.js +1 -1
  38. package/dist/types/index.d.cts +2 -3
  39. package/dist/types/index.d.ts +2 -3
  40. package/package.json +14 -3
  41. package/dist/define-schema-DgctHlI2.d.cts +0 -1034
  42. package/dist/define-schema-mrEZbY1h.d.ts +0 -1034
  43. package/dist/index-B9RfZVF1.d.ts +0 -861
  44. package/dist/index-BrvEg6-A.d.cts +0 -3729
  45. package/dist/index-Pw0onkJe.d.ts +0 -3729
  46. package/dist/index-b-AkfTUv.d.cts +0 -861
  47. package/dist/rich-text.d-rKmpgdGd.d.cts +0 -200
  48. package/dist/rich-text.d-rKmpgdGd.d.ts +0 -200
package/CHANGELOG.md CHANGED
@@ -1,5 +1,233 @@
1
1
  # @relyio/sdk
2
2
 
3
+ ## 0.14.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 133e573: **0.14: one way to say each thing.** A schema is a set of keyed records, every read takes one options object, a version-checked write takes the entry you read, and search pages by number with a total. This is a breaking release: every old signature is removed, with no aliases and no deprecation window, so each old call fails to compile and you find them all at once. Only the TypeScript authoring changes — what deploys, is stored and is hashed is the same, so YAML schemas and their schema hashes are unchanged (except the legacy list form of `localization`, below).
8
+
9
+ ## Defining a schema
10
+
11
+ The key is the id everywhere. Write each declaration under its key in `defineSchema`, with no id of its own; only `fieldset('id', …)` and `defineLocale(code, …)` keep theirs.
12
+
13
+ ```ts
14
+ // 0.13
15
+ const post = defineContentType('post', {
16
+ displayName: 'Post',
17
+ fields: [
18
+ fields.shortText('title', {
19
+ config: { required: true },
20
+ ui: { displayName: 'Title' },
21
+ localization: true,
22
+ }),
23
+ fields.enum('status', { config: { options: [{ value: 'draft', label: 'Draft' }] } }),
24
+ fields.object('seo', {
25
+ config: { of: { id: 'seo', displayName: 'SEO', fields: [fields.shortText('metaTitle')] } },
26
+ }),
27
+ ],
28
+ })
29
+
30
+ // 0.14
31
+ const seo = fieldset('seo', { displayName: 'SEO', fields: { metaTitle: fields.shortText() } })
32
+ const post = defineContentType({
33
+ fields: {
34
+ title: fields.shortText({ required: true, localized: true }),
35
+ status: fields.enum({ options: { draft: 'Draft' } }),
36
+ seo: fields.object({ of: seo }),
37
+ },
38
+ })
39
+ export const schema = defineSchema({ contentTypes: { post } })
40
+ ```
41
+
42
+ | Removed | Write instead |
43
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
44
+ | An id passed to `defineContentType`, `defineSingleton`, `defineAssetType`, `defineSettingsGroup`, `defineQuery`, `defineRole`, `defineView`, `defineWorkflowStage` | `key: defineX({ … })` under its section in `defineSchema` (`roles`, `views`) or `defineWorkflow({ stages })` |
45
+ | An id passed to a Field builder, `fields.shortText('title', …)` | `title: fields.shortText({ … })` in the type's `fields` record |
46
+ | `fields` as a list; `roles`, `views` and workflow `stages` as lists | Records, in the order the form shows them |
47
+ | `config: { … }` on a Field | Its keys beside the Field's other options: `fields.shortText({ required: true, maxLength: 120 })` |
48
+ | `ui: { displayName, description }` | `displayName` and `description` beside `ui`; `ui` holds only the editor widget's options |
49
+ | `localization: true \| { enabled, requiredLocales, fallback }` | `localized: true \| { requiredLocales, fallback }` (YAML keeps `localization`) |
50
+ | Enum options as `[{ value, label }]` | A value-to-label map, `{ draft: 'Draft', live: 'Live' }`, its key order the display order. A whole-number value (`'1'`) is refused: write `'v1'` |
51
+ | A bare `{ id, displayName, fields }` object as a Fieldset | `fieldset('id', { fields: { … } })` — in a Section, an `of`, a rich-text Block or Inline element |
52
+ | A fieldset's `collapsedByDefault` | `collapsed` |
53
+ | The types `ContentTypeConfig`, `SingletonConfig`, `AssetTypeConfig`, `SettingsGroupConfig`, `WorkflowConfig` | `KeyedContentTypeConfig`, `KeyedSingletonConfig`, `KeyedAssetTypeConfig`, `KeyedSettingsGroupConfig`, `KeyedWorkflowConfig` |
54
+ | `defineSingletonContentType` and its type `InferSingletonContentType`, the long names of one function | `defineSingleton` and `InferSingleton` |
55
+
56
+ - **Labels are derived from keys.** `displayName` is optional on types, Fields, fieldsets, roles and stages: `pageSettings` reads "Page settings". A Field written without a label now deploys the derived one, so a schema that left labels out deploys with a different hash once, adding them. `displayNamePlural` is not derived.
57
+ - **Names that point elsewhere are checked as you type.** A reference's `to` and rich text's `links.to` against the schema's content, singleton and asset types; a locale's `fallback` and a Field's `requiredLocales` against the declared locales; `displayField` against the type's own Fields; a stage's roles against the built-in and declared roles; a type's `workflow.stages` against the workflow's stages. A typo fails to compile at the key that holds it.
58
+ - **Refused at `defineSchema`:** one definition registered under two keys, a Section whose key differs from its fieldset's id, and one Field id used twice across a type's Sections (naming both paths).
59
+ - A JavaScript caller still writing the old form is refused at runtime, naming the builder and what to write, rather than deploying a malformed definition.
60
+ - **The legacy list form of `localization` is gone everywhere** — TypeScript, YAML and the stored definition: a list (`localization: ['en', 'fr']`) is refused at deploy with one error at that Field saying what to write.
61
+ - `relyio generate-types` writes the keyed form, and the header of the file it writes reads with `list()`.
62
+ - `relyio deploy` and `relyio diff` say when a removed type and an added one look like a rename (same Fields), beside the counts: `looks renamed to <type>`. The deploy plan's `type-removed` change carries `probableRenameTo`.
63
+ - The CLI's blog template labels its two `seo` objects "SEO".
64
+
65
+ ## Reading
66
+
67
+ Every read takes one options object, on every handle — `content(type)`, `asset(type)`, `singleton(key)`, `settings(group)` — and in React and Next alike.
68
+
69
+ | Removed | Write instead |
70
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
71
+ | `content(t).query()…execute()`, `fetch(options)` | `list(options)` → the API's `{ data, page }` |
72
+ | `findById(id)…execute()`, `fetchOne(id, options)` | `get(idOrSlug, options)` → the entry or `null` |
73
+ | `fetchAll(locale?)` | `for await (const entry of listAll(options))`, or `Array.fromAsync(listAll())` |
74
+ | `fetchFirst(where, locale)` | `(await list({ where, locale, limit: 1 })).data[0] ?? null` |
75
+ | `exists(id)` | `(await get(id)) !== null` |
76
+ | `fetchOneWithTag(id)` | `get(id)`: the check travels in the entry's `_metadata` |
77
+ | `ofType(type)` | `client.content(type)` |
78
+ | `withLocale(locale)` — on the client and on a handle | `{ locale }` on the call, or `client.with({ locale, dataset })` |
79
+ | A handle's `getContentType()`, `getAssetType()`, `getSingletonKey()`, `getGroupId()`, `getDefaultLocale()` | The name you passed; `client.getLocale()` for the locale |
80
+ | `asset(t).query()`, `findById`, `fetchOne`, `fetchFirst` | `asset(t).list`, `get`, `listAll`, beside `create`, `file(uid)` and `upload(file)` |
81
+ | `singleton(k).fetch()`, `fetchWithTag()`, `exists()`, the fluent `include()`/`locale()`/`reset()` | `singleton(k).get({ include, locale, dataset, fresh })` → the singleton or `null` |
82
+ | `settings(g).fetch()`, `fetchWithTag()`, `exists()`, `include()`/`locale()`/`resetOptions()` | `settings(g).get(options)` (never `null`: a declared group answers its defaults) |
83
+ | `client.search(q)` chained builder (`.in()`, `.across()`, `.where()`, `.sort()`, `.fields()`, `.execute()`) | `client.search(q, { contentType, where, searchFields, select, sort, page, limit, locale, dataset, fresh, … })` |
84
+ | `client.autocomplete(q)` and `client.similar(id)` builders | `client.autocomplete(q, { contentType, field, … })`, `client.similar(id, { contentType, limit, … })` |
85
+ | `client.adhoc()`, `client.query.execute/build/fetch/fetchSingleton/aggregate` | `client.query({ from, where, include, select, sort, limit, page, locale, aggregate, params, fullText })` |
86
+ | `client.executeQuery(name, params)` | `client.query(name, { params })` — by name alone (`POST /v1/queries/{name}`), no schema needed |
87
+ | `client.getSchemaInfo(type?)` | `client.schema(type?)` (the client's `schema` getter, which handed back the local definition, is gone) |
88
+ | Per-call `headers` | `client.with({ dataset })`, `create`'s `dataset`, or client-level `headers` |
89
+ | `version(id, n, { locale })`, a singleton's or group's `version(n)` | `version(id, { version: n, locale })`, `version({ version: n, locale })` |
90
+ | `restore(id, n, locale, { ifMatch })` | `restore(entry, { version: n })` (checked) or `restore(id, { version: n, locale })` (unchecked) |
91
+ | Classes `ContentQueryBuilder`, `ContentByIdBuilder`, `SearchClient`, `SearchBuilder`, `AutocompleteBuilder`, `SimilarBuilder`, `BatchClient`, `BatchBuilder`, `AdhocQueryClient`, `AdhocQueryBuilder`, and their types (`ContentFetchOptions`, `SingletonFetchOptions`, `SettingsFetchOptions`, `ExtendedQueryResult`, `ContentQueryResult`, `QueryOptions`, `SortOptions`, `SearchMode`, `SearchFilter`, `SearchFields`, `SearchSort`, `BatchMethod`, `BatchOperation`, `BatchOptions`, `AdhocQueryMethods`, `AdhocQueryDefinition`, `AdhocQueryOptions`, `AdhocQueryResult`, `AdhocFetchBlock`, `AdhocAggregateBlock`, `AdhocSearchBlock`, `FilterValue`, `FullTextConfig`) | The calls above, typed by `ListOptions`, `GetOptions`, `ListAllOptions`, `DocumentReadOptions`, `EntryList`, `SortOption`, `SearchOptions`, `SearchWhere`, `AdhocQuery`, `QueryAnswer`, `BatchWrite`, `BatchWriteOptions`, `BatchBody` |
92
+ | `client.ready()`, `testConnection()`, `getLocales()`, `getDefaultLocale()`, `listContentTypes()` | `health()` (rejects when the API cannot serve), `locales()` (each marked `default`), `contentTypes()` |
93
+
94
+ One name per option:
95
+
96
+ - **Paging:** `limit`, plus `cursor` (to walk; `page.nextCursor`) or `page` (an archive page like `/blog/page/3`). `offset` and `pageSize` are gone; a list carries no total and a page-number read runs no count.
97
+ - **`sort`:** an object whose key order is the priority, `{ featured: 'desc', publishedAt: 'desc' }`, its keys checked against the type.
98
+ - **`select`:** the entry's own Fields, checked; an expanded reference's Fields are picked in its include path, `'author(name)'`. It replaces `select()` and `fields()`.
99
+ - **`locale`:** a code, a list of codes or `'*'`, checked against the declared locales; the read's type gains `localizations` only for a list or `'*'`. A per-call locale replaces the client's.
100
+ - **`dataset`:** per call, sent as that request's `X-RelyIO-Dataset`.
101
+ - **`fresh`:** `fresh: '<cursor>'` keeps meaning "no older than this change"; `fresh: true` reads past every cache (`Cache-Control: no-cache`) for a write flow after a conflict.
102
+ - **`children`:** `true`, or `{ depth: 1 | 2 | 3, limit }`. `ChildrenOptions.contentTypes`, which never worked, is gone, and an unknown `?children=` value is a 400.
103
+ - **`where`:** a list's `where` now refuses two spellings it used to accept: an operator written without its `$` (`{ price: { gte: 10 } }`) and an unknown key beside an operator (`{ price: { $gte: 10, max: 20 } }`) are each a 400 naming the key — write `$gte`, and one operator per key. The query language — saved queries, the search `where`, the ad-hoc query — now reads the same `$`-prefixed `where` a list takes, parsed by one read core: `$eq $ne $gt $gte $lt $lte $in $nin $contains $startsWith $endsWith $exists $isNull $inTheLast $inTheNext`, and refuses the same two spellings. `$startsWith` and `$endsWith` match case; `notExists` is gone (`{ $exists: false }`). A search's and an autocomplete's `where` are typed as a list's. A saved query written with the bare dialect must be rewritten and redeployed. Each operator now means in a saved query's fetch and aggregate `where` exactly what it means in a list, since the same read core compiles them: `$isNull` on a reference inside a block is "no item holds one", where a saved query used to ask whichever item it read first. A refused operator object is refused in the same words by a list and a query.
104
+ - **Not found:** `get` returns `null` only when the API's not-found answer names the entry. A content type, singleton or schema nobody deployed throws `ResourceNotFoundError` naming which. `version()`, on every handle, answers the version or throws `ResourceNotFoundError` naming what is missing — the version, the entry or document, its type or the schema; a singleton's and a settings group's no longer answer `null`.
105
+ - **`children(id, options)`** takes `get`'s options — `include` checked against the type, `locale` as a code, a list or `'*'`, `dataset`, `fresh`, `children` — and its children are typed as `get` types an entry.
106
+
107
+ `client.with({ locale, dataset })` returns a client with those defaults that otherwise follows its parent: its key, base URL and every later configuration change. `versionCheckOf(entry)` is exported for building a check yourself.
108
+
109
+ ## Writing
110
+
111
+ - **A version check comes from the entry you read.** `update(entry, data)`, `delete(entry)` and `restore(entry, { version })` check the version the entry's `_metadata` carries — present whether or not a cache served the read — in the locale it was read in, so a read negotiated from `Accept-Language` is written back to that locale. A stale write throws `ResourceConflictError` naming the versions that disagreed and saying to re-read with `fresh: true` and retry. `update(id, data)` and `delete(id)` write unchecked, on purpose. No write takes `ifMatch`; `entityTagOf` and `ConditionalWriteOptions` are gone, and the SDK no longer reads the `X-Entity-Tag` header (the API still sends it for plain-HTTP callers).
112
+ - **The `If-Match` tag is four parts:** `uid:locale:localeVersion:sharedVersion`; a three-part tag is refused 400. `_metadata.sharedVersion` carries the shared-field counter, so a check from the body is as strong as one from the header. A locale with no version yet reports version 0, and a check at 0 means "this locale must not exist yet". A stale check is 409. `DELETE` is checked too.
113
+ - **A singleton's or a settings group's write:** `update(read, data)` checked, `update(data, { locale, idempotencyKey })` unchecked; a group's `reset(read)` / `reset({ locale, idempotencyKey })`. The positional `update(data, locale, options)` and `reset(locale, options)` are gone. An unchecked write's data carries no `_metadata`: a value that does is a read, and the checked form takes it.
114
+ - **Batches are one call:** `client.batch([{ create }, { update, entry | id }, { replace, entry | id }, { delete, entry | id }, { singleton }], { atomic, dryRun, stopOnError, returnData, dataset })` → `{ data: { results, summary } }`. An entry checks the operation as `update(entry)` does. The chained `client.batch()` builder is gone. A whole-entry replace is written only here, `{ replace, entry }`: a handle offers `update` and `delete`, and there is no single-entry replace route.
115
+ - `create(data, { slug, locale, parentId, sortOrder, dataset, idempotencyKey })` takes `dataset` in place of per-call `headers`.
116
+ - **Customer writes go live in the locale they name.** A write to a locale the entry has never had publishes it, at version 1; on an entry published in no locale, that locale is published while the default stays a draft. An asset's new locale is published once the file it reads is ready. A customer restore of a published locale goes live. A write refused because an editor has a pending Studio draft says so.
117
+ - An asset update honours its `If-Match` and `Idempotency-Key`.
118
+
119
+ ## Search
120
+
121
+ `search(q, { page, limit })` pages by number and answers `page: { number, limit, total, pageCount }`: `total` counts every match, so "page 2 of 3" needs no arithmetic. `offset` is refused. A hybrid search pages within one fused ranking window, the top 500 of each leg fused once, and its `total` is that window's size, so pages agree across numbers. `autocomplete` and `similar` take one options object.
122
+
123
+ ## The Content API (`api.relyio.com`)
124
+
125
+ - `GET /v1/{contentType}` no longer declares `pageSize`, `withCount` or `contentType`: each is a 400 naming it. `limit` is the one page size.
126
+ - A declared singleton nobody has written answers 404 with `details.kind: 'entry'`; an undeclared one stays `kind: 'singleton'`. The singleton route no longer declares `?locales=`.
127
+ - `GET /v1/{contentType}/{id}` answers an entry of that type only; an entry of another type is not found.
128
+ - `GET` and `POST /v1/queries/{name}` run a saved query by name, its parameters checked against the declaration (an undeclared one is refused, naming it). `queries` is a reserved type id.
129
+ - The ad-hoc query (`POST /v1/query`, `client.query({ from, … })`) takes list's option names for what its route serves today: `page` with `limit` (no `cursor`), one `locale`, and `select` or `include` but not both. Cursor paging, locale lists and `select` beside `include` arrive with a later release (REL-2460).
130
+
131
+ ## React (`@relyio/react`)
132
+
133
+ `useEntries`, `useEntry`, `useSingleton`, `useSearch` and `relyioQueryOptions()` take exactly the core's options and return its results; hook names are unchanged. React's own option shapes — `EntriesReadInput`, `EntryReadInput`, `SingletonReadInput`, `SearchReadInput`, a `sort` list, `fields`, `offset` — are deleted. An untyped `useSingleton` answers `BaseSingleton | null`. Reads are keyed by dataset as well as locale. The `@relyio/sdk` peer is `>=0.14.0`.
134
+
135
+ ## Next (`@relyio/next`)
136
+
137
+ `createNextClient` keeps tagging every read under the new options. Tags name the request's dataset (a read's `dataset` or `with({ dataset })`), so a publish in another dataset evicts what was read there; `fresh: true` fetches with `cache: 'no-store'` and no tags. `ReferenceTargetsPort` gained a `dataset` parameter. Reference targets are read with `client.schema(type)`. The `@relyio/sdk` peer is `>=0.14.0`.
138
+
139
+ ## Toolbar (`@relyio/toolbar`)
140
+
141
+ A preview whose content type is not deployed settles `unreadable`, naming why, instead of `not-found`; a missing entry is still `not-found`. The `@relyio/sdk` peer is `>=0.14.0`.
142
+
143
+ ## Agents (`@relyio/agents`)
144
+
145
+ The tools and their manifest are unchanged. Inside them: `searchEntries` and `countEntries` honour `$startsWith`, `$endsWith` (case-sensitive) and `$isNull`; an edit by key's `PATCH` names the locale it read; an entry that is not there is refused as `RESOURCE_NOT_FOUND` `kind: 'entry'` in the tool's own words; `proposeCreateEntries`' batch leaves out the API's defaults (`dryRun`, `continueOnError`). The client type the tools take picks `schema` and carries `with`. The `@relyio/sdk` peer is `>=0.14.0`.
146
+
147
+ ## 0.13.0
148
+
149
+ ### Minor Changes
150
+
151
+ - e678fb8: **`update()` and `restore()` say which entry links they left pointing nowhere.** `update()` returns the entry with `_metadata.brokenLinks`: each link another entry publishes whose anchor names a heading the revision took away, empty when it took none. `restore()` returns `_metadata.brokenLinks` beside `_metadata.leftOut`, both now in its type. A batch revision (`PATCH`, `PUT`) answers the same list under `data._metadata.brokenLinks`, with `returnData` or without, and a request retried with the same `Idempotency-Key` answers the list the first attempt did.
152
+ - c2fe14d: **Filter by a reference inside blocks and rich text.** A typed client's `where()` and `fetchFirst()` accept a reference inside a list, a page builder or an object (`'sections.features'`) and a rich-text field that links to entries or declares a block holding a reference, each compared with an entry id: "pages that feature X". `$eq`/`$in` match an entry where some block holds it, `$ne`/`$nin` one where no block does, and `$isNull` asks whether any block holds a reference at all. Any other value inside a block, and a `json` field, is refused by name.
153
+ - c2fe14d: **Heading anchors are unique per entry.** Headings are numbered across an entry's top-level rich-text fields in the order its type declares them, so `intro`'s "Overview" is `#overview` and `body`'s is `#overview-1`. `entryHeadingAnchors(entry)` computes an entry's anchors once; hand them to `<RichText anchors={…}>`, `renderRichText` or `richTextToHtml` (`anchors` option) and every field writes the ids an entry link's anchor names. A field rendered without `anchors` numbers its own headings, as before. A publish that takes away a heading other entries link to still lands, and the Content API's update answer lists the links it left pointing nowhere in `_metadata.brokenLinks`.
154
+ - 39ea8c6: **A `json` field holds data nobody models.** `fields.json('chart', { config: { schema } })` declares a field whose value is any JSON value — an object, an array, a string, a number, a boolean or `null` — held to an optional JSON Schema on every write, a draft's included: a value the schema refuses is refused (`JSON_INVALID`), never stored. A read is typed by the schema — `type`, `properties`, `required`, `additionalProperties`, `items`, `enum`, `const`, `anyOf` and `oneOf` — and is `JsonValue` without one, never `any`:
155
+
156
+ ```ts
157
+ fields.json('chart', {
158
+ config: {
159
+ schema: {
160
+ type: 'object',
161
+ properties: { points: { type: 'array', items: { type: 'number' } } },
162
+ required: ['points'],
163
+ } as const,
164
+ },
165
+ })
166
+ // a read of `chart`: { points: number[] }
167
+ ```
168
+
169
+ Write the schema `as const` so `required`, `enum` and `const` keep their literals; `relyio generate-types` writes it so, and its Python target writes the same subset inline. A `json` field takes no `defaultValue`, and is neither searched nor filterable. The schema-modelling skill names the kind and when to reach for it.
170
+
171
+ - c2d8071: **A preview can take an editor from any block to that block.** `relyioField` binds a value inside a field by its key path — `relyioField(page, 'sections[_key=="f2k6ub9s"].heading')` — the spelling a refused write already names a value by, and continues a binding it returned: `relyioField(binding, 'heading')` binds a field below it, `relyioField(binding, item)` an item of the list it binds. The attributes are the same five, version 1. `formatKeyPath` writes a key path and `parseKeyPath` reads one back, refusing an index, which names another item once a list is reordered.
172
+
173
+ `<Blocks>` and `<RichText>` take the field's binding as `field` and stamp each block, and each declared block and inline element, with its key path, on the element its component returns:
174
+
175
+ ```tsx
176
+ <Blocks
177
+ value={page.sections ?? []}
178
+ field={relyioField(page, 'sections')}
179
+ components={{
180
+ hero: (block, field) => (
181
+ <header>
182
+ <h1 {...relyioField(field, 'headline')}>{block.headline}</h1>
183
+ <RichText
184
+ value={block.body}
185
+ field={relyioField(field, 'body')}
186
+ components={{ callout: Callout }}
187
+ />
188
+ </header>
189
+ ),
190
+ }}
191
+ />
192
+ ```
193
+
194
+ Each component is handed its block's binding — a `<Blocks>` component as its second argument, a declared component as `field` — so a block inside a block carries its full path, `sections[_key=="f2k6ub9s"].body[_key=="c1"]`. A component that returns a fragment or an element of a component of its own spreads its binding itself. With no `field`, nothing is stamped and the markup is unchanged. The preview toolbar badges each stamped block and links it to its place in the Studio.
195
+
196
+ - e0b742d: **`richTextToHtml` stamps each declared block with its key path, as `<RichText>` does.** Handed the field's binding as `field`, each declared Block and Inline element's writer is handed that node's binding as its second argument, and the one element it returns carries it — `data-relyio-field="body[_key==&quot;c1&quot;]"` after the element's own attributes — so a site rendering HTML without React still lets the preview toolbar take an editor from a block to the Studio:
197
+
198
+ ```ts
199
+ richTextToHtml(post.body, {
200
+ field: relyioField(post, 'body'),
201
+ declared: {
202
+ callout: (block, field) =>
203
+ `<aside>${richTextToHtml(block.body, { field: relyioField(field, 'body') })}</aside>`,
204
+ },
205
+ })
206
+ ```
207
+
208
+ A writer that returns text, several elements or an element it bound itself is written as it returns it; it holds the binding to write where it belongs. With no `field`, nothing is stamped and every writer is handed `undefined`.
209
+
210
+ - 6f0ea50: **Rich text comes in and goes out as RelyIO Markdown: `@relyio/sdk/markdown`.** `richTextFromMarkdown(markdown)` reads a Markdown file into the rich-text tree, every node keyed and ready to write; `richTextToRelyIOMarkdown(value)` writes any rich-text value back as the same Markdown, and importing it again gives the same tree, keys aside. RelyIO Markdown is CommonMark with GitHub's tables, task lists and strikethrough, and directives for what Markdown has no syntax for — declared blocks and inline elements, entry links, code tabs, underline, highlight, sub- and superscript.
211
+
212
+ ```ts
213
+ import { richTextFromMarkdown, richTextToRelyIOMarkdown } from '@relyio/sdk/markdown'
214
+
215
+ const body = richTextFromMarkdown(await readFile('getting-started.md', 'utf8'))
216
+ await relyio.content('docPage').create({ title: 'Getting started', body })
217
+
218
+ richTextToRelyIOMarkdown(page.body) // the same Markdown, ready to edit and import again
219
+ ```
220
+
221
+ Markdown the tree cannot hold — HTML, an image, a footnote, a link's title — throws `RichTextMarkdownImportError`, whose `unplaced` names each one with its line and column; `readRichTextMarkdown` returns the tree and that list instead of throwing. A `table` or `code` field takes one block: `richTextFieldBlockFromMarkdown(markdown, 'table')` or `'codeGroup'`. Export throws `RichTextMarkdownExportError` for a node it cannot name. The parser has its own entry point, so a page that only renders rich text never loads it; `richTextToMarkdown` on the root, which writes plain Markdown and drops what it cannot hold, is unchanged.
222
+
223
+ - 9873ca5: **A typed `where` takes what the API filters by everywhere it is offered, and an expanded singleton is typed as it is served.** `useEntries` and `relyioQueryOptions().entries()` take the `where` `client.content(type).query().where()` takes, so an object's field, a reference inside a block and `$isNull` compile there, and a `json` field or a whole list or object does not. A typed `asset(type)` client's `where()` and `fetchFirst()` are held to the same paths. `WhereOperator` gains `$inTheLast` and `$inTheNext` (`{ amount, unit }`), which a typed read offers on a `date` or `dateTime` field only, as the API asks them of nothing else. `children(id)` no longer takes `depth`: the route lists a parent's direct children, and refused every request that sent one with `400`; a nested tree is a list or entry read's `children()`. A singleton an include expands carries an entry's `_metadata` (`EntryMetadata`), not `SingletonMetadata`, and is typed so — in Python as `Expanded<Name>`, which a reference to the singleton now names.
224
+ - 6c3d471: **A typed `where()` takes exactly what the API filters by.** `where()`, `fetchFirst()` and `fetch({ where })` name a field inside an object by its path — `{ 'pricing.basePrice': { $gte: 100 } }` — and take `$isNull` on any path. A condition the API refuses no longer compiles: a `json` field or a key inside one; a list, an object, a table or code as a whole; rich text that can point at no entry; and a reference or rich text a shared list translates item by item. `$regex`, which the API never read, is gone from `WhereOperator`. `fields.reference`, `fields.richText`, `fields.list` and `fields.object` keep the `localization` they were written with in their type, which is how a typed `where` tells a list translated whole from one translated item by item.
225
+
226
+ ### Patch Changes
227
+
228
+ - 9ccfff0: **Filter by a reference or rich text a shared list translates item by item.** A typed client's `where()` now accepts these paths (`'sections.logo'` where each section's logo is translated): each item is matched in the read's locale by its own translation, or by that item's fallback, as the read shows it.
229
+ - c2fe14d: **A slug may have segments.** `guide/schema/text-fields` is a valid slug; a leading, trailing or doubled slash is refused, naming which. Read an entry by such a slug with the slug encoded in the path (`guide%2Fschema%2Ftext-fields`), as the SDK already does.
230
+
3
231
  ## 0.12.0
4
232
 
5
233
  ### Minor Changes