@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
@@ -0,0 +1,3065 @@
1
+ import { c$ as paths, d0 as MergedRelyIOConfig, cS as ApiResponse, cR as RequestConfig, aM as EntryMetadata, cs as BaseContent, ap as Reading, al as CheckedIncludes, d1 as ReadWhere, cx as WhereClause, aq as ReadWith, cD as UpdateInput, at as UntypedReading, cB as RevisedContent, cA as CreateInput, cC as RestoredContent, cl as SDKError, cY as BaseValidationError, cX as ValidationViolation, b2 as AssetServedBlocks, d2 as CheckedInclude, d3 as components, cq as AssetFileBlock, ct as BaseSingleton, cu as BaseSettingsGroup, aN as InferContentType, an as ContentReading, aT as InferSingleton, cm as RelyIOConfig, b0 as InferAssetType, ak as AssetReading, as as SingletonReading, aY as InferSettingsGroup, ar as SettingsReading, cn as RelyIOConfigUpdate } from './define-content-type-DOrZb_a_.js';
2
+ import { a as SchemaDefinition, b as SchemaContentTypeNames, j as SchemaEntryOf, f as SchemaLocaleCodes, c as SchemaSingletonNames, A as AggregateBlock, K as QueryParameterValue, F as FullTextSearch, e as SchemaQueryNames, J as InferQueryParams, L as AnyQueryParameters, h as SchemaAssetTypeNames, g as SchemaSettingsGroupNames } from './define-schema-5aHmsA8j.js';
3
+ import { VisitorResyncReason } from './live-visitor.js';
4
+ import { DevUnreadableReason } from './dev-handshake/index.js';
5
+
6
+ /**
7
+ * Where the SDK's transport gets its truth.
8
+ *
9
+ * Every route this client calls is named by its **path template as the
10
+ * committed document spells it**, and the body type it expects is read out of
11
+ * that document rather than written down here. `api-schema.ts` beside this file
12
+ * is generated by `pnpm api:types` from `apps/services/api/openapi-spec.json`,
13
+ * which is itself generated from that service's `RouteContract`s
14
+ * (`INV-API-DOCUMENT-IS-DERIVED`). So the chain is: one declaration in
15
+ * `routes.ts` → the serializer that produces the body → the document → these
16
+ * types → the method that reads it.
17
+ *
18
+ * That chain is the answer to the question this phase exists to close. Before
19
+ * it, six SDK methods read a key the server does not send; each had no caller
20
+ * and no test, so nothing could see them. Now:
21
+ *
22
+ * - A route that changes **kind** — `collection` to `object`, say — changes
23
+ * whether the document's 200 body carries `page` and whether `data` is an
24
+ * array. `Wire<…>` follows, `readPayload` follows, and the method's declared
25
+ * return type stops matching. `tsc` names the method.
26
+ * - A route that changes **path** or is deleted stops being a `keyof paths`, so
27
+ * the call site fails on the literal.
28
+ * - A route that changes **dialect** changes `page` from `CursorPage` to
29
+ * `OffsetPage`, and anything reading `nextCursor` fails.
30
+ *
31
+ * `/v1` lives in these templates because it lives in the document
32
+ * (`INV-API-VERSION-IS-DECIDED-ONCE`). Before this phase the SDK's base URL was
33
+ * `https://api.relyio.com` and its paths were `/{contentType}`, so every
34
+ * versioned request went to a path the router does not serve.
35
+ *
36
+ * Spec: `devtools/docs/guide/engineering/api-contract-spec.md`,
37
+ * `INV-API-CLIENT-IS-DERIVED`.
38
+ */
39
+
40
+ /** Every path the committed document declares. */
41
+ type ApiPath = keyof paths;
42
+ type Verb$1 = 'get' | 'post' | 'put' | 'patch' | 'delete';
43
+ /** The verbs the document declares on this path — not the seven HTTP has. */
44
+ type ApiMethod<P extends ApiPath> = {
45
+ [M in Verb$1]: M extends keyof paths[P] ? paths[P][M] extends {
46
+ responses: unknown;
47
+ } ? M : never : never;
48
+ }[Verb$1];
49
+ type Operation<P extends ApiPath, M extends ApiMethod<P>> = M extends keyof paths[P] ? paths[P][M] : never;
50
+ type JsonBody<R> = R extends {
51
+ content: {
52
+ 'application/json': infer B;
53
+ };
54
+ } ? B : never;
55
+ /**
56
+ * The success body the document declares, preferring 200 where a route serves
57
+ * both. A route with neither — `kind: 'empty'`, which serves 204 — resolves to
58
+ * `never`, and `readPayload` cannot be called on it.
59
+ */
60
+ type ApiSuccessBody<P extends ApiPath, M extends ApiMethod<P>> = Operation<P, M> extends {
61
+ responses: infer R;
62
+ } ? 200 extends keyof R ? JsonBody<R[200]> : 201 extends keyof R ? JsonBody<R[201]> : never : never;
63
+ /**
64
+ * The document's success body with the payload typed as the caller's `T`.
65
+ *
66
+ * The array arm is tested **first** on purpose: `{ data: unknown[] }` also
67
+ * satisfies `{ data: unknown }`, so the object arm would swallow every
68
+ * collection and hand a list route's caller a single entity — which is the
69
+ * exact defect class (`ContentClient.children` reading `.data.items` from an
70
+ * object route) that this phase makes unrepresentable.
71
+ */
72
+ type Wire<P extends ApiPath, M extends ApiMethod<P>, T> = ApiSuccessBody<P, M> extends {
73
+ data: unknown[];
74
+ } ? Omit<ApiSuccessBody<P, M>, 'data'> & {
75
+ data: T[];
76
+ } : ApiSuccessBody<P, M> extends {
77
+ data: unknown;
78
+ } ? Omit<ApiSuccessBody<P, M>, 'data'> & {
79
+ data: T;
80
+ } : ApiSuccessBody<P, M>;
81
+ /**
82
+ * The path parameters the document declares for this operation, or `never`
83
+ * where it declares none.
84
+ *
85
+ * `never` rather than `{}`: a route with no segments must **forbid** a `path`
86
+ * argument, not merely tolerate an empty one, and `keyof Record<string, never>`
87
+ * is `string`, so an empty-object encoding cannot be told apart from a real
88
+ * one at the type level.
89
+ */
90
+ type PathParamsOf<P extends ApiPath, M extends ApiMethod<P>> = Operation<P, M> extends {
91
+ parameters: {
92
+ path?: infer X;
93
+ };
94
+ } ? [X] extends [undefined] ? never : NonNullable<X> : never;
95
+ /**
96
+ * The query parameters the document declares for this operation, or `never` where it declares
97
+ * none (REL-687).
98
+ *
99
+ * The service declares them beside each route — a query DTO's whitelisted properties, the names a
100
+ * handler reads, `?_fresh` on a read the edge keeps — and `apps/services/api`'s
101
+ * `contract/query-parameters.spec.ts` holds the document to what each handler reads. So a name the
102
+ * API stops reading stops compiling here, and a name it refuses — `offset` on the entry list, which
103
+ * the SDK once sent on every `.offset()` and the API answered with a 400 — never compiled at all.
104
+ *
105
+ * `never` for the same reason {@link PathParamsOf} uses it: a route with no query must forbid one.
106
+ */
107
+ type QueryOf<P extends ApiPath, M extends ApiMethod<P>> = Operation<P, M> extends {
108
+ parameters: {
109
+ query?: infer Q;
110
+ };
111
+ } ? [Q] extends [undefined] ? never : NonNullable<Q> : never;
112
+ /** What {@link matchRoute} recovers from a URL the client emitted. */
113
+ interface RouteMatch {
114
+ /** The document's path template — the value a tag translator switches on. */
115
+ path: ApiPath;
116
+ /** The template's `{name}` segments, decoded: the inverse of {@link fillPath}. */
117
+ params: Record<string, string>;
118
+ /** The query string; a key that appears more than once comes back as an array. */
119
+ query: Record<string, string | string[]>;
120
+ }
121
+ interface MatchRouteOptions {
122
+ /**
123
+ * The client's `baseUrl`, when it carries a path prefix of its own
124
+ * (`https://example.com/relyio`). Its path is removed before the template is
125
+ * matched. Unneeded for the default `https://api.relyio.com`.
126
+ */
127
+ baseUrl?: string;
128
+ }
129
+ /**
130
+ * Recover the route a URL names: the inverse of {@link fillPath}, over the same
131
+ * committed document (`INV-API-CLIENT-IS-DERIVED`).
132
+ *
133
+ * `matchRoute(url).path` is an `ApiPath`, so anything that keys behaviour on a
134
+ * request — a cache-tag translator, say — switches on the document's templates
135
+ * and never on a route table of its own.
136
+ *
137
+ * Returns `null` for a URL no declared template matches — a presigned storage
138
+ * URL, or a path outside `baseUrl` — rather than a guess.
139
+ */
140
+ declare function matchRoute(url: string | URL, options?: MatchRouteOptions): RouteMatch | null;
141
+
142
+ /**
143
+ * Dev-mode detection for `relyio dev` sandboxes — the SDK's I/O shell over `../dev-handshake`.
144
+ *
145
+ * Every rule about the handshake — the path, the env names, the required fields, the deadline —
146
+ * lives in `@relyio/sdk/dev-handshake`, which `relyio dev` imports too. What is left here is the
147
+ * part that is genuinely the SDK's: reading env and walking the filesystem from the cwd, both of
148
+ * which have to happen without a static `node:*` import so the module stays browser-safe.
149
+ *
150
+ * Node access goes through `process.getBuiltinModule` (Node ≥ 20.16 / 22, and the platform targets
151
+ * Node 24), which is absent in browsers and edge runtimes — so the resolver is inert everywhere it
152
+ * should be, and a bundler never has to shim `fs`.
153
+ *
154
+ * **Order: env, then file.** The wrapped-child env is the primary carrier and wins. Before REL-647
155
+ * it won *unconditionally*, because it had no deadline to lose on; it now goes through the same
156
+ * rule the file does.
157
+ */
158
+
159
+ /**
160
+ * What the handshake says about the sandbox, as the preview toolbar displays it
161
+ * (`sdk-surfaces-spec.md` decision 22). `TypedRelyIOClient.sandbox` returns it.
162
+ *
163
+ * **Plain data, on purpose.** In Next the toolbar is rendered from a server layout, and a client
164
+ * cannot cross the RSC boundary — while the browser cannot read the handshake at all, because this
165
+ * module is inert without `process.getBuiltinModule`. So the server-created client's `sandbox` is the
166
+ * prop the toolbar is handed, and it has to survive `JSON` unchanged: strings and `null`, dates as
167
+ * the ISO strings the handshake carried, no class instances.
168
+ *
169
+ * **No key.** The handshake also carries the sandbox's preview key; that is the client's credential,
170
+ * held beside this value in {@link DevResolution} and never inside it, so handing the state around
171
+ * does not hand the key with it.
172
+ *
173
+ * - `active` — the client targets the sandbox. `branchId` is `null` from the env carrier, which names
174
+ * no branch; only `.relyio/dev.json` does. `expiresAt` is `null` for an unbounded sandbox.
175
+ * - `expired` / `unreadable` — a handshake was found and refused; the client targets the configured
176
+ * dataset and key, and `mergeConfig` has said so on the console.
177
+ * - `absent` — no handshake on either carrier. This is also what a browser or edge runtime reads,
178
+ * since neither can see the env or the file.
179
+ * - `disabled` — detection did not run: `dev: false` was passed, or `NODE_ENV === 'production'`
180
+ * and `dev: true` was not.
181
+ */
182
+ type SandboxState = {
183
+ state: 'active';
184
+ datasetId: string;
185
+ branchId: string | null;
186
+ expiresAt: string | null;
187
+ source: 'env' | 'file';
188
+ } | {
189
+ state: 'expired';
190
+ expiredAt: string;
191
+ } | {
192
+ state: 'unreadable';
193
+ reason: DevUnreadableReason;
194
+ } | {
195
+ state: 'absent';
196
+ } | {
197
+ state: 'disabled';
198
+ };
199
+ /** The sandbox the client targets, once a carrier has been accepted. */
200
+ type ActiveSandbox = Extract<SandboxState, {
201
+ state: 'active';
202
+ }>;
203
+
204
+ /**
205
+ * What a call site supplies beside the route it named.
206
+ *
207
+ * `path` is required exactly when the document's template has segments, and
208
+ * forbidden when it does not — so a call that forgot to scope a request, or one
209
+ * that scoped a request the route does not scope, is a compile error rather
210
+ * than a URL with a literal `{id}` in it.
211
+ */
212
+ type SendOptions<P extends ApiPath, M extends ApiMethod<P>> = {
213
+ /**
214
+ * Query string values: exactly the names the document declares for this route, at the types it
215
+ * declares them (`QueryOf`, REL-687). An undeclared name is a compile error, not a 400.
216
+ */
217
+ query?: QueryOf<P, M>;
218
+ body?: unknown;
219
+ /** Request headers this call adds, e.g. `If-Match` / `Idempotency-Key` (API4). */
220
+ headers?: Record<string, string>;
221
+ signal?: AbortSignal;
222
+ timeout?: number;
223
+ } & ([PathParamsOf<P, M>] extends [never] ? {
224
+ path?: never;
225
+ } : {
226
+ path: PathParamsOf<P, M>;
227
+ });
228
+ /**
229
+ * Core HTTP client for the RelyIO SDK.
230
+ *
231
+ * `send` is **the** typed entry: it names a route by the path template the
232
+ * committed document declares, and returns that operation's body type with the
233
+ * payload typed as `T`. `readPayload` from `@relyio/api-contract/client` is
234
+ * then the one unwrap — there is no second spelling and no runtime test of
235
+ * which envelope arrived (`INV-API-CLIENT-IS-DERIVED`).
236
+ *
237
+ * The verb helpers below stay for the escape hatch `getHttpClient()` exposes;
238
+ * nothing inside the SDK uses them.
239
+ */
240
+ declare class HttpClient {
241
+ /** What every request reads off the configuration, derived by {@link configure} and nowhere else. */
242
+ private connection;
243
+ /**
244
+ * Set on a view {@link withHeaders} made: the client every request is sent through, and the
245
+ * headers laid beneath each request's own. A view keeps no connection of its own that matters —
246
+ * everything it is asked to change, it changes on that client.
247
+ */
248
+ private view?;
249
+ constructor(config: MergedRelyIOConfig);
250
+ /**
251
+ * A view of this client that sends every request through it with `headers` added — beneath the
252
+ * request's own, over this client's — and changes nothing else: its key, base URL, timeout,
253
+ * retries and `fetch` are this client's as they are when each request is sent, so a later
254
+ * `configure()` or `setApiKey()` on this client reaches the view, and one on the view reaches
255
+ * this client. `client.with({ dataset })` is one (REL-2417).
256
+ */
257
+ withHeaders(headers: Readonly<Record<string, string>>): HttpClient;
258
+ /**
259
+ * Run on `config` from now on: every later request — whoever holds this client — is sent to its
260
+ * `baseUrl`, with its headers, key and dataset, its timeout and retries, through its `fetch`, and
261
+ * logged by its `debug` options.
262
+ *
263
+ * The client's own `updateConfig()` calls this rather than building a second client, so a
264
+ * `content()` or other sub-client taken before the update sends through the update too. Anything
265
+ * set on this client since it was last configured — `setDefaultHeaders()`, `setBaseUrl()` — is
266
+ * replaced by what `config` says.
267
+ */
268
+ configure(config: MergedRelyIOConfig): void;
269
+ private announce;
270
+ /**
271
+ * Call a route the committed document declares, and get back the body that
272
+ * document says it serves.
273
+ */
274
+ send<P extends ApiPath, M extends ApiMethod<P>, T = unknown>(path: P, method: M, options: SendOptions<P, M>): Promise<ApiResponse<Wire<P, M, T>>>;
275
+ /**
276
+ * Call a route and return its payload — `send` plus the one unwrap, which is
277
+ * what all but a handful of call sites want.
278
+ */
279
+ fetchPayload<P extends ApiPath, M extends ApiMethod<P>, T = unknown>(path: P, method: M, options: SendOptions<P, M>): Promise<Wire<P, M, T> extends {
280
+ data: unknown;
281
+ } ? Wire<P, M, T>['data'] : never>;
282
+ /**
283
+ * Make an HTTP request, retrying only where a retry is safe.
284
+ *
285
+ * The headers are merged **once**, before the first attempt, and every attempt
286
+ * sends that same object. That is what makes a minted `Idempotency-Key` stable
287
+ * across the retries of one logical write: the service sees one key and
288
+ * answers the replay from the original outcome instead of applying it twice.
289
+ *
290
+ * The loop always makes one attempt. The incumbent's `while (attempt <
291
+ * maxAttempts)` made none at `retryAttempts: 0` — a value the config validator
292
+ * then accepted, and now refuses — and then dereferenced an unassigned
293
+ * `lastError`. `retryAttempts` is the most attempts, the first included; an
294
+ * `HttpClient` constructed directly is not validated, so the first attempt
295
+ * stays unconditional.
296
+ *
297
+ * A request that would carry no `X-API-Key` makes none: it rejects with
298
+ * `MissingApiKeyError` before `fetch` is called, is not retried, and names
299
+ * `apiKey`. This is where a client built without a key learns it — the
300
+ * constructor accepts one so that a module-scope client can be evaluated by a
301
+ * build that holds no key (REL-1120) — and so does one whose `setApiKey` was
302
+ * given none.
303
+ */
304
+ request<T = unknown>(config: RequestConfig): Promise<ApiResponse<T>>;
305
+ /**
306
+ * Make a GET request
307
+ */
308
+ get<T = unknown>(url: string, params?: Record<string, unknown>): Promise<ApiResponse<T>>;
309
+ /**
310
+ * Make a POST request
311
+ */
312
+ post<T = unknown>(url: string, data?: unknown, params?: Record<string, unknown>): Promise<ApiResponse<T>>;
313
+ /**
314
+ * Make a PUT request
315
+ */
316
+ put<T = unknown>(url: string, data?: unknown, params?: Record<string, unknown>): Promise<ApiResponse<T>>;
317
+ /**
318
+ * Make a DELETE request
319
+ */
320
+ delete<T = unknown>(url: string, params?: Record<string, unknown>): Promise<ApiResponse<T>>;
321
+ /**
322
+ * Make a PATCH request
323
+ */
324
+ patch<T = unknown>(url: string, data?: unknown, params?: Record<string, unknown>): Promise<ApiResponse<T>>;
325
+ /**
326
+ * Add headers to every later request, each replacing any header of the same name — the name
327
+ * matched without regard to case — until the client is next configured.
328
+ */
329
+ setDefaultHeaders(headers: Record<string, string>): void;
330
+ /**
331
+ * Replace the key every later request sends as `X-API-Key`, and the `apiKey` the configuration
332
+ * reports.
333
+ *
334
+ * The key is read the way construction reads it (`presentKey`): an absent, empty or blank key is
335
+ * no key, so the header is dropped and the next request is refused with `MissingApiKeyError`
336
+ * before anything is sent. A key header of any other spelling — one given in `headers` — goes
337
+ * too, from the headers sent and from the headers the configuration reports, so the old key is
338
+ * never sent beside the new one.
339
+ *
340
+ * The configuration is the one the owning `TypedRelyIOClient` holds (it hands its own to the
341
+ * constructor), so its `getConfig().apiKey` — what a surface reads to decide a key's kind — is the
342
+ * key sent from here on.
343
+ *
344
+ * This replaced `setAuthToken`, which added `Authorization: Bearer` beside an `X-API-Key` it
345
+ * never touched: the API reads `X-API-Key` first, so the old key was the one used (REL-1385).
346
+ */
347
+ setApiKey(apiKey: string): void;
348
+ /**
349
+ * Get current default headers
350
+ */
351
+ getDefaultHeaders(): Record<string, string>;
352
+ /**
353
+ * Update base URL
354
+ */
355
+ setBaseUrl(baseUrl: string): void;
356
+ /**
357
+ * Get current base URL
358
+ */
359
+ getBaseUrl(): string;
360
+ /**
361
+ * One attempt.
362
+ *
363
+ * The caller's `signal` and the timeout both end it, through one controller.
364
+ * The incumbent handed `fetch` the caller's signal **instead of** its own, so a
365
+ * request that passed a signal had no timeout at all — and a caller abort then
366
+ * surfaced as `Request timed out`. `timedOut` is what tells the two apart: a
367
+ * timeout is a `TimeoutError`, a caller abort is the platform's `AbortError`,
368
+ * rethrown as it came.
369
+ */
370
+ private makeRequest;
371
+ /**
372
+ * The headers without those a browser controls itself, when this is a browser; unchanged when it
373
+ * is not.
374
+ *
375
+ * A name is matched without regard to case (RFC 9110 §5.1), because a browser refuses `Origin`
376
+ * whatever case a page spells it in. This matched only the all-lowercase and all-uppercase
377
+ * spellings, so `Origin` and `User-Agent` — how people write them — reached `fetch` (REL-1409).
378
+ */
379
+ private filterHeadersForBrowser;
380
+ /**
381
+ * Prepare request body based on content type and environment
382
+ */
383
+ private prepareRequestBody;
384
+ private parseResponse;
385
+ private resolveUrl;
386
+ private mergeRequestConfig;
387
+ private buildQueryString;
388
+ private headersToObject;
389
+ private log;
390
+ }
391
+
392
+ /**
393
+ * API4's two request headers, as the SDK's writes send them.
394
+ *
395
+ * `Idempotency-Key` is an option on every write whose route declares it (`idempotencyKey`), and the
396
+ * SDK mints one when none is given. `If-Match` is never an option: a checked write builds it from
397
+ * the entry it was read as (`versionCheckOf`), from the `_metadata` every read carries in its body
398
+ * whether or not the edge cache served it (REL-2418). The SDK does not read the separate
399
+ * `X-Entity-Tag` header, which the edge removes from what it stores; the API keeps sending it for
400
+ * callers using plain HTTP (REL-2427). A batch operation carries its check in its own body, as
401
+ * `ifMatch` (`batch/batch-operations.ts`, REL-2421).
402
+ */
403
+ interface RetryableWriteOptions {
404
+ /**
405
+ * A key that makes a retry of **this** write safe.
406
+ *
407
+ * A replay inside the service's window returns the original outcome rather
408
+ * than applying twice. The service derives that response from committed state
409
+ * pinned to the recorded version — it does not store bytes — so a replay after
410
+ * the version has been pruned is refused `RESOURCE_EXPIRED` rather than
411
+ * answered from a stale copy.
412
+ *
413
+ * **The SDK mints one when you do not.** It used to be caller-supplied and
414
+ * nothing else, which meant the default write carried no key while the retry
415
+ * loop replayed it anyway — the one mechanism that makes a replay safe was
416
+ * optional on the one path that needed it. Supply your own to make a key
417
+ * outlive the process: a job that may be restarted wants a key derived from
418
+ * the work, not one minted per call.
419
+ */
420
+ idempotencyKey?: string;
421
+ }
422
+ /**
423
+ * An entry a read handed back, as much of it as a version check needs: its `_metadata`'s id, the
424
+ * locale it was read in and the two counters it was read at (REL-2418).
425
+ *
426
+ * Every read carries these in its body, whether origin or the edge cache served it — the edge
427
+ * removes only the separate `X-Entity-Tag` header from what it stores, so a check taken from that
428
+ * header vanished after a cached read. A `Pick` of the document's `EntryMetadata`, so a counter the
429
+ * API stops reporting stops compiling here.
430
+ */
431
+ interface ReadEntry {
432
+ readonly _metadata: Pick<EntryMetadata, 'id' | 'locale' | 'version' | 'sharedVersion'>;
433
+ }
434
+ /**
435
+ * The version check a write takes from the entry it was read as: the entry's id, the locale it was
436
+ * read in — which the write addresses, so a read negotiated from `Accept-Language` is written back
437
+ * to that locale — and the `If-Match` tag over its two counters, spelled as the API mints it.
438
+ */
439
+ declare function versionCheckOf(entry: ReadEntry): {
440
+ id: string;
441
+ locale: string;
442
+ ifMatch: string;
443
+ };
444
+
445
+ /**
446
+ * The client's locale as a sub-client holds it: a function it asks on every call, or a locale fixed
447
+ * when it was built.
448
+ *
449
+ * `TypedRelyIOClient` hands every sub-client and builder a function over the configuration it holds,
450
+ * so one taken before `updateConfig({ locale })` sends the locale the client holds when the call is
451
+ * made, and none once `updateConfig({ locale: null })` has cleared it (REL-1453). A string is a locale
452
+ * fixed at construction — a handle built directly, as the tests build one — and stays what it was.
453
+ */
454
+ type ClientLocale = string | (() => string | undefined);
455
+
456
+ /**
457
+ * Two read terms every read that takes them writes the same way: the children an entry embeds,
458
+ * and the refusal of a condition on `_metadata`. Here once, for the list and entry reads
459
+ * (`read-options.ts`), the search family and the query call alike.
460
+ */
461
+
462
+ /**
463
+ * Children population options: how deep, and how many per parent.
464
+ *
465
+ * The API expands an entry's children of **every** content type (`?children=true|1|2|3`); there is
466
+ * no filter by type. The option that used to name types was sent as `?children=<type>,<type>`,
467
+ * which the API parsed as no expansion at all, and `depth` was never sent (REL-1118).
468
+ */
469
+ interface ChildrenOptions {
470
+ /** Levels of children to embed: 1 (the default) to 3 */
471
+ depth?: 1 | 2 | 3;
472
+ /** Maximum children per parent: 1 to 50, default 25 */
473
+ limit?: number;
474
+ }
475
+
476
+ /**
477
+ * The options a content type's reads take, and the one place each becomes the query its route
478
+ * declares (REL-2415, REL-2417; REL-2409 decisions Q3, Q5, Q6, Q9, Q10, Q11, Q12, Q14, Q15 and Q16).
479
+ *
480
+ * Every read takes **one options object**, with one name per concern. Each option that becomes a
481
+ * query term is typed from the query the committed document declares for its route (`QueryOf`,
482
+ * REL-687), and {@link listQuery} / {@link entryQuery} return that query type — so an option whose
483
+ * term the API stops reading stops compiling here, and a term the API does not read (`offset`,
484
+ * refused with a 400 on every list) cannot be offered. Two options are headers, which no route
485
+ * declares: `dataset` (`X-RelyIO-Dataset`) and `fresh: true` (`Cache-Control: no-cache`), written
486
+ * by {@link readHeaders}.
487
+ *
488
+ * `pageSize` is not offered: `limit` is the page size of a cursor read and of a page-number read
489
+ * alike (`listPageLimit` in `@relyio/content-db` reads `limit` first).
490
+ *
491
+ * `select` and `sort` are a list's alone: `GET /v1/{contentType}/{id}` declares neither `fields`
492
+ * nor `sort`, so `get` offers neither.
493
+ */
494
+
495
+ /** One declared term's value, as the document types it. */
496
+ type Term<P extends ApiPath, M extends ApiMethod<P>, K extends keyof QueryOf<P, M>> = QueryOf<P, M>[K];
497
+ /**
498
+ * What `list` answers: the API's own `{ data, page }`, the page in the cursor dialect —
499
+ * `{ nextCursor, hasMore }` and no total, which the dialect cannot carry (Q10).
500
+ */
501
+ type EntryList<E> = Wire<'/v1/{contentType}', 'get', E>;
502
+ /**
503
+ * The Fields an entry of `T` declares, by key: what `select` picks and `sort` orders by. Any name
504
+ * where `T` declares none — a client no schema types — since the API alone knows which exist.
505
+ */
506
+ type FieldKey<T> = [Exclude<keyof T, '_metadata'>] extends [never] ? string : Exclude<keyof T, '_metadata'> & string;
507
+ /**
508
+ * The entry's own dates a list can be ordered by beside its Fields: the system keys the list route
509
+ * documents as indexed (`GetEntriesDto.sort`), which `@relyio/content-db` reads at the top level of
510
+ * the entry rather than under its fields.
511
+ */
512
+ type SystemSortKey = 'createdAt' | 'updatedAt' | 'publishedAt' | 'firstPublishedAt';
513
+ /**
514
+ * How a list is ordered: a direction per key, the **key order** the priority —
515
+ * `{ featured: 'desc', publishedAt: 'desc' }` orders by `featured` and breaks its ties by
516
+ * `publishedAt` (Q14). Each key is a Field `T` declares or a {@link SystemSortKey}.
517
+ */
518
+ type SortOption<T> = {
519
+ readonly [K in FieldKey<T> | SystemSortKey]?: 'asc' | 'desc';
520
+ };
521
+ /**
522
+ * The locale codes a read of `R` may name: the codes the schema declares, or any where no schema
523
+ * types the read.
524
+ */
525
+ type ReadLocale<R extends Reading> = R extends {
526
+ readonly locale: infer C extends string;
527
+ } ? C : string;
528
+ /**
529
+ * What `locale` takes (Q16): one code; a list of codes, the first answered at the top level and
530
+ * the others under `_metadata.localizations`; or `'*'`, every locale the entry has. Each code is
531
+ * one the schema declares.
532
+ */
533
+ type LocaleOption<C extends string> = C | '*' | readonly [C, ...C[]];
534
+ /**
535
+ * How fresh an answer must be:
536
+ *
537
+ * - a feed position (`'<cursor>'`, a change's `fresh`) — no answer older than that change, which the
538
+ * edge still caches under that position (the sdk-surfaces spec's meaning). Live refetches use it;
539
+ * - `true` — past every cache, to origin, with `Cache-Control: no-cache`. For write flows, a re-read
540
+ * after a conflict; never for live refetches, which would send every viewer to origin at once.
541
+ */
542
+ type FreshOption = true | NonNullable<Term<'/v1/{contentType}', 'get', '_fresh'>>;
543
+ /**
544
+ * The options every entry read takes, beside how a list pages and what it keeps.
545
+ *
546
+ * - `include` — the references to expand, each path checked against the type (REL-1801); the read's
547
+ * type follows the paths.
548
+ * - `locale` — {@link LocaleOption}; replaces the client's locale for this read.
549
+ * - `dataset` — the dataset this one read is of, by id or name; the client's otherwise.
550
+ * - `fresh` — {@link FreshOption}.
551
+ * - `children` — each entry's children, of every content type, under `_children`: `true` for one
552
+ * level, or a depth of 1 to 3 and at most `limit` per parent (REL-1118).
553
+ */
554
+ interface EntryReadOptions<R extends Reading, P extends readonly string[], L extends LocaleOption<ReadLocale<R>> | undefined = undefined> {
555
+ /** Include paths: the references to expand into the entries they point at (REL-1801). */
556
+ readonly include?: CheckedIncludes<R, P>;
557
+ /** The locale, locales or `'*'` to read, over the client's locale (Q16). */
558
+ readonly locale?: L;
559
+ /** The dataset to read, by id or name, over the client's (Q9). */
560
+ readonly dataset?: string;
561
+ /** How fresh the answer must be: a feed position, or `true` for past every cache. */
562
+ readonly fresh?: FreshOption;
563
+ /** Embed each entry's children: `true`, or a depth and a per-parent limit (REL-1118). */
564
+ readonly children?: true | ChildrenOptions;
565
+ }
566
+ /** The options a list read takes beside how it pages: what it keeps, how, and how many a page holds. */
567
+ interface ListReadOptions<T extends BaseContent, R extends Reading, P extends readonly string[], S extends readonly FieldKey<T>[] | undefined = undefined, L extends LocaleOption<ReadLocale<R>> | undefined = undefined> extends EntryReadOptions<R, P, L> {
568
+ /**
569
+ * The conditions an entry must meet, by the paths the API filters on (REL-2136). A condition on
570
+ * `_metadata` is refused before anything is sent.
571
+ */
572
+ readonly where?: ReadWhere<WhereClause<T>, R>;
573
+ /** How many entries a page holds, whether it is read by cursor or by number. */
574
+ readonly limit?: Term<'/v1/{contentType}', 'get', 'limit'>;
575
+ /**
576
+ * The entry's own Fields to return, beside `_metadata` and every Field an include path starts
577
+ * at (Q15). An expanded reference's Fields are picked in its include path:
578
+ * `include: ['author(name)']`.
579
+ */
580
+ readonly select?: S;
581
+ /** The order, by Field or system date, the key order the priority (Q14). */
582
+ readonly sort?: SortOption<T>;
583
+ }
584
+ /**
585
+ * Which page: the cursor a page handed back (`page.nextCursor`), or a page number (1-based) for an
586
+ * archive page like `/blog/page/3` — one or the other, never both (Q6).
587
+ */
588
+ type Paging = {
589
+ readonly cursor?: Term<'/v1/{contentType}', 'get', 'cursor'>;
590
+ readonly page?: never;
591
+ } | {
592
+ readonly page?: Term<'/v1/{contentType}', 'get', 'page'>;
593
+ readonly cursor?: never;
594
+ };
595
+ /** What `list(options)` takes. */
596
+ type ListOptions<T extends BaseContent, R extends Reading, P extends readonly string[], S extends readonly FieldKey<T>[] | undefined = undefined, L extends LocaleOption<ReadLocale<R>> | undefined = undefined> = ListReadOptions<T, R, P, S, L> & Paging;
597
+ /** What `listAll(options)` takes: a list's options, paged by the walk itself. */
598
+ type ListAllOptions<T extends BaseContent, R extends Reading, P extends readonly string[], S extends readonly FieldKey<T>[] | undefined = undefined, L extends LocaleOption<ReadLocale<R>> | undefined = undefined> = ListReadOptions<T, R, P, S, L>;
599
+ /** What `get(idOrSlug, options)` takes. */
600
+ type GetOptions<R extends Reading, P extends readonly string[], L extends LocaleOption<ReadLocale<R>> | undefined = undefined> = EntryReadOptions<R, P, L>;
601
+ /**
602
+ * What a one-document read takes — `singleton(key).get(options)` and `settings(group).get(options)`
603
+ * (REL-2419, REL-2409 decision Q18): a content read's options where their routes declare them.
604
+ *
605
+ * - `include`, `dataset` and `fresh` are a content read's.
606
+ * - `locale` is **one** declared code. Neither route declares `locales`: a singleton's or a group's
607
+ * read composes one locale's fields and no `_metadata.localizations`, so a list or `'*'` would be
608
+ * typed as an answer neither gives.
609
+ * - No `select`, `sort` or paging: neither route declares `fields`, `sort` or a page; no `children`:
610
+ * neither declares it.
611
+ */
612
+ interface DocumentReadOptions<R extends Reading, P extends readonly string[]> {
613
+ /** Include paths: the references to expand into the entries they point at (REL-1801). */
614
+ readonly include?: CheckedIncludes<R, P>;
615
+ /** The locale to read, over the client's locale. */
616
+ readonly locale?: ReadLocale<R>;
617
+ /** The dataset to read, by id or name, over the client's (Q9). */
618
+ readonly dataset?: string;
619
+ /** How fresh the answer must be: a feed position, or `true` for past every cache. */
620
+ readonly fresh?: FreshOption;
621
+ }
622
+ /**
623
+ * What `version()` takes on every handle (REL-2427): the version to read, and the locale whose
624
+ * timeline it is numbered in — versions are numbered per locale, so version 2 is different content
625
+ * in each one. `locale` defaults to the client's.
626
+ */
627
+ interface VersionOptions {
628
+ readonly version: number;
629
+ readonly locale?: string;
630
+ }
631
+ /** The Field an include path starts at: `sections` for `sections.features(name)`. */
632
+ type IncludeRoot<X extends string> = X extends `${infer Head}.${string}` ? PathName<Head> : PathName<X>;
633
+ type PathName<X extends string> = X extends `${infer Name}(${string}` ? Name : X;
634
+ /**
635
+ * `E` with only the Fields `select` named, `_metadata`, and each Field an include path starts at —
636
+ * which the API keeps in the projection, since an expanded reference needs its own Field. `E` when
637
+ * nothing is selected.
638
+ */
639
+ type SelectedFields<E, S, I extends string> = S extends readonly [] | undefined ? E : S extends readonly string[] ? Pick<E, Extract<keyof E, S[number] | IncludeRoot<I> | '_metadata'>> : E;
640
+ /**
641
+ * The fields of the other locales a list of codes or `'*'` asked for, keyed by code: a locale with
642
+ * no visible fields is left out, so each is optional.
643
+ */
644
+ type Localizations<E, C extends string> = {
645
+ readonly [K in C]?: Partial<Omit<E, '_metadata'>>;
646
+ };
647
+ /**
648
+ * `E` as a read in `L` returns it. A list of codes or `'*'` adds `_metadata.localizations`, keyed by
649
+ * the codes it can hold; one code, or none, leaves `E` as it is.
650
+ */
651
+ type Localized<E, L, C extends string> = L extends '*' | readonly string[] ? E extends {
652
+ readonly _metadata: infer M;
653
+ } ? Omit<E, '_metadata'> & {
654
+ _metadata: Omit<M, 'localizations'> & {
655
+ localizations: Localizations<E, L extends readonly string[] ? L[number] : C>;
656
+ };
657
+ } : E : E;
658
+ /** What a read of `R` returns under its include paths `P`, selection `S` and locale `L`. */
659
+ type ReadAs<R extends Reading, P extends readonly string[], S = undefined, L = undefined> = Localized<SelectedFields<ReadWith<R, P[number]>, S, P[number]>, L, ReadLocale<R>>;
660
+ /**
661
+ * Where a read reads and how fresh it must be, as an implementation reads them whatever the type
662
+ * parameters checked: the clump every read takes — an entry read, a list, a one-document read, a
663
+ * search, an ad-hoc query and React's keyed reads alike. One declaration, because each surface
664
+ * restating it had already drifted (`fresh` was `true | string` in one, `FreshOption` in another).
665
+ * A read that takes one code (a search, a one-document read) narrows `locale` to `string`.
666
+ */
667
+ interface WrittenReadScope {
668
+ readonly locale?: string | readonly string[];
669
+ readonly dataset?: string;
670
+ readonly fresh?: FreshOption;
671
+ }
672
+ /**
673
+ * A `sort` as an implementation reads it, whatever keys the type checked ({@link SortOption}): a
674
+ * direction per key, the key order the priority.
675
+ */
676
+ type WrittenSort = Readonly<Record<string, 'asc' | 'desc' | undefined>>;
677
+
678
+ /**
679
+ * The verbs every handle over the entry routes (`/v1/{type}`, `/v1/{type}/{id}`) shares — a content
680
+ * type's and an asset type's alike (REL-2419, REL-2409 decision Q18): `list`, `listAll`, `get`,
681
+ * `update` and `delete`, written once. An asset is an entry on the same routes, so a second copy of
682
+ * these per handle is what drifted before: the asset handle had none of the options, the not-found
683
+ * split or the check from the entry.
684
+ *
685
+ * What differs per handle is in the type parameters, not in a copy:
686
+ *
687
+ * - `Revised` — what an update answers;
688
+ * - `Placement` — what a write may place beside its data, as its route reads it: a content entry's
689
+ * `slug`, `parentId` and `sortOrder`; an asset's `slug` alone, since the asset branch of
690
+ * `PATCH /{type}/{id}` reads no other (`EntriesController.updateEntry`).
691
+ */
692
+ declare abstract class EntryHandle<T extends BaseContent, R extends Reading, Revised, Placement extends object> {
693
+ protected readonly httpClient: HttpClient;
694
+ protected readonly typeName: string;
695
+ /** The client's locale, asked on every call rather than copied when this handle was taken. */
696
+ protected readonly clientLocale: () => string | undefined;
697
+ /**
698
+ * @param typeName - The content or asset type the routes are addressed by
699
+ * @param locale - The client's locale: the function `TypedRelyIOClient` hands over, asked on
700
+ * every call
701
+ */
702
+ constructor(httpClient: HttpClient, typeName: string, locale?: ClientLocale);
703
+ /**
704
+ * One page of this type's entries: the API's own `{ data, page }`, as it answered (REL-2415).
705
+ *
706
+ * Page by `cursor` — the `page.nextCursor` a page handed back — to walk, or by `page` number for an
707
+ * archive page like `/blog/page/3`; `limit` is the page size for both. The page is the cursor
708
+ * dialect's `{ nextCursor, hasMore }`: a list carries no total.
709
+ *
710
+ * The rest of the options have one name per concern (REL-2417): `select` the entry's own Fields,
711
+ * `sort` an order whose key order is the priority, `locale` a code, a list of codes or `'*'`,
712
+ * `dataset` this read's dataset, `fresh` how fresh the answer must be, `children` the children to
713
+ * embed. See `ListReadOptions`.
714
+ *
715
+ * @example
716
+ * ```typescript
717
+ * const { data, page } = await relyio.content('post').list({
718
+ * where: { category: 'news' },
719
+ * include: ['author'],
720
+ * select: ['title', 'slug'],
721
+ * sort: { featured: 'desc', publishedAt: 'desc' },
722
+ * limit: 10,
723
+ * page: 3,
724
+ * })
725
+ * ```
726
+ */
727
+ list<const P extends readonly string[] = [], const S extends readonly FieldKey<T>[] | undefined = undefined, const L extends LocaleOption<ReadLocale<R>> | undefined = undefined>(options?: ListOptions<T, R, P, S, L>): Promise<EntryList<ReadAs<R, P, S, L>>>;
728
+ /**
729
+ * Every entry of this type that `where` keeps, page after page, as an async iterable: the walk
730
+ * follows each page's cursor and stops where the API says there is no more (REL-2415).
731
+ * `Array.fromAsync(relyio.content('post').listAll())` makes it an array. `limit` is how many each
732
+ * request reads; every other option is a list's, sent with each page.
733
+ *
734
+ * Iterating throws when a page says there is more and hands back no cursor to read it by, rather
735
+ * than ending the walk short.
736
+ */
737
+ listAll<const P extends readonly string[] = [], const S extends readonly FieldKey<T>[] | undefined = undefined, const L extends LocaleOption<ReadLocale<R>> | undefined = undefined>(options?: ListAllOptions<T, R, P, S, L>): AsyncIterable<ReadAs<R, P, S, L>>;
738
+ private walk;
739
+ /**
740
+ * One entry by its id or slug, or `null` when there is no such entry (REL-2415).
741
+ *
742
+ * `null` only for a missing **entry**: a type the schema does not declare, or a dataset with no
743
+ * schema deployed, throws `ResourceNotFoundError` naming which (`context.resourceType`), so an
744
+ * undeployed type never reads as a missing page.
745
+ *
746
+ * It takes the options of a list that are not about which entries or in what order — `include`,
747
+ * `locale`, `dataset`, `fresh`, `children` (REL-2417). The route declares no `fields`, so there is
748
+ * no `select`.
749
+ *
750
+ * @example
751
+ * ```typescript
752
+ * const post = await relyio.content('post').get('hello-world', { include: ['author'] })
753
+ * if (post === null) return notFound()
754
+ * ```
755
+ */
756
+ get<const P extends readonly string[] = [], const L extends LocaleOption<ReadLocale<R>> | undefined = undefined>(idOrSlug: string, options?: GetOptions<R, P, L>): Promise<ReadAs<R, P, undefined, L> | null>;
757
+ /**
758
+ * Update an entry you read, checked against the version it was read at (REL-2418).
759
+ *
760
+ * The check comes from the entry's own `_metadata`, which every read carries whether or not the
761
+ * cache served it, and the write addresses the locale the entry was read in — so a read whose
762
+ * locale came from the browser's language is written back to that locale. If the entry changed
763
+ * since the read, the write is refused with `ResourceConflictError` naming the version you
764
+ * read and the version it holds now: re-read it with `fresh: true` and retry. The SDK does not
765
+ * retry on its own.
766
+ *
767
+ * Pass an id instead to write **unchecked**, on purpose — an import that holds only ids stays one
768
+ * request per row.
769
+ *
770
+ * @example
771
+ * ```typescript
772
+ * const post = await relyio.content('post').get('hello-world')
773
+ * if (post) await relyio.content('post').update(post, { title: 'Hello, world' })
774
+ * ```
775
+ */
776
+ update(entry: ReadEntry, data: UpdateInput<T>, options?: Placement & RetryableWriteOptions): Promise<Revised>;
777
+ /**
778
+ * Update an entry by id, with no version check (partial update), on purpose: an import that holds
779
+ * only ids stays one request per row. `locale` defaults to the client's own.
780
+ */
781
+ update(id: string, data: UpdateInput<T>, options?: Placement & {
782
+ locale?: string;
783
+ } & RetryableWriteOptions): Promise<Revised>;
784
+ /**
785
+ * Delete an entry you read, checked against the version it was read at, as {@link update} is:
786
+ * an entry changed since the read is refused with `ResourceConflictError` and stays.
787
+ */
788
+ delete(entry: ReadEntry, options?: RetryableWriteOptions): Promise<void>;
789
+ /** Delete an entry by id, with no version check. */
790
+ delete(id: string, options?: RetryableWriteOptions): Promise<void>;
791
+ }
792
+
793
+ /** What a content entry's write places beside its data, as `PATCH /{contentType}/{id}` reads it. */
794
+ interface EntryPlacement {
795
+ slug?: string;
796
+ parentId?: string;
797
+ sortOrder?: number;
798
+ }
799
+ /**
800
+ * What a checked write takes beside the entry it was read as (REL-2418). No `locale` and no
801
+ * `ifMatch`: the entry names both, and a second source for either is how a check was dropped.
802
+ */
803
+ interface EntryWriteOptions extends EntryPlacement, RetryableWriteOptions {
804
+ }
805
+ /** What `create(data, options)` takes beside the data. */
806
+ interface CreateEntryOptions extends EntryPlacement, RetryableWriteOptions {
807
+ /** The locale the entry is created in; the client's own when left out. */
808
+ locale?: string;
809
+ /** The dataset to write, by id or name, over the client's. */
810
+ dataset?: string;
811
+ }
812
+ /**
813
+ * One content type's entries: read with `list`, `listAll` and `get`, written with `create`,
814
+ * `update` and `delete` (each from {@link EntryHandle}), and their history and children beside.
815
+ *
816
+ * @example
817
+ * ```typescript
818
+ * const products = client.content('product')
819
+ *
820
+ * // One page, one options object
821
+ * const { data: active, page } = await products.list({
822
+ * where: { status: 'active' },
823
+ * locale: 'en',
824
+ * include: ['brand'],
825
+ * limit: 20,
826
+ * })
827
+ *
828
+ * // Every entry, page after page
829
+ * for await (const product of products.listAll()) console.log(product.title)
830
+ * ```
831
+ */
832
+ declare class ContentClient<T extends BaseContent = BaseContent, R extends Reading = UntypedReading<T>> extends EntryHandle<T, R, RevisedContent<T>, EntryPlacement> {
833
+ /**
834
+ * Create a new content entry.
835
+ *
836
+ * `locale` defaults to the client's own, and `dataset` to the client's (`client.with({ dataset })`
837
+ * sets it for every call).
838
+ *
839
+ * @example
840
+ * ```typescript
841
+ * const post = await relyio.content('post').create({ title: 'Hello' }, { slug: 'hello' })
842
+ * ```
843
+ */
844
+ create(data: CreateInput<T>, options?: CreateEntryOptions): Promise<T>;
845
+ /**
846
+ * Get version history for one locale of an entry.
847
+ *
848
+ * Versions are numbered per locale, so this is one locale's timeline: `locale` defaults to the
849
+ * client's own, else the schema default; no fallback is applied, and a locale
850
+ * with no history returns an empty array rather than another locale's rows. Every row names the
851
+ * locale it belongs to.
852
+ */
853
+ versions(id: string, options?: {
854
+ limit?: number;
855
+ locale?: string;
856
+ }): Promise<Array<T & {
857
+ versionNumber: number;
858
+ locale: string;
859
+ }>>;
860
+ /**
861
+ * One version of an entry: `version(id, { version, locale })`.
862
+ *
863
+ * `locale` defaults to the client's own, else the schema default. Version numbers are per locale,
864
+ * so version 2 is different content in each one.
865
+ *
866
+ * There is no `null`: a version the locale does not have, an entry that is not there, a type the
867
+ * schema does not declare and a dataset with no schema all throw `ResourceNotFoundError`, its
868
+ * `context.resourceType` naming which (REL-2409 Q12).
869
+ */
870
+ version(id: string, options: VersionOptions): Promise<T>;
871
+ /**
872
+ * Revert one locale of an entry you read to a previous version: `restore(entry, { version })`,
873
+ * checked against the version the entry carries, in the locale it was read in — as
874
+ * `update(entry, data)` is. An entry changed since the read is refused with
875
+ * `ResourceConflictError`: re-read it with `fresh: true` and retry.
876
+ *
877
+ * Only that locale's fields change — shared fields and every other locale are left untouched. The
878
+ * restore is validated as strictly as a publish, so it cannot publish invalid content, and it
879
+ * records a new version of its own rather than rewinding the counter — so the revert is auditable
880
+ * and itself reversible.
881
+ *
882
+ * @returns The entry, its `_metadata.leftOut` naming each value of the version the active schema
883
+ * no longer declares, and `_metadata.brokenLinks` the entry links it left pointing nowhere.
884
+ */
885
+ restore(entry: ReadEntry, options: {
886
+ version: number;
887
+ } & RetryableWriteOptions): Promise<RestoredContent<T>>;
888
+ /**
889
+ * Revert one locale of an entry by id, with no version check: `restore(id, { version, locale })`.
890
+ *
891
+ * `locale` is **required**, and that is not a convention: versions are numbered per locale, so
892
+ * `version` alone is ambiguous, and defaulting it would silently revert the wrong translation.
893
+ */
894
+ restore(id: string, options: {
895
+ version: number;
896
+ locale: string;
897
+ } & RetryableWriteOptions): Promise<RestoredContent<T>>;
898
+ /**
899
+ * The direct children of a hierarchical entry, each read as `get` reads an entry.
900
+ *
901
+ * It takes `get`'s options — `include` (checked against the type), `locale` (a code, a list or
902
+ * `'*'`), `dataset`, `fresh` and `children` — the terms its route declares as the entry route
903
+ * does, sent the same way. The route lists one level and takes no depth (REL-2195); a tree nested
904
+ * under each child is the `children` option.
905
+ *
906
+ * @param id - Parent entry ID
907
+ */
908
+ children<const P extends readonly string[] = [], const L extends LocaleOption<ReadLocale<R>> | undefined = undefined>(id: string, options?: GetOptions<R, P, L>): Promise<Array<ReadAs<R, P, undefined, L>>>;
909
+ }
910
+
911
+ /**
912
+ * Upload/processing error taxonomy (plan 17, 00b Step 4).
913
+ *
914
+ * A **thin customer-side mirror** of the live-contracts `FileErrorCode`
915
+ * (correction C6 — the SDK must not import the server/Studio contracts package).
916
+ * Keep the members in sync with `@relyio/live-contracts`.
917
+ *
918
+ * Written once, as values, because a processing failure's code arrives as a string the service
919
+ * stored and has to be recognised at runtime; `FileErrorCode` is read off this list. The module is
920
+ * internal — `errors/index.ts` does not re-export it — so the list is not a second public name for
921
+ * the type.
922
+ */
923
+ declare const FILE_ERROR_CODES: readonly ["UPLOAD_TOO_LARGE", "UNSUPPORTED_MIME", "DIMENSION_OUT_OF_RANGE", "ASPECT_RATIO", "CORRUPT_FILE", "UNSUPPORTED_CODEC", "IMAGES_LIMIT_EXCEEDED", "STREAM_ENCODE_FAILED", "SCAN_INFECTED", "INTERNAL"];
924
+
925
+ /**
926
+ * Upload/processing error taxonomy (plan 17, 00b Step 4): a customer-side mirror of the
927
+ * live-contracts `FileErrorCode`, read off the list in `./file-error-codes`.
928
+ */
929
+ type FileErrorCode = (typeof FILE_ERROR_CODES)[number];
930
+ /**
931
+ * The code a {@link FileUploadError} carries: a {@link FileErrorCode}, or `PROCESSING_FAILED` for a
932
+ * processing failure this SDK cannot name — the service stored no code, as an image or file
933
+ * processing failure is stored today, or one this version of the SDK predates. What the service
934
+ * stored is in the error's `context.processingError` either way.
935
+ */
936
+ type FileUploadErrorCode = FileErrorCode | 'PROCESSING_FAILED';
937
+ /**
938
+ * A failure during the upload protocol (presign / PUT / multipart / confirm) or
939
+ * surfaced by the server's processing pipeline. Carries a typed {@link FileUploadErrorCode}
940
+ * and a `retriable` hint so callers (and `upload()`'s bounded auto-retry) can
941
+ * decide whether to retry on the same entry.
942
+ */
943
+ declare class FileUploadError extends SDKError {
944
+ readonly code: FileUploadErrorCode;
945
+ readonly retryable: boolean;
946
+ constructor(code: FileUploadErrorCode, message: string, options?: {
947
+ retriable?: boolean;
948
+ statusCode?: number;
949
+ cause?: Error;
950
+ context?: Record<string, unknown>;
951
+ });
952
+ getUserMessage(): string;
953
+ }
954
+ /**
955
+ * Client-side file validation failure (MIME/size/etc.) raised **before** any
956
+ * bytes leave the machine (UX only — the server presign stays authoritative).
957
+ */
958
+ declare class FileValidationError extends BaseValidationError {
959
+ readonly code = "FILE_VALIDATION_ERROR";
960
+ constructor(message: string, violations?: ValidationViolation[], cause?: Error);
961
+ getUserMessage(): string;
962
+ }
963
+ /** Constraints `validateFile` checks a file against (subset of the schema upload gate). */
964
+ interface FileValidationConstraints {
965
+ /** Allowed MIME types (exact match). */
966
+ allowedMimeTypes?: string[];
967
+ /** Max size in bytes. */
968
+ maxSizeBytes?: number;
969
+ }
970
+ /**
971
+ * Validate a file against explicit constraints, returning typed violations.
972
+ *
973
+ * The customer SDK does not carry a `SchemaReader` (that lives in the Studio
974
+ * layer), so callers pass the constraints they want enforced client-side; the
975
+ * server presign remains the authority. Returns an empty array when valid.
976
+ */
977
+ declare function validateFile(file: {
978
+ type?: string;
979
+ size?: number;
980
+ }, constraints?: FileValidationConstraints): ValidationViolation[];
981
+
982
+ /** Delivery visibility for a file binding (gateway-enforced, no byte relocation). */
983
+ type AssetVisibility = 'public' | 'protected' | 'private';
984
+ /** Binary lifecycle status — independent of the entry's publishing status. */
985
+ type FileStatus = 'pending_upload' | 'uploading' | 'processing' | 'ready' | 'failed';
986
+ /** Storage backend: `r2` = image + file kinds, `stream` = video kind. */
987
+ type FileBackend = 'r2' | 'stream';
988
+ /** Extracted media metadata (Phase 1 basics + Phase 2 enrichment, plans 15–19); open-ended. */
989
+ type AssetFileExtracted = NonNullable<AssetFileBlock['extracted']>;
990
+ /**
991
+ * Where one file binding's upload got to, as {@link AssetFileBuilder.status} reads it
992
+ * (`GET /v1/{assetType}/{id}/file`): the document's `FileStatus` — the binding's lifecycle and
993
+ * nothing of the entry's content. `processing.error` is present exactly while `status` is `failed`.
994
+ */
995
+ type FileStatusResult = components['schemas']['FileStatus'];
996
+
997
+ /**
998
+ * An asset entry response: schema fields at the top level plus the reserved
999
+ * `file` + `urls` blocks and `_metadata`. `T` carries the inferred field shape — for a typed client
1000
+ * `InferAssetType`, so this is `InferAssetEntry`, the shape an expanded asset reads as too.
1001
+ */
1002
+ type AssetEntryResponse<T extends BaseContent = BaseContent> = T & AssetServedBlocks;
1003
+ /** Request a single upload target for the binding. */
1004
+ interface PresignInput {
1005
+ /** Original file name (required). */
1006
+ fileName: string;
1007
+ /** MIME type the client will upload with. */
1008
+ contentType?: string;
1009
+ /** File size in bytes (early upload-gate validation). */
1010
+ size?: number;
1011
+ /** Locale to target for file-localized asset types. */
1012
+ locale?: string;
1013
+ /** Advisory client-computed SHA-256 (duplicate detection). */
1014
+ hash?: string;
1015
+ }
1016
+ /** Single upload target returned by `presign`. */
1017
+ interface PresignResult {
1018
+ backend: FileBackend;
1019
+ /** `put` = single PUT / Stream POST; `multipart` = resumable R2 session (plan 17). */
1020
+ mode?: 'put' | 'multipart';
1021
+ /** R2 presigned PUT (image/file) or Stream direct-creator URL (video). */
1022
+ uploadUrl?: string;
1023
+ method: 'PUT' | 'POST';
1024
+ headers?: Record<string, string>;
1025
+ /** Object key for the binding — present for R2 (needed to drive multipart). */
1026
+ objectKey?: string;
1027
+ storageVersion?: number;
1028
+ /** Multipart upload id — present only for `mode: 'multipart'`. */
1029
+ uploadId?: string;
1030
+ /** Multipart part size in bytes — present only for `mode: 'multipart'`. */
1031
+ partSize?: number;
1032
+ expiresAt: string;
1033
+ locale?: string;
1034
+ }
1035
+ /** Result of opening a multipart session (`file(uid).createMultipart()`). */
1036
+ interface MultipartCreateResult {
1037
+ uploadId: string;
1038
+ objectKey: string;
1039
+ partSize: number;
1040
+ storageVersion: number;
1041
+ locale?: string;
1042
+ }
1043
+ /** A single uploaded part (`{ PartNumber, ETag }` ordering for `complete`). */
1044
+ interface MultipartPart {
1045
+ partNumber: number;
1046
+ etag: string;
1047
+ size?: number;
1048
+ }
1049
+ /** Target a multipart session in subsequent ops. */
1050
+ interface MultipartTarget {
1051
+ uploadId: string;
1052
+ objectKey: string;
1053
+ locale?: string;
1054
+ }
1055
+ /** Upload progress tick handed to `.onProgress()`. */
1056
+ interface UploadProgress {
1057
+ /** `upload` for client → storage bytes. */
1058
+ phase: 'upload';
1059
+ /** Bytes transferred so far. */
1060
+ loaded: number;
1061
+ /** Total bytes (when known). */
1062
+ total: number;
1063
+ /** 0–100 integer. */
1064
+ progress: number;
1065
+ }
1066
+ /** Acknowledge a completed client upload. */
1067
+ interface ConfirmInput {
1068
+ locale?: string;
1069
+ hash?: string;
1070
+ }
1071
+ /** Result of `confirm`. */
1072
+ interface ConfirmResult {
1073
+ accepted: boolean;
1074
+ }
1075
+ /** Resolve a single gateway delivery URL. */
1076
+ interface ResolveUrlInput {
1077
+ locale?: string;
1078
+ /** Named image variant id. */
1079
+ variantId?: string;
1080
+ /** Video delivery target. */
1081
+ purpose?: 'playback' | 'poster' | 'download';
1082
+ }
1083
+ /** Result of `url`. `expiresAt` is omitted for `public` bindings. */
1084
+ interface ResolveUrlResult {
1085
+ url: string;
1086
+ expiresAt?: string;
1087
+ }
1088
+ /** Options when creating an asset entry. */
1089
+ interface CreateAssetOptions {
1090
+ slug?: string;
1091
+ locale?: string;
1092
+ /** Original file name (also settable at presign). */
1093
+ fileName?: string;
1094
+ /** Initial delivery visibility (default `public`). */
1095
+ visibility?: AssetVisibility;
1096
+ /** Auto-publish the entry when the file becomes ready (default `true`). */
1097
+ publish?: boolean;
1098
+ }
1099
+ /**
1100
+ * What an asset's write places beside its data: its `slug` alone, the one the asset branch of
1101
+ * `PATCH /{assetType}/{id}` reads (`EntriesController.updateEntry`) — a `parentId` or `sortOrder`
1102
+ * would be dropped there, so neither is offered.
1103
+ */
1104
+ interface AssetPlacement {
1105
+ slug?: string;
1106
+ }
1107
+ /** Options for {@link AssetClient.update} by id: its placement, its locale and a retry key. */
1108
+ type UpdateAssetOptions = AssetPlacement & {
1109
+ locale?: string;
1110
+ } & RetryableWriteOptions;
1111
+ /** Options for {@link AssetFileBuilder.waitUntilReady}. */
1112
+ interface WaitUntilReadyOptions {
1113
+ /** Locale of the binding to watch. */
1114
+ locale?: string;
1115
+ /**
1116
+ * Total time to wait before giving up (ms), from 0 to 2147483647. Default 60000. `0` reads the
1117
+ * file once and rejects with a `TimeoutError` if it is not ready.
1118
+ */
1119
+ timeoutMs?: number;
1120
+ /** Delay between polls (ms), from 1 to 2147483647. Default 1500. */
1121
+ pollIntervalMs?: number;
1122
+ }
1123
+ /**
1124
+ * Operations on a single asset entry's file binding, backed by the
1125
+ * `/{assetType}/{entryUid}/file/*` routes (plan 06 §2).
1126
+ */
1127
+ declare class AssetFileBuilder<T extends BaseContent = BaseContent> {
1128
+ private readonly httpClient;
1129
+ private readonly assetType;
1130
+ private readonly entryUid;
1131
+ /** The client's locale, asked on every call rather than copied when this builder was taken. */
1132
+ private readonly clientLocale;
1133
+ /**
1134
+ * @param locale - The client's locale, sent when a call names none: the function
1135
+ * `TypedRelyIOClient` hands over, or a locale fixed for this builder
1136
+ */
1137
+ constructor(httpClient: HttpClient, assetType: string, entryUid: string, locale?: ClientLocale);
1138
+ /**
1139
+ * The path parameters every `/file/*` route on this builder takes.
1140
+ *
1141
+ * The path *template* is the document's; only these two values are the
1142
+ * builder's, so a route that gains or loses a segment is a compile error here
1143
+ * rather than a hand-built string that silently misses.
1144
+ */
1145
+ private get target();
1146
+ private localeOf;
1147
+ /**
1148
+ * The `locale` body field of a multipart call: the session's, else the client's. The API finds
1149
+ * the binding by it, so a session a `with({ locale })` client opened must name the same locale on
1150
+ * every later call or its `objectKey` is refused as another binding's.
1151
+ */
1152
+ private localeField;
1153
+ /**
1154
+ * Issue a single upload target — an R2 presigned PUT (image/file) or a
1155
+ * Cloudflare Stream direct-creator URL (video). Resumable multipart is plan 17.
1156
+ */
1157
+ presign(input: PresignInput): Promise<PresignResult>;
1158
+ /**
1159
+ * Acknowledge a completed client upload (kicks off the fast-path intake for
1160
+ * R2; a no-op for Stream, whose readiness is webhook-driven).
1161
+ */
1162
+ confirm(input?: ConfirmInput): Promise<ConfirmResult>;
1163
+ /** Open an R2 multipart session for a large file (standalone of `presign`). */
1164
+ createMultipart(input: PresignInput): Promise<MultipartCreateResult>;
1165
+ /** Presign a single part PUT (1-based `partNumber`). */
1166
+ signPart(target: MultipartTarget & {
1167
+ partNumber: number;
1168
+ }): Promise<{
1169
+ url: string;
1170
+ }>;
1171
+ /** List parts already uploaded (cross-session resume). */
1172
+ listParts(target: MultipartTarget): Promise<MultipartPart[]>;
1173
+ /** Finalize a multipart session. */
1174
+ completeMultipart(target: MultipartTarget & {
1175
+ parts: MultipartPart[];
1176
+ }): Promise<{
1177
+ ok: boolean;
1178
+ etag?: string;
1179
+ }>;
1180
+ /** Abort a multipart session, freeing staged parts. */
1181
+ abortMultipart(target: MultipartTarget): Promise<{
1182
+ aborted: boolean;
1183
+ }>;
1184
+ /**
1185
+ * Resolve a single gateway delivery URL for the requested locale/variant/purpose.
1186
+ * Throws if the binding is not `ready`.
1187
+ */
1188
+ url(input?: ResolveUrlInput): Promise<ResolveUrlResult>;
1189
+ /**
1190
+ * Read where this file's upload got to — `status`, and the processing stage, progress and error
1191
+ * the pipeline recorded — whatever the asset entry is published in.
1192
+ *
1193
+ * An asset is a draft until its file is ready, and stays one when its file fails, so a key that
1194
+ * reads published content finds no entry for an upload in flight or one that failed. This read
1195
+ * answers for the binding itself. It takes the capability the upload calls take, so a read-only
1196
+ * key is refused with `ReadOnlyKeyError`, and it never returns the entry's fields.
1197
+ */
1198
+ status(options?: {
1199
+ locale?: string;
1200
+ }): Promise<FileStatusResult>;
1201
+ /**
1202
+ * Update the binding's delivery visibility (token-based; no byte relocation —
1203
+ * a downgrade purges the gateway cache). Returns the refreshed asset entry.
1204
+ */
1205
+ updateVisibility(visibility: AssetVisibility, options?: {
1206
+ locale?: string;
1207
+ }): Promise<AssetEntryResponse<T>>;
1208
+ /**
1209
+ * Set the binding's normalized (0..1, origin top-left) smart-crop focal point
1210
+ * (plan 18) so `cover`/`crop` variants frame the subject. Per locale for
1211
+ * file-localized asset types; token-based (no byte change). Returns the
1212
+ * refreshed asset entry.
1213
+ */
1214
+ setFocalPoint(focalPoint: {
1215
+ x: number;
1216
+ y: number;
1217
+ }, options?: {
1218
+ locale?: string;
1219
+ }): Promise<AssetEntryResponse<T>>;
1220
+ /**
1221
+ * Remove the file binding, clean up the underlying bytes, and purge the
1222
+ * gateway cache. The asset entry itself is preserved.
1223
+ */
1224
+ delete(options?: {
1225
+ locale?: string;
1226
+ }): Promise<void>;
1227
+ /**
1228
+ * Poll the asset entry until its file binding reaches `ready`, and resolve with the ready entry.
1229
+ *
1230
+ * Each poll reads the entry through the key's lens. When that finds nothing — a key that reads
1231
+ * published content, and an asset that is a draft until its file is ready and stays one when it
1232
+ * fails — the poll reads the binding itself ({@link status}), so a failed upload rejects as one
1233
+ * rather than running out the limit (REL-1481).
1234
+ *
1235
+ * @throws {FileUploadError} When processing failed. `code` is the processing code the service
1236
+ * stored — `UNSUPPORTED_CODEC`, `CORRUPT_FILE` and so on — or `PROCESSING_FAILED` where it
1237
+ * stored none this SDK knows; `context.processingError` holds what it stored, and `retryable`
1238
+ * is its `retriable` hint.
1239
+ * @throws {TimeoutError} When `timeoutMs` passes first. The file may still become ready; the
1240
+ * error's `context.timeout` is the limit and `context.fileStatus` the status last seen. A file
1241
+ * that is `ready` in an asset the key's lens never shows — created with `publish: false`, or
1242
+ * awaiting a workflow — ends here too, and the message says so.
1243
+ * @throws {ReadOnlyKeyError} When the key's lens does not show the asset and the key may not read
1244
+ * its binding either: nothing it can read would ever report the file.
1245
+ */
1246
+ /** The asset entry through the key's lens, or `null` where that lens finds none. */
1247
+ private readEntry;
1248
+ waitUntilReady(options?: WaitUntilReadyOptions): Promise<AssetEntryResponse<T>>;
1249
+ }
1250
+ /**
1251
+ * High-level one-shot upload: create draft entry → presign → PUT bytes →
1252
+ * confirm → (optionally) wait for `ready`. Handles the common image/file flow
1253
+ * (single R2 PUT). For advanced/resumable flows (large files, video tus,
1254
+ * progress) use the lower-level `file(uid)` builder — full upload resilience is
1255
+ * plan 17.
1256
+ */
1257
+ declare class AssetUploadBuilder<T extends BaseContent = BaseContent> {
1258
+ private readonly httpClient;
1259
+ private readonly assetType;
1260
+ private readonly file;
1261
+ private _fileName?;
1262
+ private _visibility?;
1263
+ private _locale?;
1264
+ private _publish?;
1265
+ private _content;
1266
+ private _slug?;
1267
+ private _waitForReady;
1268
+ private _waitOptions;
1269
+ private _onProgress?;
1270
+ private _validate?;
1271
+ private _maxPartRetries;
1272
+ /** The client's locale, read once when the upload starts. */
1273
+ private readonly clientLocale;
1274
+ /**
1275
+ * @param locale - The client's locale, the upload's when `.locale()` names none: the function
1276
+ * `TypedRelyIOClient` hands over, or a locale fixed for this upload
1277
+ */
1278
+ constructor(httpClient: HttpClient, assetType: string, file: Blob, locale?: ClientLocale);
1279
+ /** Subscribe to client → storage upload progress (single PUT + multipart). */
1280
+ onProgress(callback: (_progress: UploadProgress) => void): this;
1281
+ /**
1282
+ * Validate the file client-side (UX only) against explicit constraints before
1283
+ * any bytes leave the machine. Rejects instantly with a {@link FileValidationError}.
1284
+ */
1285
+ validate(constraints: FileValidationConstraints): this;
1286
+ /** Bounded per-part retry attempts for multipart (default 3). */
1287
+ maxPartRetries(attempts: number): this;
1288
+ /** Set the original file name (defaults to `file.name` for `File` inputs). */
1289
+ filename(name: string): this;
1290
+ /** Set the initial delivery visibility (default `public`). */
1291
+ visibility(visibility: AssetVisibility): this;
1292
+ /** Target locale for file-localized asset types. */
1293
+ locale(locale: string): this;
1294
+ /** Suppress (or enable) auto-publish on ready. */
1295
+ publish(publish: boolean): this;
1296
+ /** Set the asset's metadata field values (e.g. `{ alt: '...' }`). */
1297
+ content(content: Record<string, unknown>): this;
1298
+ /** Set an explicit slug for the new entry. */
1299
+ slug(slug: string): this;
1300
+ /**
1301
+ * Wait until the file binding becomes `ready` before resolving.
1302
+ *
1303
+ * @throws {Error} When `timeoutMs` or `pollIntervalMs` is one the wait cannot honour — refused
1304
+ * here, before anything is uploaded, rather than after the bytes are in.
1305
+ */
1306
+ waitForReady(options?: boolean | Omit<WaitUntilReadyOptions, 'locale'>): this;
1307
+ /** Execute the full upload flow and resolve with the asset entry. */
1308
+ execute(): Promise<AssetEntryResponse<T>>;
1309
+ /** Single presigned PUT (≤ 50 MB / Stream POST). */
1310
+ private uploadSingle;
1311
+ /**
1312
+ * Resumable R2 multipart: slice into `partSize` chunks, skip already-uploaded
1313
+ * parts (resume via `list-parts`), PUT each remaining part with bounded retry,
1314
+ * then `complete`. Aborts the session on unrecoverable failure.
1315
+ */
1316
+ private uploadMultipart;
1317
+ /** PUT a single part, retrying transient failures up to `_maxPartRetries`. */
1318
+ private putPartWithRetry;
1319
+ private emitProgress;
1320
+ }
1321
+ /**
1322
+ * An asset read typed by the asset type's reading `R`: what the type's include paths make of its
1323
+ * fields, beside the `file` and `urls` blocks every asset read carries (REL-1801).
1324
+ */
1325
+ interface AssetResponseReading<R extends Reading, T extends BaseContent = BaseContent> extends Reading {
1326
+ readonly read: AssetEntryResponse<Extract<ReadWith<R, this['include']>, BaseContent>>;
1327
+ readonly checked: CheckedInclude<R, this['path']>;
1328
+ /**
1329
+ * The asset type's own `where` — every path the API filters its fields by, never the `file` and
1330
+ * `urls` blocks — or, where no schema types it, the conditions `T`'s keys take (REL-2195).
1331
+ */
1332
+ readonly where: ReadWhere<WhereClause<T>, R>;
1333
+ /** The locale codes its `locale` may name: the asset type's reading's. */
1334
+ readonly locale: ReadLocale<R>;
1335
+ }
1336
+ /**
1337
+ * One asset type's entries: `list`, `listAll`, `get`, `update` and `delete` as a content type's
1338
+ * handle has them (assets are `entryType: 'asset'` entries on the same `/{assetType}` routes), plus
1339
+ * `create`, the `file(uid)` sub-resource builder and an `upload()` helper.
1340
+ */
1341
+ declare class AssetClient<T extends BaseContent = BaseContent, R extends Reading = UntypedReading<T>> extends EntryHandle<T, AssetResponseReading<R, T>, AssetEntryResponse<T>, AssetPlacement> {
1342
+ /**
1343
+ * Create a new asset entry as a draft. The binary is uploaded separately via
1344
+ * `file(uid)` (or use the high-level `upload()` helper), and the entry
1345
+ * auto-publishes when its file becomes ready (unless `publish: false`).
1346
+ * `locale` defaults to the client's own.
1347
+ */
1348
+ create(data: CreateInput<T>, options?: CreateAssetOptions & RetryableWriteOptions): Promise<AssetEntryResponse<T>>;
1349
+ /** Get the file sub-resource builder for a specific asset entry. */
1350
+ file(entryUid: string): AssetFileBuilder<T>;
1351
+ /** Start a high-level upload (create → presign → PUT → confirm). */
1352
+ upload(file: Blob): AssetUploadBuilder<T>;
1353
+ }
1354
+ /**
1355
+ * Narrow an entry (e.g. a populated reference from `include`) to an asset entry.
1356
+ * Relies on the `_metadata.entryType` discriminator the API emits (plan 06).
1357
+ *
1358
+ * @example
1359
+ * ```typescript
1360
+ * const article = await relyio.content('article').get(id, { include: ['hero'] })
1361
+ * if (isAssetEntry(article?.hero)) {
1362
+ * console.log(article.hero.urls?.original)
1363
+ * }
1364
+ * ```
1365
+ */
1366
+ declare function isAssetEntry<T extends BaseContent = BaseContent>(entry: unknown): entry is AssetEntryResponse<T>;
1367
+ /**
1368
+ * A reference field populated (via `include`) with a full asset entry. Identical
1369
+ * to {@link AssetEntryResponse}; named for readability at reference call sites.
1370
+ */
1371
+ type PopulatedAssetReference<T extends BaseContent = BaseContent> = AssetEntryResponse<T>;
1372
+
1373
+ /**
1374
+ * What a one-document write sends: the data, the locale it writes (absent: the API's default) and
1375
+ * the `If-Match` check of a read, when it is one.
1376
+ */
1377
+ interface DocumentWrite<T> {
1378
+ readonly content: DocumentFields<T>;
1379
+ readonly locale: string | undefined;
1380
+ readonly ifMatch: string | undefined;
1381
+ }
1382
+ /**
1383
+ * A document's fields as a write sends them: any of them, and no `_metadata` — the read that
1384
+ * carries one is the checked form's first argument, never the data.
1385
+ */
1386
+ type DocumentFields<T> = Partial<Omit<T, '_metadata'>> & {
1387
+ readonly _metadata?: never;
1388
+ };
1389
+ /** What an unchecked one-document write takes beside its data: the locale it writes, and its key. */
1390
+ interface DocumentWriteOptions extends RetryableWriteOptions {
1391
+ /** The locale to write; the client's own when left out. */
1392
+ readonly locale?: string;
1393
+ }
1394
+ /** What `versions(options)` takes on a one-document handle. */
1395
+ interface DocumentVersionsOptions {
1396
+ /** How many versions to list, newest first; 10 when left out. */
1397
+ readonly limit?: number;
1398
+ /** The locale whose timeline it is; the client's own, then the schema default, when left out. */
1399
+ readonly locale?: string;
1400
+ }
1401
+ /** One row of a one-document handle's history: the document as it was, in the locale it names. */
1402
+ type DocumentVersion<T> = T & {
1403
+ versionNumber: number;
1404
+ locale: string;
1405
+ };
1406
+ /**
1407
+ * The verbs a singleton's and a settings group's handle share (REL-2419, REL-2409 decision Q18):
1408
+ * `update`, `versions` and `version`, written once, as {@link EntryHandle} writes the entry routes'
1409
+ * once for a content type and an asset type. The two were near-clones that had already drifted —
1410
+ * in which locale they treated as none, and in what `version()` answered for a missing document.
1411
+ *
1412
+ * What differs per handle is its route, so each names its own four requests; every rule about
1413
+ * them — which locale a call sends, which form of a write is checked, what a missing version is —
1414
+ * is here.
1415
+ *
1416
+ * Every call is in the locale it names, and otherwise in the client's own as it is when the call is
1417
+ * made; a write of a document you read writes the locale it was read in.
1418
+ */
1419
+ declare abstract class DocumentHandle<T> {
1420
+ protected readonly httpClient: HttpClient;
1421
+ protected readonly name: string;
1422
+ /** The client's locale, asked on every call rather than copied when this handle was taken. */
1423
+ protected readonly clientLocale: () => string | undefined;
1424
+ /**
1425
+ * @param name - The singleton key or settings group id the routes are addressed by
1426
+ * @param locale - The client's locale: the function `TypedRelyIOClient` hands over, asked on
1427
+ * every call
1428
+ */
1429
+ constructor(httpClient: HttpClient, name: string, locale?: ClientLocale);
1430
+ /**
1431
+ * Write the document you read, checked against the version it was read at (REL-2419), in the
1432
+ * locale it was read in — as a content entry's `update(entry, data)` is. If it changed since the
1433
+ * read, the write is refused with `ResourceConflictError` naming both versions: re-read it with
1434
+ * `fresh: true` and retry.
1435
+ */
1436
+ update(read: ReadEntry, data: DocumentFields<T>, options?: RetryableWriteOptions): Promise<T>;
1437
+ /**
1438
+ * Write the document with no version check, on purpose, in `options.locale` or the client's own —
1439
+ * creating it, when nobody has yet.
1440
+ */
1441
+ update(data: DocumentFields<T>, options?: DocumentWriteOptions): Promise<T>;
1442
+ /**
1443
+ * Version history for one locale of the document.
1444
+ *
1445
+ * Versions are numbered per locale, so this is one locale's timeline: `locale` defaults to the
1446
+ * client's own, then the schema default; no fallback is applied, and a locale with no history
1447
+ * returns an empty array rather than another locale's rows. Every row names the locale it belongs
1448
+ * to.
1449
+ */
1450
+ versions(options?: DocumentVersionsOptions): Promise<Array<DocumentVersion<T>>>;
1451
+ /**
1452
+ * One version of the document: `version({ version, locale })`. `locale` defaults to the client's
1453
+ * own, then the schema default; version numbers are per locale, so version 2 is different content
1454
+ * in each one.
1455
+ *
1456
+ * There is no `null`: a version the locale does not have, a document nobody has written, one the
1457
+ * schema does not declare and a dataset with no schema all throw `ResourceNotFoundError`, its
1458
+ * `context.resourceType` naming which (REL-2409 Q12).
1459
+ */
1460
+ version(options: VersionOptions): Promise<T>;
1461
+ /** `PATCH` the document's route with this write. */
1462
+ protected abstract patch(write: DocumentWrite<T>, options?: RetryableWriteOptions): Promise<T>;
1463
+ /** `GET` the document's version history route. */
1464
+ protected abstract readVersions(query: {
1465
+ limit: number;
1466
+ locale?: string;
1467
+ }): Promise<{
1468
+ versions: Array<DocumentVersion<T>>;
1469
+ }>;
1470
+ /** `GET` one version of the document. */
1471
+ protected abstract readVersion(versionNumber: string, query: {
1472
+ locale?: string;
1473
+ }): Promise<T>;
1474
+ /** The locale a write or a history read sends: the call's, else the client's. */
1475
+ protected localeOf(named: string | undefined): string | undefined;
1476
+ /** The write's body: its data, and the locale it names when it names one. */
1477
+ protected static bodyOf<T>(write: DocumentWrite<T>): {
1478
+ content: DocumentFields<T>;
1479
+ locale?: string;
1480
+ };
1481
+ /** The two headers a write sends: its check, when it has one, and its `Idempotency-Key`. */
1482
+ protected static headersOf(write: Pick<DocumentWrite<unknown>, 'ifMatch'>, options?: RetryableWriteOptions): Record<string, string> | undefined;
1483
+ }
1484
+
1485
+ /**
1486
+ * One singleton: read with `get(options)`, written with `update`, and its history beside — the
1487
+ * verbs a content type's handle has, for one document (REL-2419, REL-2409 Q18). `update`,
1488
+ * `versions` and `version` are the ones every one-document handle shares ({@link DocumentHandle}).
1489
+ *
1490
+ * Every call is in the locale it names — the argument (`get({ locale })`, `versions({ locale })`,
1491
+ * `update(data, { locale })`) — and otherwise in the client's own (`client.with({ locale })`,
1492
+ * `updateConfig({ locale })`) as it is when the call is made, as `content()`'s are: reads, the write,
1493
+ * and version history alike. An update of a singleton you read writes the locale it was read in.
1494
+ *
1495
+ * @example
1496
+ * ```typescript
1497
+ * const homepage = await client.singleton('homepage').get({
1498
+ * include: ['featuredProducts', 'heroImage'],
1499
+ * locale: 'en',
1500
+ * })
1501
+ * if (homepage) await client.singleton('homepage').update(homepage, { title: 'Welcome' })
1502
+ * ```
1503
+ */
1504
+ declare class SingletonClient<T = BaseSingleton, R extends Reading = UntypedReading<T>> extends DocumentHandle<T> {
1505
+ /**
1506
+ * The singleton, or `null` when nobody has written it yet (REL-2419, REL-2409 Q18).
1507
+ *
1508
+ * `null` only for a singleton with no content: one the schema does not declare, or a dataset with
1509
+ * no schema deployed, throws `ResourceNotFoundError` naming which (`context.resourceType`
1510
+ * `'singleton'` or `'schema'`), as a content type's `get` does.
1511
+ *
1512
+ * It takes a content read's options where the route declares them — `include`, `locale` (one
1513
+ * code), `dataset` and `fresh` (`DocumentReadOptions`).
1514
+ *
1515
+ * @example
1516
+ * ```typescript
1517
+ * const home = await relyio.singleton('homepage').get({ include: ['hero'], locale: 'fr-FR' })
1518
+ * if (home) await relyio.singleton('homepage').update(home, { headline: 'Bonjour' })
1519
+ * ```
1520
+ */
1521
+ get<const P extends readonly string[] = []>(options?: DocumentReadOptions<R, P>): Promise<ReadWith<R, P[number]> | null>;
1522
+ /** The API publishes on PATCH: there is no separate publish route. */
1523
+ protected patch(write: DocumentWrite<T>, options?: RetryableWriteOptions): Promise<T>;
1524
+ protected readVersions(query: {
1525
+ limit: number;
1526
+ locale?: string;
1527
+ }): Promise<{
1528
+ versions: Array<DocumentVersion<T>>;
1529
+ }>;
1530
+ protected readVersion(versionNumber: string, query: {
1531
+ locale?: string;
1532
+ }): Promise<T>;
1533
+ }
1534
+
1535
+ /**
1536
+ * One settings group: read with `get(options)`, written with `update` and `reset`, and its history
1537
+ * beside — the verbs a content type's handle has, for one document (REL-2419, REL-2409 Q18).
1538
+ * `update`, `versions` and `version` are the ones every one-document handle shares
1539
+ * ({@link DocumentHandle}).
1540
+ *
1541
+ * Every call is in the locale it names — the argument (`get({ locale })`, `versions({ locale })`,
1542
+ * `update(data, { locale })`, `reset({ locale })`) — and otherwise in the client's own
1543
+ * (`client.with({ locale })`, `updateConfig({ locale })`) as it is when the call is made, as
1544
+ * `content()`'s are: reads, writes and version history alike. An update or reset of a group you
1545
+ * read writes the locale it was read in.
1546
+ *
1547
+ * The fields of a write travel as `content` beside an optional `locale`, the body the route
1548
+ * validates (`UpdateSettingsDto`); any other top-level key is refused `REQUEST_INVALID`.
1549
+ *
1550
+ * @example
1551
+ * ```typescript
1552
+ * const emailConfig = await client.settings('email-config').get({
1553
+ * include: ['defaultTemplate'],
1554
+ * locale: 'en',
1555
+ * })
1556
+ * await client.settings('email-config').update(emailConfig, { fromName: 'RelyIO' })
1557
+ * ```
1558
+ */
1559
+ declare class SettingsClient<T = BaseSettingsGroup, R extends Reading = UntypedReading<T>> extends DocumentHandle<T> {
1560
+ /**
1561
+ * The settings group (REL-2419, REL-2409 Q18).
1562
+ *
1563
+ * Never `null`: the API answers a declared group that nobody has written yet with its declared
1564
+ * defaults (it initializes the group on that first read), so there is no missing group to report.
1565
+ * A group the schema does not declare, or a dataset with no schema deployed, throws
1566
+ * `ResourceNotFoundError` naming which (`context.resourceType` `'settingsGroup'` or `'schema'`) —
1567
+ * the not-found rule every handle's `get` follows.
1568
+ *
1569
+ * It takes a content read's options where the route declares them — `include`, `locale` (one
1570
+ * code), `dataset` and `fresh` (`DocumentReadOptions`).
1571
+ *
1572
+ * @example
1573
+ * ```typescript
1574
+ * const site = await relyio.settings('site').get({ include: ['logo'] })
1575
+ * await relyio.settings('site').update(site, { title: 'New title' })
1576
+ * ```
1577
+ */
1578
+ get<const P extends readonly string[] = []>(options?: DocumentReadOptions<R, P>): Promise<ReadWith<R, P[number]>>;
1579
+ /**
1580
+ * Reset the group you read to its default values, checked against the version it was read at,
1581
+ * in the locale it was read in (REL-2419) — refused with `ResourceConflictError` if it changed
1582
+ * since the read.
1583
+ */
1584
+ reset(read: ReadEntry, options?: RetryableWriteOptions): Promise<T>;
1585
+ /**
1586
+ * Reset the group to its default values with no version check, in `options.locale` or the
1587
+ * client's own. The route reads its locale from `?locale=` and takes no body, so that is where it
1588
+ * is sent.
1589
+ */
1590
+ reset(options?: DocumentWriteOptions): Promise<T>;
1591
+ /** The group's default values, as the schema declares them. */
1592
+ getDefaults(): Promise<T>;
1593
+ protected patch(write: DocumentWrite<T>, options?: RetryableWriteOptions): Promise<T>;
1594
+ protected readVersions(query: {
1595
+ limit: number;
1596
+ locale?: string;
1597
+ }): Promise<{
1598
+ versions: Array<DocumentVersion<T>>;
1599
+ }>;
1600
+ protected readVersion(versionNumber: string, query: {
1601
+ locale?: string;
1602
+ }): Promise<T>;
1603
+ }
1604
+
1605
+ /**
1606
+ * Any schema the client can be typed by. `any` is deliberate: the client accepts whatever content
1607
+ * types, singletons, queries and settings groups a customer declares.
1608
+ *
1609
+ * This is the constraint `TypedRelyIOClient`'s `TSchema` carries, so `RegisteredSchema` is inferred
1610
+ * against the same one — `createClient` returns `TypedRelyIOClient<RegisteredSchema>`, and a schema
1611
+ * inferred against any wider alias would not be provably inside the client's bound.
1612
+ */
1613
+ type AnySchemaDefinition = SchemaDefinition<any, any, any, any, any>;
1614
+ /**
1615
+ * The seam `relyio generate-types` fills (`INV-SS-TYPES-INFERRED`).
1616
+ *
1617
+ * Empty here on purpose. The generated `schema.ts` augments it —
1618
+ *
1619
+ * ```ts
1620
+ * declare module '@relyio/sdk' {
1621
+ * interface Register {
1622
+ * schema: typeof schema
1623
+ * }
1624
+ * }
1625
+ * ```
1626
+ *
1627
+ * — and from then on `createClient({ apiKey })`, with no generic and no `schema:`, is typed by that
1628
+ * schema. The pattern is TanStack Router's. It is type-level only: nothing at runtime reads it.
1629
+ */
1630
+ interface Register {
1631
+ }
1632
+ /**
1633
+ * The schema the project registered through `Register`, or `undefined` when nothing did — which is
1634
+ * the untyped client, exactly as before the generator ran.
1635
+ */
1636
+ type RegisteredSchema = Register extends {
1637
+ schema: infer S extends AnySchemaDefinition;
1638
+ } ? S : undefined;
1639
+
1640
+ /**
1641
+ * What the search family's routes answer, read from the committed document, and what a search over
1642
+ * a client's content types returns (`search-options.ts` takes the options).
1643
+ */
1644
+
1645
+ /**
1646
+ * What a search or similar hit reports beside its fields: the committed document's
1647
+ * `SearchHitMetadata`, read through `http/api-schema.ts`.
1648
+ *
1649
+ * Narrower than an entry read's `EntryMetadata`, because a hit is not built by the entry read: it
1650
+ * carries no `entryType`, `defaultLocale`, `availableLocales`, `refs`, `firstPublishedAt` or
1651
+ * version history. Until REL-1413 a hit was typed as an entry and so promised all five — the
1652
+ * locale helpers kept an optional chain after `availableLocales` for the hits that lacked it. Read
1653
+ * the entry (`content(type).get(hit._metadata.id)`) for those.
1654
+ */
1655
+ type SearchHitMetadata = components['schemas']['SearchHitMetadata'];
1656
+ /** What a hit carries beside the entry's fields, as the document declares it. */
1657
+ type SearchHitSystemFields = Pick<components['schemas']['SearchHit'], '_metadata' | '_score' | '_scoreBreakdown' | '_highlights'>;
1658
+ /**
1659
+ * One search hit as the route emits it — the document's `SearchHit`: the fields `T` declares at the
1660
+ * top level (at `_metadata.locale`, or only the aliases a `fields` projection names), the hit's own
1661
+ * `_metadata`, and the score annotations the request asked for. There is no per-item `data` wrapper.
1662
+ *
1663
+ * `T`'s `_metadata` is replaced, not kept: `T` is an entry type, and a hit is not an entry read.
1664
+ * Every field is optional, a required one too: a `fields` projection names only some of them, and a
1665
+ * hit is built from the search row, not by the read that fills a required field a draft holds empty
1666
+ * (REL-1795). Distributes over a union of entry types, so a search across two types keeps each
1667
+ * one's fields.
1668
+ */
1669
+ type SearchResultItem<T = unknown> = T extends unknown ? Partial<Omit<T, '_metadata'>> & SearchHitSystemFields : never;
1670
+ /**
1671
+ * What `POST /v1/search` serves, **read from the committed document**.
1672
+ *
1673
+ * `{ data, page: { number, limit, total, pageCount }, meta }` — search is the page dialect
1674
+ * (Decision 5, amended for REL-2420): `total` counts every entry the search matched. The facets,
1675
+ * the strategy the engine chose and its warnings ride the declared `meta` sibling because none of
1676
+ * them is an item or a page fact.
1677
+ *
1678
+ * The hand-written `{ items, total?, metadata? }` this replaced named three keys the route does not
1679
+ * send.
1680
+ */
1681
+ type SearchResponse<T = unknown> = Wire<'/v1/search', 'post', SearchResultItem<T>>;
1682
+ /**
1683
+ * One autocomplete suggestion — the document's `AutocompleteSuggestion`: the value, its best score,
1684
+ * an entry holding it (`id`), and the value with its match wrapped in `<em>` when Atlas highlighted
1685
+ * it. The hand-written type this replaces named `highlighted`, `entryId` and `contentType`, none of
1686
+ * which the route sends, and made `score` optional where it is always sent.
1687
+ */
1688
+ type AutocompleteSuggestion = components['schemas']['AutocompleteSuggestion'];
1689
+ /**
1690
+ * What `POST /v1/search/autocomplete` serves. The suggestions are the
1691
+ * collection's `data`; the query it completed and the field it matched on ride
1692
+ * `meta`. Autocomplete is unpaginated, so its page reports `offset: 0` and a
1693
+ * `total` equal to what it returned — the dialect is declared rather than
1694
+ * omitted so one collection shape covers every list.
1695
+ */
1696
+ type AutocompleteResponse = Wire<'/v1/search/autocomplete', 'post', AutocompleteSuggestion>;
1697
+ /**
1698
+ * One similar-content hit as the route emits it: the same `SearchHit` a search serves, built by
1699
+ * the same transform, with `_score` on every hit.
1700
+ */
1701
+ type SimilarResult<T = unknown> = SearchResultItem<T>;
1702
+ /** What `POST /v1/search/similar` serves. */
1703
+ type SimilarResponse<T = unknown> = Wire<'/v1/search/similar', 'post', SimilarResult<T>>;
1704
+ /**
1705
+ * The content type names a search may be scoped to: the schema's where one types the client, any
1706
+ * name where none does.
1707
+ */
1708
+ type SearchTypeName<S extends AnySchemaDefinition | undefined> = S extends AnySchemaDefinition ? SchemaContentTypeNames<S> : string;
1709
+ /**
1710
+ * What a search over the content types `K` returns: their entries, typed by the schema
1711
+ * (`INV-SS-TYPES-INFERRED`) — or `T` unchanged where no schema types the client.
1712
+ */
1713
+ type SearchEntry<S extends AnySchemaDefinition | undefined, K extends SearchTypeName<S> = SearchTypeName<S>, T extends BaseContent = BaseContent> = S extends AnySchemaDefinition ? SchemaEntryOf<S, K & SchemaContentTypeNames<S>> : T;
1714
+
1715
+ /**
1716
+ * The search family in one options object each (REL-2420; REL-2409 decisions Q3 and Q19).
1717
+ *
1718
+ * `search(q, options)` pages by number — `page` and `limit`, as an archive page does — and answers
1719
+ * the API's page dialect, `{ number, limit, total, pageCount }`, so "page 2 of 3" needs no
1720
+ * arithmetic. It takes the read options that apply to a search, under the names every read uses:
1721
+ * `select`, `locale` (one code: a search composes its hits in one locale), `dataset` and `fresh`.
1722
+ * `autocomplete(q, options)` and `similar(id, options)` take one options object too.
1723
+ *
1724
+ * The routes declare no request body in the committed document, so each body is written here by
1725
+ * hand, in the route's own words: `select` is the route's `fields` alias map with each Field under
1726
+ * its own name, and `sort` an array in the object's key order. `dataset` and `fresh` are the same
1727
+ * header and query term every read sends ({@link readHeaders}).
1728
+ *
1729
+ */
1730
+
1731
+ /**
1732
+ * The Fields an entry of `E` declares, by key — of each type, where `E` is a union of the types a
1733
+ * search spans. Any name where `E` declares none: a client no schema types.
1734
+ */
1735
+ type SearchFieldKey<E> = [E] extends [never] ? string : E extends unknown ? [Exclude<keyof E, '_metadata'>] extends [never] ? string : Exclude<keyof E, '_metadata'> & string : never;
1736
+ /**
1737
+ * Which content types a search spans: one name, or a list of them. Left out, every content type
1738
+ * the key can see.
1739
+ */
1740
+ type SearchScope<K extends string> = K | readonly [K, ...K[]];
1741
+ /**
1742
+ * How hits are ordered: a direction per key, the **key order** the priority (Q14). `_score` is
1743
+ * relevance itself; any other key is a Field of the types searched. Left out, by relevance.
1744
+ */
1745
+ type SearchSortOption<E> = {
1746
+ readonly [K in SearchFieldKey<E> | '_score']?: 'asc' | 'desc';
1747
+ };
1748
+ /**
1749
+ * What a search's `where` takes over the content types `K`: the `where` `list` takes on each of them,
1750
+ * typed as `list` types it (REL-2427; one dialect, D11) — Field names checked, `$`-prefixed
1751
+ * operators, each value of its Field's type. Over several types, a `where` one of them takes. A
1752
+ * client no schema types takes the untyped list's `where`.
1753
+ */
1754
+ type SearchWhere<S extends AnySchemaDefinition | undefined, K extends string> = S extends AnySchemaDefinition ? K extends SchemaContentTypeNames<S> ? NonNullable<ListReadOptions<InferContentType<S['contentTypes'][K], S>, ContentReading<S['contentTypes'][K], S>, [
1755
+ ]>['where']> : never : NonNullable<ListReadOptions<BaseContent, UntypedReading<BaseContent>, []>['where']>;
1756
+ /** How a matching term is highlighted: `true` for the route's defaults, or where and how much. */
1757
+ interface SearchHighlight {
1758
+ readonly field?: string;
1759
+ readonly maxWords?: number;
1760
+ readonly minWords?: number;
1761
+ }
1762
+ /** One facet a search counts beside its hits, answered under `meta.facets` by its name. */
1763
+ interface SearchFacet {
1764
+ readonly type: 'string' | 'number' | 'date' | 'reference';
1765
+ /** The Field counted; a nested path such as `pricing.basePrice` is allowed. */
1766
+ readonly field: string;
1767
+ /** The most buckets a `string` or `reference` facet answers. */
1768
+ readonly limit?: number;
1769
+ /** A `number` or `date` facet's bucket edges. */
1770
+ readonly boundaries?: readonly number[];
1771
+ /** A `reference` facet's Field of the referenced entry to label each bucket with. */
1772
+ readonly displayField?: string;
1773
+ }
1774
+ /** The options every read of the search family takes: where, and how fresh. */
1775
+ interface SearchReadOptions<C extends string> {
1776
+ /** The locale the hits are composed in, over the client's: one code the schema declares. */
1777
+ readonly locale?: C;
1778
+ /** The dataset to search, by id or name, over the client's (Q9). */
1779
+ readonly dataset?: string;
1780
+ /** How fresh the answer must be: a feed position, or `true` for past every cache. */
1781
+ readonly fresh?: FreshOption;
1782
+ }
1783
+ /**
1784
+ * What `search(q, options)` takes.
1785
+ *
1786
+ * - `contentType` — {@link SearchScope}; every content type when left out.
1787
+ * - `page` and `limit` — which page, counted from 1, and how many hits a page holds (the route's
1788
+ * default 20, at most 100).
1789
+ * - `select` — the Fields each hit carries, beside `_metadata` and the scores asked for.
1790
+ * - `where`, `searchFields`, `sort`, `includeScore`, `explain`, `highlight`, `facets` — the search's
1791
+ * own, as the route documents them.
1792
+ */
1793
+ interface SearchOptions<K extends string, E, C extends string, W, S extends readonly SearchFieldKey<E>[] | undefined = undefined> extends SearchReadOptions<C> {
1794
+ readonly contentType?: SearchScope<K>;
1795
+ /**
1796
+ * The conditions a hit must meet: {@link SearchWhere}, the `where` `list` takes. A condition on
1797
+ * `_metadata` is refused before anything is sent.
1798
+ */
1799
+ readonly where?: W;
1800
+ /** The Fields the text is matched in; every text Field when left out. */
1801
+ readonly searchFields?: readonly SearchFieldKey<E>[];
1802
+ /** The Fields each hit carries (Q15). */
1803
+ readonly select?: S;
1804
+ readonly sort?: SearchSortOption<E>;
1805
+ /** Which page, counted from 1. */
1806
+ readonly page?: number;
1807
+ /** How many hits a page holds. */
1808
+ readonly limit?: number;
1809
+ /** Each hit's relevance as `_score`. */
1810
+ readonly includeScore?: boolean;
1811
+ /** Each hit's score broken down, for debugging relevance. */
1812
+ readonly explain?: boolean;
1813
+ readonly highlight?: boolean | SearchHighlight;
1814
+ /** Counts to answer beside the hits, under `meta.facets`, each by its name. */
1815
+ readonly facets?: Readonly<Record<string, SearchFacet>>;
1816
+ }
1817
+ /** What `autocomplete(q, options)` takes: the type and the Field to complete are required. */
1818
+ interface AutocompleteOptions<K extends string, E, C extends string, W> extends SearchReadOptions<C> {
1819
+ readonly contentType: K;
1820
+ /** The Field to complete, or a nested path under one (`category.name`). */
1821
+ readonly field: SearchFieldKey<E> | `${SearchFieldKey<E>}.${string}`;
1822
+ /** Tolerate typos, on a Field that is not localized. */
1823
+ readonly fuzzy?: boolean;
1824
+ /**
1825
+ * The conditions an entry must meet to be suggested from, on Fields that are not localized:
1826
+ * {@link SearchWhere}, the `where` `list` takes.
1827
+ */
1828
+ readonly where?: W;
1829
+ /** How many suggestions to answer. */
1830
+ readonly limit?: number;
1831
+ }
1832
+ /** What `similar(id, options)` takes. */
1833
+ interface SimilarOptions<K extends string, E, C extends string, S extends readonly SearchFieldKey<E>[] | undefined = undefined> extends SearchReadOptions<C> {
1834
+ /** Which content types to find neighbours in; every content type when left out. */
1835
+ readonly contentType?: SearchScope<K>;
1836
+ /** The Fields each hit carries. */
1837
+ readonly select?: S;
1838
+ /** How many neighbours to answer. */
1839
+ readonly limit?: number;
1840
+ }
1841
+ /** `E` with only the Fields `select` named, and `_metadata`; `E` when nothing is selected. */
1842
+ type SearchSelected<E, S> = S extends readonly [] | undefined ? E : S extends readonly string[] ? E extends unknown ? Pick<E, Extract<keyof E, S[number] | '_metadata'>> : never : E;
1843
+ /**
1844
+ * What `search(q, options)` answers: the API's own `{ data, page, meta }`, the page in the page
1845
+ * dialect — `{ number, limit, total, pageCount }`, `total` counting every entry the search matched.
1846
+ */
1847
+ type SearchPage<E> = Wire<'/v1/search', 'post', SearchResultItem<E>>;
1848
+
1849
+ /**
1850
+ * A batch as one call: `client.batch(operations, options)` (REL-2421; REL-2409 decision Q22).
1851
+ *
1852
+ * Each operation reads like the single write it batches:
1853
+ *
1854
+ * - `{ create: 'post', data, slug?, locale?, parentId?, sortOrder? }` — `content('post').create`;
1855
+ * - `{ update: 'post', entry, data }` — `update(entry, data)`: checked against the version the entry
1856
+ * you read carries, in the locale it was read in (REL-2418);
1857
+ * - `{ update: 'post', id, data, locale? }` — `update(id, data)`: unchecked, on purpose;
1858
+ * - `{ replace: 'post', entry | id, data }` — the same, replacing the entry's content in that locale.
1859
+ * The Content API has no single-entry replace route, so a batch is where a replace is written;
1860
+ * - `{ delete: 'post', entry | id }` — checked in the locale the entry was read in, or unchecked;
1861
+ * - `{ singleton: 'homepage', data, locale? }` — a singleton write, which creates it the first time.
1862
+ *
1863
+ * A check travels as the operation's `ifMatch`, the tag `If-Match` takes on the single-entry routes:
1864
+ * one header cannot guard many writes. A stale one fails that operation with `RESOURCE_CONFLICT`
1865
+ * naming the versions that disagreed; under `atomic: true` nothing in the batch is written.
1866
+ *
1867
+ * The call answers the API's own body, `{ data: { results, summary } }`: a batch is answered `200`
1868
+ * whatever its operations did, and each result says what happened to its operation — an `outcome`,
1869
+ * or an `error` naming the refusal. Nothing is thrown for an operation that failed.
1870
+ */
1871
+
1872
+ /** The content types an operation may name: the schema's, or any where no schema types the client. */
1873
+ type ContentTypeName<S> = S extends AnySchemaDefinition ? SchemaContentTypeNames<S> : string;
1874
+ /** The singletons a write may name: the schema's, or any where no schema types the client. */
1875
+ type SingletonName<S> = S extends AnySchemaDefinition ? SchemaSingletonNames<S> : string;
1876
+ /** The locale codes an operation may name: the schema's, or any. */
1877
+ type LocaleName<S> = S extends AnySchemaDefinition ? SchemaLocaleCodes<S> : string;
1878
+ /** An entry of content type `K`, as the schema declares it. */
1879
+ type EntryOf$1<S, K> = S extends AnySchemaDefinition ? InferContentType<S['contentTypes'][K & keyof S['contentTypes']], S> : BaseContent;
1880
+ /** The fields a singleton write sends: some of what the singleton declares, or any. */
1881
+ type SingletonData<S, K> = S extends AnySchemaDefinition ? Partial<Omit<InferSingleton<S['singletonContentTypes'][K & keyof S['singletonContentTypes']], S>, '_metadata'>> : {
1882
+ readonly [field: string]: unknown;
1883
+ };
1884
+ /** Where a written entry sits: every write may move it, as the single writes do. */
1885
+ interface Placement {
1886
+ readonly slug?: string;
1887
+ readonly parentId?: string;
1888
+ readonly sortOrder?: number;
1889
+ }
1890
+ /**
1891
+ * Which entry an update or replace writes: the entry you read — checked, in the locale it was read
1892
+ * in, so it takes no `locale` — or an id, unchecked, in the locale it names or the client's.
1893
+ */
1894
+ type BatchTarget<L extends string> = {
1895
+ readonly entry: ReadEntry;
1896
+ readonly id?: never;
1897
+ readonly locale?: never;
1898
+ } | {
1899
+ readonly id: string;
1900
+ readonly entry?: never;
1901
+ readonly locale?: L;
1902
+ };
1903
+ /** Which entry a delete removes: the entry you read, checked, or an id, unchecked. */
1904
+ type BatchDeleteTarget = {
1905
+ readonly entry: ReadEntry;
1906
+ readonly id?: never;
1907
+ } | {
1908
+ readonly id: string;
1909
+ readonly entry?: never;
1910
+ };
1911
+ /** The verbs an operation is named by, exactly one per operation. */
1912
+ declare const VERBS: readonly ["create", "update", "replace", "delete", "singleton"];
1913
+ type Verb = (typeof VERBS)[number];
1914
+ /**
1915
+ * Every verb but `V`, absent. Each operation then names all five, so the one present is the union's
1916
+ * discriminant and a mistake is reported against the operation it was meant as.
1917
+ */
1918
+ type Only<V extends Verb> = {
1919
+ readonly [Other in Exclude<Verb, V>]?: never;
1920
+ };
1921
+ /** A create of content type `K`. */
1922
+ type BatchCreate<K extends string, T extends BaseContent, L extends string> = Placement & Only<'create'> & {
1923
+ readonly create: K;
1924
+ readonly data: CreateInput<T>;
1925
+ readonly locale?: L;
1926
+ };
1927
+ /** An update of an entry of content type `K`: its fields merged into the entry. */
1928
+ type BatchUpdate<K extends string, T extends BaseContent, L extends string> = Placement & BatchTarget<L> & Only<'update'> & {
1929
+ readonly update: K;
1930
+ readonly data: UpdateInput<T>;
1931
+ };
1932
+ /** A replace of an entry of content type `K`: fields left out are cleared in that locale. */
1933
+ type BatchReplace<K extends string, T extends BaseContent, L extends string> = Placement & BatchTarget<L> & Only<'replace'> & {
1934
+ readonly replace: K;
1935
+ readonly data: CreateInput<T>;
1936
+ };
1937
+ /** A delete of an entry of content type `K`. */
1938
+ type BatchDelete<K extends string> = BatchDeleteTarget & Only<'delete'> & {
1939
+ readonly delete: K;
1940
+ };
1941
+ /** A write to singleton `K`, which creates it the first time. */
1942
+ type BatchSingletonWrite<K extends string, D, L extends string> = Only<'singleton'> & {
1943
+ readonly singleton: K;
1944
+ readonly data: D;
1945
+ readonly locale?: L;
1946
+ };
1947
+ /**
1948
+ * One operation of `client.batch(operations)`, each content type's `data` typed by what it declares
1949
+ * where a schema types the client.
1950
+ */
1951
+ type BatchWrite<S extends AnySchemaDefinition | undefined = undefined> = {
1952
+ [K in ContentTypeName<S>]: BatchCreate<K, EntryOf$1<S, K>, LocaleName<S>> | BatchUpdate<K, EntryOf$1<S, K>, LocaleName<S>> | BatchReplace<K, EntryOf$1<S, K>, LocaleName<S>> | BatchDelete<K>;
1953
+ }[ContentTypeName<S>] | {
1954
+ [K in SingletonName<S>]: BatchSingletonWrite<K, SingletonData<S, K>, LocaleName<S>>;
1955
+ }[SingletonName<S>];
1956
+ /** How a batch runs. Each is the API's default when left out. */
1957
+ interface BatchWriteOptions {
1958
+ /**
1959
+ * One transaction: every operation lands or none does. The API takes it only when every operation
1960
+ * is the same kind of write and none creates a singleton (`REQUEST_INVALID` otherwise).
1961
+ */
1962
+ readonly atomic?: boolean;
1963
+ /** Run every check a real write runs, and write nothing. */
1964
+ readonly dryRun?: boolean;
1965
+ /** Stop at the first operation that fails; the rest are answered `skipped`. Not with `atomic`. */
1966
+ readonly stopOnError?: boolean;
1967
+ /** `false` answers each written entry by id alone instead of reading it back. */
1968
+ readonly returnData?: boolean;
1969
+ /** The dataset to write, by id or name, over the client's. */
1970
+ readonly dataset?: string;
1971
+ }
1972
+ /**
1973
+ * One batch item, as the committed document describes the `multi` kind: an `outcome` —
1974
+ * `created`, `updated`, `unchanged`, `duplicate` or `skipped` — or an `error` naming the refusal.
1975
+ */
1976
+ type BatchOperationResult = components['schemas']['MultiPayload']['results'][number];
1977
+ /** A batch's counts, from the contract. */
1978
+ type BatchSummary = components['schemas']['MultiSummary'];
1979
+ /** The batch payload: `{ results, summary }`. */
1980
+ type BatchResponse = components['schemas']['MultiPayload'];
1981
+ /** What `client.batch(operations)` answers: the API's body, `{ data: { results, summary } }`. */
1982
+ type BatchBody = Wire<'/v1/batch', 'post', BatchResponse>;
1983
+
1984
+ /**
1985
+ * Ad-hoc and named queries as single calls: `client.query(…)` (REL-2421; REL-2409 decision Q22).
1986
+ *
1987
+ * - `client.query({ from, … })` runs a query inline. It takes `list`'s option names, typed from the
1988
+ * same option types — `include`, `select`, `sort`, `limit`, `page`, `locale`, `dataset`, `fresh` —
1989
+ * plus its own: `aggregate`, `params` and `fullText`. `where` is `list`'s, typed as `list` types
1990
+ * it (`{ views: { $gte: 10 } }`): the query route reads the `where` a list takes (REL-2439,
1991
+ * REL-2409 decision D11), and refuses the bare form a saved query used to declare.
1992
+ * - `client.query(name, { params, … })` runs the saved query the schema declares under `name`, by
1993
+ * that name alone (`POST /v1/queries/{name}`, D9). Its parameters are typed from the declaration
1994
+ * ({@link InferQueryParams}): a name it does not declare does not compile, and the API refuses it
1995
+ * too, naming it.
1996
+ *
1997
+ * Both answer the API's body, `{ data: { result, metadata } }`.
1998
+ *
1999
+ * The query route pages by `limit` and `offset` and answers no cursor, so a query pages by number:
2000
+ * `page` with `limit`, sent as the offset that page starts at. There is no `cursor`.
2001
+ *
2002
+ */
2003
+
2004
+ /** The content types a query reads: the schema's, or any where no schema types the client. */
2005
+ type QueryTypeName<S> = S extends AnySchemaDefinition ? SchemaContentTypeNames<S> : string;
2006
+ /** An entry of content type `K`, as the schema declares it. */
2007
+ type EntryOf<S, K> = S extends AnySchemaDefinition ? InferContentType<S['contentTypes'][K & keyof S['contentTypes']], S> : BaseContent;
2008
+ /** A read of content type `K`, which checks its include paths and names its locales. */
2009
+ type ReadingOf<S, K> = S extends AnySchemaDefinition ? ContentReading<S['contentTypes'][K & keyof S['contentTypes']], S> : UntypedReading<BaseContent>;
2010
+ /** The locale codes a query may name: the schema's, or any. */
2011
+ type QueryLocale<S> = S extends AnySchemaDefinition ? SchemaLocaleCodes<S> : string;
2012
+ /** The saved queries a client may run by name: the schema's, or any. */
2013
+ type QueryName<S> = S extends AnySchemaDefinition ? SchemaQueryNames<S> : string;
2014
+ /** The parameters saved query `N` declares; any, where no schema types the client. */
2015
+ type NamedQueryParams<S, N> = S extends AnySchemaDefinition ? InferQueryParams<S['queries'][N & keyof S['queries']]> : AnyQueryParameters;
2016
+ /**
2017
+ * What an ad-hoc query computes over the entries it fetched: the groups and metrics of the query
2018
+ * route's `aggregate` block. Its source is `from`.
2019
+ */
2020
+ type QueryAggregate = Omit<AggregateBlock, 'source'>;
2021
+ /** How a query pages: by number, with `limit` its size — or not at all. */
2022
+ type QueryPaging<T extends BaseContent, R extends Reading> = {
2023
+ readonly page?: never;
2024
+ } | {
2025
+ /** Which page, from 1: sent as the offset it starts at, `(page - 1) * limit`. */
2026
+ readonly page: number;
2027
+ readonly limit: NonNullable<ListReadOptions<T, R, []>['limit']>;
2028
+ };
2029
+ /** `select` or `include`: the route refuses an include path beside a selection. */
2030
+ type SelectOrInclude<R extends Reading, P extends readonly string[], Sel> = {
2031
+ readonly select?: Sel;
2032
+ readonly include?: undefined;
2033
+ } | {
2034
+ readonly select?: undefined;
2035
+ readonly include?: CheckedIncludes<R, P>;
2036
+ };
2037
+ /**
2038
+ * What `client.query({ from, … })` takes: `list`'s options, typed as `list` types them, and the
2039
+ * query's own.
2040
+ */
2041
+ type AdhocQuery<K extends string, T extends BaseContent, R extends Reading, P extends readonly string[], Sel extends readonly FieldKey<T>[] | undefined, A extends QueryAggregate | undefined> = {
2042
+ /** The content type the query reads. */
2043
+ readonly from: K;
2044
+ /**
2045
+ * The conditions an entry must meet: the `where` `list` takes, typed as `list` types it — a value,
2046
+ * a list, or `$`-prefixed operators (`{ views: { $gte: 10 } }`). A condition on `_metadata` is
2047
+ * refused before anything is sent.
2048
+ */
2049
+ readonly where?: ListReadOptions<T, R, P>['where'];
2050
+ /** The order, by Field or system date, the key order the priority. */
2051
+ readonly sort?: SortOption<T>;
2052
+ /** How many entries the query answers — a page's size where `page` names one. */
2053
+ readonly limit?: ListReadOptions<T, R, P>['limit'];
2054
+ /** The one locale the entries are composed in, over the client's. */
2055
+ readonly locale?: ReadLocale<R>;
2056
+ /** The dataset to read, by id or name, over the client's. */
2057
+ readonly dataset?: string;
2058
+ /** How fresh the answer must be: a feed position, or `true` for past every cache. */
2059
+ readonly fresh?: FreshOption;
2060
+ /** Groups and metrics computed over the entries fetched; the answer is then their rows. */
2061
+ readonly aggregate?: A;
2062
+ /** Values for the `${param}` expressions the query writes. */
2063
+ readonly params?: {
2064
+ readonly [name: string]: QueryParameterValue;
2065
+ };
2066
+ /** A text search over the Fields it names, beside the fetch. */
2067
+ readonly fullText?: FullTextSearch;
2068
+ } & QueryPaging<T, R> & SelectOrInclude<R, P, Sel>;
2069
+ /** What `client.query(name, options)` takes beside the parameters. */
2070
+ interface NamedQueryReadOptions<L extends string> {
2071
+ /** The one locale the query composes in, over the client's. */
2072
+ readonly locale?: L;
2073
+ /** The dataset to read, by id or name, over the client's. */
2074
+ readonly dataset?: string;
2075
+ /**
2076
+ * `true` reads past every cache. The route is not edge-kept, so a feed position has no cached
2077
+ * answer to be fresher than and is not offered.
2078
+ */
2079
+ readonly fresh?: true;
2080
+ }
2081
+ /** What `client.query(name, options)` takes: `params` required where the query requires one. */
2082
+ type NamedQueryOptions<Params, L extends string> = NamedQueryReadOptions<L> & (Record<never, never> extends Params ? {
2083
+ readonly params?: Params;
2084
+ } : {
2085
+ readonly params: Params;
2086
+ });
2087
+ /** The options argument, which may be left out only where no parameter is required. */
2088
+ type NamedQueryArgs<Params, L extends string> = Record<never, never> extends Params ? [options?: NamedQueryOptions<Params, L>] : [options: NamedQueryOptions<Params, L>];
2089
+ /**
2090
+ * One metric, as the Query DSL writes it: `<field>.<fn>`, where `fn` is `count`, `sum`, `avg`,
2091
+ * `min`, `max`, `first` or `last` — `'id.count'`, `'pricing.basePrice.avg'`. The service validates
2092
+ * every metric against that form (`INVALID_AGGREGATION_EXPRESSION`); `count` ignores the field it
2093
+ * names (REL-1470).
2094
+ */
2095
+ type AggregateMetric = `${string}.${'count' | 'sum' | 'avg' | 'min' | 'max' | 'first' | 'last'}`;
2096
+ /** How a query ran, as the query routes report it beside the records. */
2097
+ interface QueryMetadata {
2098
+ /** Query identifier */
2099
+ queryId: string;
2100
+ /** Execution time in milliseconds */
2101
+ executionTimeMs: number;
2102
+ /** Number of results */
2103
+ resultCount: number;
2104
+ /** ISO timestamp */
2105
+ timestamp: string;
2106
+ /** Whether result was from cache */
2107
+ cacheHit?: boolean;
2108
+ /** Generated pipeline (if debug enabled) */
2109
+ pipeline?: unknown[];
2110
+ /** Resolved parameters (if debug enabled) */
2111
+ parameters?: Record<string, unknown>;
2112
+ /** Validation warnings */
2113
+ warnings?: string[];
2114
+ }
2115
+ /** The API's body for a query: the records it produced and how it ran. */
2116
+ interface QueryAnswer<T> {
2117
+ readonly data: {
2118
+ /** The records the query produced. */
2119
+ readonly result: T;
2120
+ readonly metadata: QueryMetadata;
2121
+ };
2122
+ }
2123
+ /**
2124
+ * One record of an ad-hoc query: an aggregate's row, the selected Fields of an entry, or the entry
2125
+ * as its include paths read it.
2126
+ */
2127
+ type AdhocRecord<R extends Reading, P extends readonly string[], Sel, A> = A extends QueryAggregate ? Readonly<Record<string, unknown>> : Sel extends readonly string[] ? Pick<ReadWith<R, P[number]>, Extract<keyof ReadWith<R, P[number]>, Sel[number]>> : ReadWith<R, P[number]>;
2128
+ /**
2129
+ * `client.query`: callable two ways, beside the chained client it was before.
2130
+ *
2131
+ * Each way is read off a method (`['run']`), so its parameters compare as a method's do: a client
2132
+ * typed by one schema stays assignable to one typed by a wider one, as its other methods let it be.
2133
+ */
2134
+ type QueryCall<S extends AnySchemaDefinition | undefined> = AdhocQueryCall<S> & NamedQueryCall<S>;
2135
+ /** `client.query({ from, … })`. */
2136
+ type AdhocQueryCall<S extends AnySchemaDefinition | undefined> = {
2137
+ /**
2138
+ * Run a query inline: `list`'s options plus `aggregate`, `params` and `fullText`. Answers the
2139
+ * API's body, its `result` the entries — or the aggregate's rows.
2140
+ *
2141
+ * @example
2142
+ * ```typescript
2143
+ * const { data } = await relyio.query({
2144
+ * from: 'post',
2145
+ * where: { views: { $gte: 100 } },
2146
+ * select: ['title', 'views'],
2147
+ * sort: { views: 'desc' },
2148
+ * limit: 10,
2149
+ * page: 2,
2150
+ * })
2151
+ * ```
2152
+ */
2153
+ run<K extends QueryTypeName<S>, const P extends readonly string[] = [], const Sel extends readonly FieldKey<EntryOf<S, K>>[] | undefined = undefined, const A extends QueryAggregate | undefined = undefined>(query: AdhocQuery<K, EntryOf<S, K>, ReadingOf<S, K>, P, Sel, A>): Promise<QueryAnswer<AdhocRecord<ReadingOf<S, K>, P, Sel, A>[]>>;
2154
+ }['run'];
2155
+ /** `client.query(name, { params, … })`. */
2156
+ type NamedQueryCall<S extends AnySchemaDefinition | undefined> = {
2157
+ /**
2158
+ * Run the saved query the deployed schema declares under `name`, with the parameters it declares
2159
+ * (`POST /v1/queries/{name}`, D9). Answers the API's body.
2160
+ *
2161
+ * It needs only the name: a client created without a schema runs it too, its parameters untyped.
2162
+ * A schema the client is typed by (`createClient({ schema })`, or `Register`) types the name and
2163
+ * the parameters. The API refuses a parameter the query does not declare, naming it.
2164
+ *
2165
+ * @example
2166
+ * ```typescript
2167
+ * const { data } = await relyio.query('postsByTag', { params: { tag: 'birds' } })
2168
+ * ```
2169
+ */
2170
+ run<N extends QueryName<S>>(name: N, ...options: NamedQueryArgs<NamedQueryParams<S, N>, QueryLocale<S>>): Promise<QueryAnswer<unknown>>;
2171
+ }['run'];
2172
+
2173
+ /**
2174
+ * Content and resource errors.
2175
+ *
2176
+ * `ContentTypeNotFoundError` and `ContentNotFoundError` were deleted at API5.
2177
+ * The mapper reached them by sniffing `CONTENT_TYPE_NOT_FOUND` and
2178
+ * `CONTENT_NOT_FOUND`, neither of which is in the closed vocabulary, so neither
2179
+ * class was reachable. Both cases are `RESOURCE_NOT_FOUND`, whose declared
2180
+ * `details` are `{ kind, id? }` — the same two facts, from the one code a route
2181
+ * can actually declare.
2182
+ */
2183
+ declare class ResourceNotFoundError extends SDKError {
2184
+ readonly code = "RESOURCE_NOT_FOUND";
2185
+ readonly retryable = false;
2186
+ constructor(message?: string, statusCode?: number, cause?: Error, context?: Record<string, unknown>);
2187
+ getUserMessage(): string;
2188
+ }
2189
+ declare class ResourceConflictError extends SDKError {
2190
+ readonly code = "RESOURCE_CONFLICT";
2191
+ readonly retryable = false;
2192
+ constructor(message?: string, statusCode?: number, cause?: Error, context?: Record<string, unknown>);
2193
+ getUserMessage(): string;
2194
+ }
2195
+ declare class ResourceExpiredError extends SDKError {
2196
+ readonly code = "RESOURCE_EXPIRED";
2197
+ readonly retryable = false;
2198
+ constructor(message?: string, statusCode?: number, cause?: Error, context?: Record<string, unknown>);
2199
+ getUserMessage(): string;
2200
+ }
2201
+ /**
2202
+ * "Did the service say this resource does not exist?"
2203
+ *
2204
+ * Reads the **code**, not the status. Four contract codes are 4xx on a read and
2205
+ * only one of them means "not there"; the incumbent checked `statusCode === 404`
2206
+ * on the thrown value, and a second arm duck-typed a plain object for the same
2207
+ * number so a hand-written test double would pass. Both are the branch
2208
+ * `INV-API-CLIENT-IS-DERIVED` forbids where a code exists — and the second one
2209
+ * only existed because the tests faked the boundary (rule 20). They now stub
2210
+ * `fetch`, so the double is gone with the branch.
2211
+ */
2212
+ declare function isResourceNotFound(error: unknown): boolean;
2213
+ /**
2214
+ * "Did the service say the **entry** does not exist?" — and not its content type, its schema or its
2215
+ * dataset (REL-2415, REL-2409 decision Q12).
2216
+ *
2217
+ * `RESOURCE_NOT_FOUND` carries `details.kind`, which the mapper keeps as `context.resourceType`. A
2218
+ * read that answered `null` for every kind made a content type nobody deployed look like a missing
2219
+ * page, so `get` answers `null` only for this one and throws the rest as they arrived.
2220
+ */
2221
+ declare function isMissingEntry(error: unknown): boolean;
2222
+
2223
+ /**
2224
+ * `relyio.changes()` — the changes feed as a typed long-poll iterator
2225
+ * (`sdk-surfaces-spec.md` decisions 16–18).
2226
+ *
2227
+ * Long-poll only, on purpose. `GET /v1/changes` is an ordinary cursor-paged
2228
+ * collection the committed document declares, so each poll is one `send` over
2229
+ * the one HTTP client (`INV-SS-ONE-CORE`, `INV-API-CLIENT-IS-DERIVED`). The SSE
2230
+ * branch the route also serves would be a second transport path inside that
2231
+ * client, and `parseResponse` buffers a whole body; it stays for `EventSource`
2232
+ * and curl consumers.
2233
+ */
2234
+
2235
+ type PollChanges = paths['/v1/changes']['get'];
2236
+ /**
2237
+ * The 200 body the document declares, read from its `application/json` content
2238
+ * **by name**. The route also negotiates `text/event-stream`; naming the JSON
2239
+ * content is what keeps that alternate from changing this type when the
2240
+ * document gains it.
2241
+ */
2242
+ type ChangesPageBody = PollChanges['responses'][200]['content']['application/json'];
2243
+ /**
2244
+ * One change, as the committed document describes an item of the feed's `data`
2245
+ * (`INV-SS-CHANGE-IS-IDS`: ids, content type, locale, kind and cursor — never a
2246
+ * document).
2247
+ *
2248
+ * Read from the document, never written here: the api's route row names the
2249
+ * `Change` component schema, and a change to it reaches this type when the
2250
+ * document is regenerated. `changes.types.ts` holds it equal to the document's
2251
+ * item, and fails if it is ever `unknown` again.
2252
+ */
2253
+ type Change = ChangesPageBody['data'][number];
2254
+ /**
2255
+ * A change as `relyio.changes()` and `relyio.liveChanges()` deliver it: the document's
2256
+ * {@link Change}, and `fresh`.
2257
+ *
2258
+ * `fresh` is the feed position a refetch for this change names, as the one query term the API
2259
+ * declares on every read its edge may keep (`?_fresh`, `sdk-surfaces-spec.md` decision 32): hand it
2260
+ * to the read as its `fresh` option — `get(id, { fresh: change.fresh })`, `list({ fresh })`,
2261
+ * `search(q, { fresh })`, a singleton's `get({ fresh })`. A position no request named before the change is a key no cache holds a
2262
+ * body under, so the read is answered after the change — at the edge and in the browser, whether
2263
+ * or not the change's purge has reached them — and every later read naming it shares that answer.
2264
+ *
2265
+ * It is at or after the change, never before: on `/v1/changes` the change's own cursor, the ledger
2266
+ * row it came from; at the live edge the cursor of the batch it arrived in. It is not always
2267
+ * `cursor`: at the edge an earlier change of a batch carries the position **before** the batch as
2268
+ * its `cursor`, so a failure part-way through replays the batch, and a refetch naming that position
2269
+ * would be answered from before the change.
2270
+ */
2271
+ type FeedChange = Change & {
2272
+ /** The feed position a refetch for this change names (`?_fresh`); at or after the change. */
2273
+ readonly fresh: string;
2274
+ };
2275
+ /**
2276
+ * Why a cursor could no longer be replayed, or why everything must be refetched. `/v1/changes`
2277
+ * names `retention` and `dataset-replaced`; the live edge's `feed.resync` may name any reason its
2278
+ * wire declares (`VisitorResyncReason` in `@relyio/api-contract`, read from there rather than
2279
+ * restated). `unknown` when the message named none.
2280
+ */
2281
+ type ChangesExpiredReason = VisitorResyncReason | 'unknown';
2282
+ /**
2283
+ * `RESOURCE_EXPIRED` from the changes feed: the cursor names a position the
2284
+ * ledger can no longer replay from (`INV-SS-CURSOR-REPLAYS`). The consumer
2285
+ * resyncs from the read path and starts again without a cursor.
2286
+ *
2287
+ * A `ResourceExpiredError`, so code that already catches that still does. The
2288
+ * refusal declares no details, so `reason` is read from the message the api
2289
+ * writes (`apps/services/api/src/changes/changes-request.ts` `refusalOf`).
2290
+ *
2291
+ * The live edge's `feed.resync` — a socket's frame or a poll's answer — is the
2292
+ * same instruction and arrives as the same error ({@link ChangesExpiredError.resync}),
2293
+ * so `onExpired` means one thing on both transports.
2294
+ */
2295
+ declare class ChangesExpiredError extends ResourceExpiredError {
2296
+ readonly reason: ChangesExpiredReason;
2297
+ /**
2298
+ * The position the feed resynced to, which a refetch of everything names as `?_fresh`, as a
2299
+ * change's {@link FeedChange.fresh} is named: the head a `/v1/changes` resync took, or the cursor
2300
+ * the edge's `feed.resync` sent. `undefined` when there was no resync to take one — the error was
2301
+ * thrown (`onExpired: 'throw'`) — and on an error a caller built itself.
2302
+ */
2303
+ readonly fresh: string | undefined;
2304
+ /** The base class's signature, so `withContext` can rebuild one; then the position resynced to. */
2305
+ constructor(message: string, statusCode?: number, cause?: Error, context?: Record<string, unknown>, fresh?: string);
2306
+ /** The base class rebuilds from its four arguments, which would drop the position resynced to. */
2307
+ withContext(context: Record<string, unknown>): this;
2308
+ /** `/v1/changes`' refusal, with the head the feed resynced to when it did. */
2309
+ static of(expired: ResourceExpiredError, fresh?: string): ChangesExpiredError;
2310
+ /** The live edge's `feed.resync`: refetch everything, then continue from the cursor it sent. */
2311
+ static resync(reason: VisitorResyncReason, cursor: string): ChangesExpiredError;
2312
+ }
2313
+ interface ChangesOptions {
2314
+ /**
2315
+ * A `page.nextCursor` — or a change's `cursor` — from an earlier poll. Omitted,
2316
+ * the first poll answers the dataset's current position and no changes, at
2317
+ * once, and the feed starts from there.
2318
+ */
2319
+ since?: string;
2320
+ /** Content types to keep. Omitted keeps every type; an empty list is refused (400). */
2321
+ types?: readonly string[];
2322
+ /**
2323
+ * Entry ids to keep. Omitted keeps every entry. The api refuses more than it
2324
+ * allows (100 today) as `REQUEST_INVALID`, thrown from the iterator as an
2325
+ * `InvalidPayloadError` on the first poll; the SDK does not restate the
2326
+ * cap, so it cannot drift from the service's.
2327
+ */
2328
+ ids?: readonly string[];
2329
+ /** Ledger rows one poll scans, 1 to 500; the api defaults to 100. */
2330
+ limit?: number;
2331
+ /**
2332
+ * Ends the iteration. An abort — in flight, between polls, or while waiting
2333
+ * out a `Retry-After` — makes the `for await` **complete**, with no further
2334
+ * request and no `AbortError` thrown into the loop.
2335
+ */
2336
+ signal?: AbortSignal;
2337
+ /**
2338
+ * What to do when the cursor has expired.
2339
+ *
2340
+ * - `'throw'` (default) — the iterator throws {@link ChangesExpiredError}.
2341
+ * - `'resync'` — restart from the dataset's current position and keep going.
2342
+ * - a function — resync, and call it with the error once the new position is
2343
+ * taken and before any change after it is yielded. This is the observable
2344
+ * form: whatever the consumer derived from the feed may be stale, so the
2345
+ * function is where it invalidates everything — naming the error's `fresh`,
2346
+ * the new position, on every refetch. The iterator waits for it.
2347
+ */
2348
+ onExpired?: 'throw' | 'resync' | ((expired: ChangesExpiredError) => void | Promise<void>);
2349
+ /**
2350
+ * Called once, when the feed was opened without `since`, with the position it starts from: the
2351
+ * first poll's answer, the dataset's head — before the next poll is sent. Read what you show
2352
+ * **after** it and a write your read did not see is after the position, so this feed delivers it,
2353
+ * with no second request to take the position first (`changesHead()` is that request) (REL-1513).
2354
+ *
2355
+ * `followChanges()` passes it (`ChangeSourceOptions.reached`), so a feed that drops before its first
2356
+ * change reopens from there rather than re-reading everything. Never called with a later empty
2357
+ * page's cursor: filtered, that cursor is past rows the filter hid, and a caller that widens the
2358
+ * filter must not reopen from it.
2359
+ */
2360
+ reached?: (cursor: string) => void;
2361
+ }
2362
+ interface ChangesHeadOptions {
2363
+ /** Cancels the request; the promise then rejects. */
2364
+ signal?: AbortSignal;
2365
+ }
2366
+
2367
+ /**
2368
+ * `relyio.liveChanges()` — a dataset's published changes at RelyIO's live edge, for a publishable key
2369
+ * (`live-layer-spec.md` decision 20; LV5.2, REL-1158).
2370
+ *
2371
+ * The wire is `@relyio/api-contract`'s `live-visitor.ts`, read from there and never restated: the
2372
+ * path and query names, the frames and their reader, the keepalive, the poll's answer and its reader,
2373
+ * the poll interval, and what each close code and each refused poll means a client does next.
2374
+ *
2375
+ * **One open, two ways in.** A subscription opens a WebSocket to the edge. Where it cannot — the
2376
+ * runtime has no `WebSocket` (Node 18 and 20), constructing one throws, the socket fails before it
2377
+ * opens (a network that blocks sockets), or it has not opened and been caught up within
2378
+ * {@link LIVE_SOCKET_OPEN_TIMEOUT_MS} (a network that holds it) — it polls the edge instead, from the
2379
+ * same cursor, about every {@link VISITOR_POLL_INTERVAL_MS}, and tries a socket again about once a
2380
+ * minute. Both carry one cursor space, so a switch either way loses nothing.
2381
+ *
2382
+ * **Where it starts.** Without `since`, the edge answers the dataset's current position before
2383
+ * anything else — the socket's `feed.opened`, or the first poll's answer — and the subscription hands
2384
+ * it to `reached`. That call is the position, with no request of its own for it: a read sent after it
2385
+ * is answered at or after the position, and every change after it is delivered here, so a publish the
2386
+ * read did not see is announced (REL-1513). The edge counts the subscription as one live connection,
2387
+ * whichever way in it took (`INV-LV-LIVE-IS-METERED`).
2388
+ *
2389
+ * **It ends only when its signal aborts, and throws every other end** — a socket closed by a refusal
2390
+ * or a deploy, a refused poll, a keepalive nobody answered — as a `LiveEdgeError` saying what to do
2391
+ * next, so `followChanges()` reconnects, backs off or stops by the edge's own tables. That loop is
2392
+ * the one reconnect policy (`INV-SS-ONE-CORE`, REL-1215); this module keeps no back-off of its own.
2393
+ *
2394
+ * **Cursors.** The edge sends one cursor after a batch of changes. The last change of a batch carries
2395
+ * it; each earlier one carries the position before the batch, so a failure part-way through a batch
2396
+ * replays the whole batch rather than skipping its rest. Delivery is at-least-once: a replay and the
2397
+ * live frames after it may name one change twice, and a refetch makes a repeat cost a read, never a
2398
+ * wrong view (`INV-SS-CHANGE-IS-IDS`).
2399
+ *
2400
+ * **What a refetch names.** Every change of a batch carries the batch's own cursor as `fresh`
2401
+ * (`FeedChange`), the position its refetch names as `?_fresh` — not `cursor`, which for an earlier
2402
+ * change is the position before the batch: the previous batch's refetches were answered under that
2403
+ * one, before this batch, and a cache would hand the same answer back. A `feed.resync`'s error
2404
+ * carries the cursor it sent the same way.
2405
+ */
2406
+
2407
+ /**
2408
+ * How often an open socket sends the keepalive, which the edge's runtime answers without waking the
2409
+ * feed — the Studio's dataset room sends its own on the same interval. A socket that has heard
2410
+ * nothing by the next one, not even the answer, is presumed dead and reconnected: a client that only
2411
+ * receives never learns otherwise that a network change dropped its connection.
2412
+ */
2413
+ declare const LIVE_KEEPALIVE_INTERVAL_MS = 30000;
2414
+ /**
2415
+ * How long a socket has to open and be caught up — the feed's `feed.opened` — before the subscription
2416
+ * stops waiting on it, closes it and polls instead, as it does for a socket that fails before it opens
2417
+ * (REL-1513).
2418
+ *
2419
+ * The poll interval, because that is what a socket is for: telling the page sooner than a poll would.
2420
+ * A poll is answered at once and polled again within this interval, so a socket that has not caught
2421
+ * up by then has already served the page worse than polling would have. It is well past a healthy
2422
+ * open — the door's checks and one ledger read at the edge — and half the keepalive's verdict on an
2423
+ * open socket that has gone quiet ({@link LIVE_KEEPALIVE_INTERVAL_MS}, twice). It bounds how long
2424
+ * `<RelyIOProvider live>`'s first reads wait for a position on a network that neither opens a socket
2425
+ * nor refuses one — a proxy that holds the upgrade — which without it they would wait on for as long
2426
+ * as the page is open.
2427
+ */
2428
+ declare const LIVE_SOCKET_OPEN_TIMEOUT_MS = 15000;
2429
+ interface LiveChangesOptions {
2430
+ /**
2431
+ * A change's cursor from an earlier subscription. Omitted, the edge answers the dataset's current
2432
+ * position first and replays nothing, and the subscription hands that position to `reached` before
2433
+ * anything after it. Read what you show **after** that first call — a read sent before it, from a
2434
+ * subscription opened afterwards without `since`, leaves a publish between the two that neither
2435
+ * sees (REL-1513). With `followChanges()`, which hands the source its own `reached`, call both —
2436
+ * and let the read go if the loop fails first, so content never waits on a feed that is down:
2437
+ *
2438
+ * ```ts
2439
+ * let positioned!: () => void
2440
+ * const position = new Promise<void>(resolve => (positioned = resolve))
2441
+ * const following = followChanges(
2442
+ * from =>
2443
+ * relyio.liveChanges({
2444
+ * ...from,
2445
+ * ids: [id],
2446
+ * reached: cursor => {
2447
+ * from.reached(cursor)
2448
+ * positioned()
2449
+ * },
2450
+ * }),
2451
+ * { signal, onChange, onResync, onState: state => state.kind !== 'following' && positioned() },
2452
+ * )
2453
+ * await position
2454
+ * const post = await relyio.content('post').get(id)
2455
+ * ```
2456
+ *
2457
+ * The wait is bounded: a socket that has not opened and been told its position within
2458
+ * {@link LIVE_SOCKET_OPEN_TIMEOUT_MS} is given up for a poll, answered at once.
2459
+ */
2460
+ since?: string;
2461
+ /** Content types to keep. Omitted keeps every type; an empty list is refused. At most 50. */
2462
+ types?: readonly string[];
2463
+ /** Entry ids to keep. Omitted keeps every entry; an empty list is refused. At most 100. */
2464
+ ids?: readonly string[];
2465
+ /**
2466
+ * Ends the subscription. An abort — while connected, while polling, or between polls — makes the
2467
+ * `for await` **complete**, with the socket closed, no further request and nothing thrown.
2468
+ */
2469
+ signal?: AbortSignal;
2470
+ /**
2471
+ * What to do when the edge says to refetch everything (`feed.resync`: the cursor is too old, was
2472
+ * never this dataset's, or the dataset was replaced) — as `relyio.changes()` takes it: `'throw'`
2473
+ * (default) throws a `ChangesExpiredError`, `'resync'` carries on from the edge's position, and a
2474
+ * function is awaited with the error before any change after that position is yielded.
2475
+ */
2476
+ onExpired?: ChangesOptions['onExpired'];
2477
+ /**
2478
+ * Called when the edge has the subscription in hand — its `feed.opened` frame, or an answered
2479
+ * poll. `followChanges()` passes it to reset its back-off; see `ChangeSourceOptions.opened`.
2480
+ */
2481
+ opened?: () => void;
2482
+ /**
2483
+ * Called with a position reached without a change to carry it. `followChanges()` passes it so a
2484
+ * connection dropped before its first change reconnects from there; see
2485
+ * `ChangeSourceOptions.reached`. Without `since`, its first call is the position the subscription
2486
+ * starts from (see `since`).
2487
+ */
2488
+ reached?: (cursor: string) => void;
2489
+ }
2490
+
2491
+ /**
2492
+ * The key's project, as `GET /v1/project` describes it: organization, project, dataset and branch,
2493
+ * each by id and name, and `studioUrl` — the Studio root the preview toolbar deep-links from
2494
+ * (`sdk-surfaces-spec.md` decision 24), or `null` when the service has no Dashboard address.
2495
+ *
2496
+ * Read from the committed document's `application/json` content by name, never written here
2497
+ * (`INV-API-CLIENT-IS-DERIVED`); `derived-transport.types.ts` holds the two equal.
2498
+ */
2499
+ type Project = paths['/v1/project']['get']['responses'][200]['content']['application/json']['data'];
2500
+ /** What `health()` resolves: the API answered and reaches every store it reads. */
2501
+ interface Health {
2502
+ readonly status: 'ok';
2503
+ /** When the API answered, as an ISO 8601 string. */
2504
+ readonly timestamp: string;
2505
+ }
2506
+ /** One locale `locales()` reports, as the deployed schema declares it. */
2507
+ interface DeployedLocale {
2508
+ /** BCP 47 code, as a read's `locale` takes it. */
2509
+ readonly code: string;
2510
+ readonly displayName: string;
2511
+ /** Whether this is the default locale. Exactly one locale is. */
2512
+ readonly default: boolean;
2513
+ /** The locales a read in this one falls back to, in order. */
2514
+ readonly fallback?: string[];
2515
+ readonly direction?: 'ltr' | 'rtl';
2516
+ /** `false` when the locale is declared and switched off. */
2517
+ readonly active?: boolean;
2518
+ }
2519
+ /** One type `contentTypes()` reports. */
2520
+ interface DeployedContentType {
2521
+ /** What `content()` or `singleton()` takes. */
2522
+ readonly id: string;
2523
+ /** The type's `displayName`. */
2524
+ readonly name: string;
2525
+ readonly type: 'content' | 'singleton';
2526
+ }
2527
+ /**
2528
+ * The defaults `client.with()` gives the client it makes (REL-2417, Q9): the locale and the dataset
2529
+ * its reads and writes are in unless a call names its own. A locale is one the schema declares.
2530
+ */
2531
+ interface ClientDefaults<TSchema extends AnySchemaDefinition | undefined = undefined> {
2532
+ readonly locale?: LocaleCodeOf<TSchema>;
2533
+ readonly dataset?: string;
2534
+ }
2535
+ /** The locale codes a client's reads may name: the schema's, or any where no schema types it. */
2536
+ type LocaleCodeOf<TSchema extends AnySchemaDefinition | undefined> = TSchema extends AnySchemaDefinition ? SchemaLocaleCodes<TSchema> : string;
2537
+ /**
2538
+ * Client configuration with optional schema
2539
+ */
2540
+ interface TypedClientConfig<TSchema extends AnySchemaDefinition | undefined = undefined> extends RelyIOConfig {
2541
+ /** Schema definition for type inference (optional - omit for dynamic/untyped usage) */
2542
+ schema?: TSchema;
2543
+ }
2544
+ /**
2545
+ * Typed RelyIO client with full schema awareness
2546
+ *
2547
+ * @example
2548
+ * ```typescript
2549
+ * import { createClient } from '@relyio/sdk'
2550
+ * import { schema } from './schema'
2551
+ *
2552
+ * const relyio = createClient({
2553
+ * apiKey: process.env.RELYIO_API_KEY!,
2554
+ * schema,
2555
+ * })
2556
+ *
2557
+ * // Content operations
2558
+ * const { data: products } = await relyio.content('product').list({ limit: 20 })
2559
+ *
2560
+ * // Search
2561
+ * const results = await relyio.search('wireless', { contentType: 'product' })
2562
+ *
2563
+ * // Batch operations
2564
+ * const batch = await relyio.batch([{ create: 'product', data: { title: 'New' } }])
2565
+ * ```
2566
+ */
2567
+ declare class TypedRelyIOClient<TSchema extends AnySchemaDefinition | undefined = undefined> {
2568
+ /**
2569
+ * The one HTTP client every sub-client sends through. `updateConfig()` and `setApiKey()` change it
2570
+ * in place rather than replacing it, so a sub-client taken before either sends through the change.
2571
+ */
2572
+ private httpClient;
2573
+ private config;
2574
+ /**
2575
+ * The configuration as `createClient()`, `setApiKey()` and `updateConfig()` gave it, before the
2576
+ * `relyio dev` handshake: what the next update is laid over, and what `with()` starts from.
2577
+ */
2578
+ private configured;
2579
+ private _sandbox;
2580
+ private _schema;
2581
+ /**
2582
+ * The client's locale as it is when a call is made. Every sub-client and builder is handed this
2583
+ * rather than a copy of the locale, so one taken before `updateConfig({ locale })` sends the new
2584
+ * locale, or none after `locale: null` (REL-1453).
2585
+ */
2586
+ private readonly currentLocale;
2587
+ /**
2588
+ * The client `with()` was called on, for a client it made: whose key, base URL and configuration
2589
+ * this one reads with as they are when each request is sent. `undefined` for a client
2590
+ * `createClient()` made, which reads its own.
2591
+ */
2592
+ private parent;
2593
+ /** What `with()` gave this client over its parent's: the locale and dataset it names, and no other. */
2594
+ private defaults;
2595
+ /** The live edge's poll client id: chosen once, when first needed, and kept for this client's life. */
2596
+ private _liveClientId;
2597
+ constructor(config: TypedClientConfig<TSchema>);
2598
+ /**
2599
+ * The `relyio dev` sandbox this client resolved at construction, as the preview toolbar displays
2600
+ * it (`sdk-surfaces-spec.md` decision 22).
2601
+ *
2602
+ * Plain, JSON-serializable data and no key, so a server component can hand it to a client
2603
+ * component as a prop — the browser cannot read the handshake itself, so a client created there
2604
+ * reports `absent`. See {@link SandboxState} for what each state means.
2605
+ *
2606
+ * @example
2607
+ * ```tsx
2608
+ * // app/layout.tsx — a server component handing the state to a client component
2609
+ * <SandboxBadge sandbox={relyio.sandbox} />
2610
+ * ```
2611
+ */
2612
+ get sandbox(): SandboxState;
2613
+ /**
2614
+ * Run a query in one call (REL-2421, REL-2409 Q22):
2615
+ *
2616
+ * - `query({ from, … })` runs one inline, with `list`'s option names — `where` (the query
2617
+ * route's dialect), `include`, `select`, `sort`, `limit`, `page`, `locale`, `dataset`, `fresh` —
2618
+ * plus `aggregate`, `params` and `fullText`;
2619
+ * - `query(name, { params, … })` runs the saved query the schema declares under `name`, its
2620
+ * `params` typed from that declaration.
2621
+ *
2622
+ * Both answer the API's body, `{ data: { result, metadata } }`. See {@link QueryCall}. Every query
2623
+ * is sent in the client's locale as it is then, unless it names its own.
2624
+ */
2625
+ get query(): QueryCall<TSchema>;
2626
+ /**
2627
+ * Get a content client for a specific content type
2628
+ * Type-safe when schema is provided: autocompletes content type names AND field names
2629
+ *
2630
+ * @example
2631
+ * ```typescript
2632
+ * const { data: products } = await relyio.content('product').list({
2633
+ * where: { status: 'active' }, // ← Field names autocomplete with schema!
2634
+ * locale: 'en-US',
2635
+ * limit: 20,
2636
+ * })
2637
+ * ```
2638
+ */
2639
+ content<K extends TSchema extends AnySchemaDefinition ? SchemaContentTypeNames<TSchema> : string>(contentType: K): TSchema extends AnySchemaDefinition ? ContentClient<InferContentType<TSchema['contentTypes'][K & keyof TSchema['contentTypes']], TSchema>, ContentReading<TSchema['contentTypes'][K & keyof TSchema['contentTypes']], TSchema>> : ContentClient<BaseContent>;
2640
+ /**
2641
+ * Get an asset client for a specific asset type.
2642
+ *
2643
+ * Reads and writes as `content()` does — `list`, `get`, `listAll`, `update`, `delete` — over
2644
+ * `entryType: 'asset'` entries on the same dynamic `/{assetTypeId}` routes, and adds `create`,
2645
+ * the `file(entryUid)` sub-resource builder and a high-level `upload()` helper. Type-safe when a schema is
2646
+ * provided: autocompletes asset type names AND metadata field names.
2647
+ *
2648
+ * @example
2649
+ * ```typescript
2650
+ * const photo = await relyio.asset('productPhoto').upload(file)
2651
+ * .filename('shoe.jpg')
2652
+ * .visibility('public')
2653
+ * .execute()
2654
+ *
2655
+ * const heroUrl = await relyio.asset('productPhoto').file(photo._metadata.id).url({ variantId: 'hero' })
2656
+ * ```
2657
+ */
2658
+ asset<K extends TSchema extends AnySchemaDefinition ? SchemaAssetTypeNames<TSchema> : string>(assetType: K): TSchema extends AnySchemaDefinition ? AssetClient<InferAssetType<NonNullable<TSchema['assetTypes']>[K & keyof NonNullable<TSchema['assetTypes']>], TSchema>, AssetReading<NonNullable<TSchema['assetTypes']>[K & keyof NonNullable<TSchema['assetTypes']>], TSchema>> : AssetClient<BaseContent>;
2659
+ /**
2660
+ * Get a singleton client for a specific singleton key
2661
+ * Type-safe when schema is provided: autocompletes singleton names AND field names
2662
+ *
2663
+ * @example
2664
+ * ```typescript
2665
+ * const homepage = await relyio.singleton('homepage').get({
2666
+ * include: ['featuredProducts'],
2667
+ * locale: 'en',
2668
+ * })
2669
+ * ```
2670
+ */
2671
+ singleton<K extends TSchema extends AnySchemaDefinition ? SchemaSingletonNames<TSchema> : string>(key: K): TSchema extends AnySchemaDefinition ? SingletonClient<InferSingleton<TSchema['singletonContentTypes'][K & keyof TSchema['singletonContentTypes']], TSchema>, SingletonReading<TSchema['singletonContentTypes'][K & keyof TSchema['singletonContentTypes']], TSchema>> : SingletonClient<BaseSingleton>;
2672
+ /**
2673
+ * Get a settings client for a specific settings group
2674
+ * Type-safe when schema is provided: autocompletes settings group names AND field names
2675
+ *
2676
+ * @example
2677
+ * ```typescript
2678
+ * const emailConfig = await relyio.settings('email-config').get({
2679
+ * include: ['defaultTemplate'],
2680
+ * })
2681
+ * ```
2682
+ */
2683
+ settings<K extends TSchema extends AnySchemaDefinition ? SchemaSettingsGroupNames<TSchema> : string>(groupId: K): TSchema extends AnySchemaDefinition ? SettingsClient<InferSettingsGroup<NonNullable<TSchema['settingsGroups']>[K & keyof NonNullable<TSchema['settingsGroups']>], TSchema>, SettingsReading<NonNullable<TSchema['settingsGroups']>[K & keyof NonNullable<TSchema['settingsGroups']>], TSchema>> : SettingsClient<BaseSettingsGroup>;
2684
+ /**
2685
+ * Search, one page at a time: `search(q, options)` (REL-2420, REL-2409 Q19).
2686
+ *
2687
+ * Pages by number — `page` and `limit` — and answers the API's `{ data, page, meta }`, the page
2688
+ * `{ number, limit, total, pageCount }`: `total` counts every entry the search matched, so "page 2
2689
+ * of 3" needs no arithmetic. Where a schema types the client, `contentType`, `select`,
2690
+ * `searchFields`, `sort` and `locale` are checked against it and the hits are its entries — every
2691
+ * content type's when `contentType` is left out (`INV-SS-TYPES-INFERRED`).
2692
+ *
2693
+ * @example
2694
+ * ```typescript
2695
+ * const { data, page } = await relyio.search('wireless headphones', {
2696
+ * contentType: 'product',
2697
+ * where: { status: 'active' },
2698
+ * page: 2,
2699
+ * limit: 20,
2700
+ * })
2701
+ * console.log(`page ${page.number} of ${page.pageCount}`)
2702
+ * ```
2703
+ */
2704
+ search<K extends SearchTypeName<TSchema> = SearchTypeName<TSchema>, const S extends readonly SearchFieldKey<SearchEntry<TSchema, K>>[] | undefined = undefined>(query: string, options: SearchOptions<K, SearchEntry<TSchema, K>, LocaleCodeOf<TSchema>, SearchWhere<TSchema, K>, S>): Promise<SearchPage<SearchSelected<SearchEntry<TSchema, K>, S>>>;
2705
+ /**
2706
+ * Suggestions completing `query` in one Field: `autocomplete(q, options)` (REL-2420).
2707
+ *
2708
+ * @example
2709
+ * ```typescript
2710
+ * const { data } = await relyio.autocomplete('wire', { contentType: 'product', field: 'title' })
2711
+ * ```
2712
+ */
2713
+ autocomplete<K extends SearchTypeName<TSchema>>(query: string, options: AutocompleteOptions<K, SearchEntry<TSchema, K>, LocaleCodeOf<TSchema>, SearchWhere<TSchema, K>>): Promise<AutocompleteResponse>;
2714
+ /**
2715
+ * The entries most like the entry `sourceId`, by its stored vector: `similar(id, options)`
2716
+ * (REL-2420). Every content type's neighbours unless `contentType` names some.
2717
+ *
2718
+ * @example
2719
+ * ```typescript
2720
+ * const { data } = await relyio.similar('entry_abc123', { contentType: 'product', limit: 5 })
2721
+ * ```
2722
+ */
2723
+ similar<K extends SearchTypeName<TSchema> = SearchTypeName<TSchema>, const S extends readonly SearchFieldKey<SearchEntry<TSchema, K>>[] | undefined = undefined>(sourceId: string, options: SimilarOptions<K, SearchEntry<TSchema, K>, LocaleCodeOf<TSchema>, S>): Promise<SimilarResponse<SearchSelected<SearchEntry<TSchema, K>, S>>>;
2724
+ /**
2725
+ * Write a list of operations in one request (REL-2421, REL-2409 Q22): each a create, update,
2726
+ * replace or delete of an entry, or a singleton write.
2727
+ *
2728
+ * An update, replace or delete takes the **entry you read** — checked against the version it
2729
+ * carries, in the locale it was read in, as `update(entry, data)` is — or an **id**, which writes
2730
+ * unchecked, on purpose. A stale entry fails its operation with `RESOURCE_CONFLICT` naming the
2731
+ * versions that disagreed: re-read it with `fresh: true` and retry. With `atomic: true` nothing
2732
+ * in the batch is written then — and the API takes an atomic batch only when every operation is
2733
+ * the same kind of write.
2734
+ *
2735
+ * Answers the API's body, `{ data: { results, summary } }`: one result per operation, in order,
2736
+ * each an `outcome` or an `error`. An operation that failed is not thrown.
2737
+ *
2738
+ * @example
2739
+ * ```typescript
2740
+ * const post = await relyio.content('post').get('hello-world')
2741
+ * const { data } = await relyio.batch([
2742
+ * { create: 'post', data: { title: 'New' } },
2743
+ * ...(post ? [{ update: 'post' as const, entry: post, data: { title: 'Changed' } }] : []),
2744
+ * { delete: 'post', id: 'entry_xyz' },
2745
+ * { singleton: 'homepage', data: { hero: 'Welcome!' } },
2746
+ * ])
2747
+ * console.log(data.summary.succeeded)
2748
+ * ```
2749
+ */
2750
+ batch(operations: readonly BatchWrite<TSchema>[], options?: BatchWriteOptions): Promise<BatchBody>;
2751
+ /**
2752
+ * Follow the dataset's changes feed: each write this key can see, as ids, in
2753
+ * order, long-polled from `GET /v1/changes`.
2754
+ *
2755
+ * Lazy — nothing is requested until the first `next()`. It runs until
2756
+ * `signal` aborts, which ends the `for await` cleanly rather than throwing.
2757
+ * A change names what changed; refetch it through the ordinary read path
2758
+ * (`INV-SS-CHANGE-IS-IDS`), naming its `fresh` so no cache answers with the
2759
+ * body from before it (`FeedChange`).
2760
+ *
2761
+ * @example
2762
+ * ```typescript
2763
+ * const controller = new AbortController()
2764
+ * for await (const change of relyio.changes({ since: cursor, types: ['post'], signal: controller.signal })) {
2765
+ * // { entryId, contentType, locale, kind, cursor, fresh }
2766
+ * await relyio.content(change.contentType).get(change.entryId, { fresh: change.fresh })
2767
+ * }
2768
+ * ```
2769
+ *
2770
+ * @example Resync when the cursor has expired
2771
+ * ```typescript
2772
+ * for await (const change of relyio.changes({
2773
+ * since: cursor,
2774
+ * onExpired: async expired => refetchEverything(expired.fresh),
2775
+ * })) { … }
2776
+ * ```
2777
+ */
2778
+ changes(options?: ChangesOptions): AsyncIterable<FeedChange>;
2779
+ /**
2780
+ * Follow the dataset's **published** changes at RelyIO's live edge, with a publishable key
2781
+ * (`rly_pub_…`): each change as ids, in order — the transport a browser follows changes with.
2782
+ *
2783
+ * A WebSocket to the edge, from `since` or the dataset's current position; where a socket cannot
2784
+ * open — the runtime has none, the network refuses it, or it has not opened and been told its
2785
+ * position within `LIVE_SOCKET_OPEN_TIMEOUT_MS` — it polls the edge instead, about every fifteen
2786
+ * seconds, from the same cursor. The edge is `liveUrl`, or derived from the default `baseUrl`
2787
+ * (see `RelyIOConfig.liveUrl`).
2788
+ *
2789
+ * Without `since` it starts at the dataset's current position, which it hands `reached` before
2790
+ * anything after it: read what you show after that call, and a publish your read did not see is
2791
+ * one this subscription announces (see `LiveChangesOptions.since`).
2792
+ *
2793
+ * One subscription ends only when `signal` aborts, which ends the `for await` cleanly; every other
2794
+ * end — a socket a deploy dropped, a refusal, a refused poll — is thrown as a `LiveEdgeError`
2795
+ * that says whether to stop, retry or back off. Hand it to `followChanges()`, the one loop that
2796
+ * reconnects from the last cursor with jittered back-off and honours those answers:
2797
+ *
2798
+ * @example
2799
+ * ```typescript
2800
+ * const relyio = createClient({ apiKey: 'rly_pub_…' })
2801
+ * const controller = new AbortController()
2802
+ * await followChanges(from => relyio.liveChanges({ ...from, types: ['post'] }), {
2803
+ * signal: controller.signal,
2804
+ * // `fresh` names a position no cache holds a body for from before the change.
2805
+ * onChange: change =>
2806
+ * relyio.content(change.contentType).get(change.entryId, { fresh: change.fresh }),
2807
+ * onResync: resync => refetchEverything(resync.cause === 'expired' ? resync.expired.fresh : undefined),
2808
+ * })
2809
+ * ```
2810
+ *
2811
+ * A client without a publishable key sends nothing: a secret or preview key is never put in the
2812
+ * edge's URL. On a server, follow `changes()` with it instead.
2813
+ */
2814
+ liveChanges(options?: LiveChangesOptions): AsyncIterable<FeedChange>;
2815
+ /**
2816
+ * The dataset's current position in the changes feed, as a cursor to start
2817
+ * `changes({ since })` or `followChanges(…, { since })` from. One
2818
+ * `GET /v1/changes` without a cursor: answered at once, holding nothing.
2819
+ *
2820
+ * Take it **before** you read what you will follow. A write your read did not
2821
+ * see is then after the position and replayed; opening the feed afterwards
2822
+ * without a `since` starts after it and misses it.
2823
+ *
2824
+ * @example
2825
+ * ```typescript
2826
+ * const since = await relyio.changesHead()
2827
+ * const post = await relyio.content('post').get(id)
2828
+ * for await (const change of relyio.changes({ since, ids: [id] })) { … }
2829
+ * ```
2830
+ */
2831
+ changesHead(options?: ChangesHeadOptions): Promise<string>;
2832
+ /**
2833
+ * Describe the project this key resolves to — `GET /v1/project`.
2834
+ *
2835
+ * `dataset` is the one this client's requests select (`dataset`, a sandbox from the `relyio dev`
2836
+ * handshake, or the key's default), which is not always the project's default, so a Studio link
2837
+ * built from `studioUrl` should carry it as `?dataset=`. `studioUrl` is `null` — a value, not an
2838
+ * error — when the service has no Dashboard address configured.
2839
+ *
2840
+ * @example
2841
+ * ```typescript
2842
+ * const { studioUrl, dataset } = await relyio.project()
2843
+ * if (studioUrl !== null) window.open(`${studioUrl}?dataset=${dataset.id}`)
2844
+ * ```
2845
+ */
2846
+ project(): Promise<Project>;
2847
+ /**
2848
+ * The schema definition the key's branch has deployed, as stored — `GET /v1/meta/schema` — or,
2849
+ * given a content type or singleton id, that one type's definition — `GET /v1/meta/schema/{id}`.
2850
+ *
2851
+ * It answers what the deployed schema declares; `project()` answers where the key points
2852
+ * (organization, project, dataset, branch, Studio link). The two share no field.
2853
+ *
2854
+ * @example
2855
+ * ```typescript
2856
+ * const definition = await relyio.schema('post')
2857
+ * ```
2858
+ */
2859
+ schema(contentType?: string): Promise<unknown>;
2860
+ /**
2861
+ * The content types and singletons the key's branch has deployed — `GET /v1/meta/content-types`.
2862
+ *
2863
+ * Each is named by its `id` (what `content()` and `singleton()` take), its `name` (the schema's
2864
+ * `displayName`) and its `type`, content types first, each group in the order the schema declares it.
2865
+ *
2866
+ * @example
2867
+ * ```typescript
2868
+ * const types = await relyio.contentTypes()
2869
+ * const singletons = types.filter(type => type.type === 'singleton').map(type => type.id)
2870
+ * ```
2871
+ */
2872
+ contentTypes(): Promise<DeployedContentType[]>;
2873
+ /**
2874
+ * Whether the API can serve requests now — `GET /health/ready`.
2875
+ *
2876
+ * Resolves `{ status: 'ok', timestamp }` when the API answers and reaches every store it reads.
2877
+ * Rejects otherwise, with the error the request met: a `503` naming the store that did not answer,
2878
+ * or a network error when the API could not be reached at all. So there is no `status` to check:
2879
+ * a call that resolves is the healthy answer.
2880
+ *
2881
+ * @example
2882
+ * ```typescript
2883
+ * try {
2884
+ * await relyio.health()
2885
+ * } catch (error) {
2886
+ * console.error('RelyIO cannot serve now', error)
2887
+ * }
2888
+ * ```
2889
+ */
2890
+ health(): Promise<Health>;
2891
+ /**
2892
+ * Change some of the client's options and keep the rest.
2893
+ *
2894
+ * What you pass is laid over the configuration the client holds, field by field:
2895
+ *
2896
+ * - an option you name replaces the value the client holds;
2897
+ * - an option left out, or given as `undefined`, keeps the value the client holds;
2898
+ * - an option with no default given as `null` is cleared — `locale`, `dataset`, `apiKey`, `dev`,
2899
+ * `fetch`, `browser.onCorsError`, `realtime.wsUrl`, `debug.logger` — so the client runs as though
2900
+ * it had never been given one. An option with a default takes no `null`;
2901
+ * - a bag of options you name — `browser`, `cache`, `realtime`, `debug` — keeps every field of it
2902
+ * you do not name, so `updateConfig({ browser: { includeCredentials: true } })` keeps a
2903
+ * configured `onCorsError`;
2904
+ * - `headers` is laid over the headers the client sends, a name matched without regard to case: a
2905
+ * header you name is added or replaces every spelling of it, every other one is still sent, and a
2906
+ * header given as `null` is no longer sent;
2907
+ * - naming `apiKey` — a key, or `null` — replaces every key the client holds, one given in
2908
+ * `headers` included.
2909
+ *
2910
+ * The configuration it is laid over is what `createClient()`, `setApiKey()` and earlier updates
2911
+ * gave the client. The `relyio dev` handshake is read again, as `createClient()` reads it: while a
2912
+ * sandbox is active its key and dataset are sent over the configured ones, and once it has gone
2913
+ * the configured ones are.
2914
+ *
2915
+ * The client's one HTTP client is changed in place, so every request from now on is sent as the
2916
+ * update says — including one from a `content()`, `asset()`, `singleton()` or `settings()`
2917
+ * sub-client taken before the call. The locale reaches them too: each reads the client's locale
2918
+ * when it sends, so a new `locale` is the default of its next request and `locale: null` leaves
2919
+ * that request with none. A locale the call names still wins over it, and so does the locale of a
2920
+ * client `with({ locale })` made from this one.
2921
+ *
2922
+ * @example
2923
+ * ```typescript
2924
+ * const relyio = createClient({ apiKey, headers: { 'X-Tenant': 'acme' }, browser: { onCorsError } })
2925
+ *
2926
+ * relyio.updateConfig({ headers: { 'X-Trace': traceId } }) // X-Tenant is still sent
2927
+ * relyio.updateConfig({ browser: { includeCredentials: true } }) // onCorsError is kept
2928
+ * relyio.updateConfig({ headers: { 'X-Tenant': null } }) // X-Tenant is no longer sent
2929
+ * relyio.updateConfig({ locale: null }) // reads name no locale of the client's
2930
+ * ```
2931
+ */
2932
+ updateConfig(update: RelyIOConfigUpdate): void;
2933
+ /**
2934
+ * Get the current configuration
2935
+ */
2936
+ getConfig(): Readonly<MergedRelyIOConfig>;
2937
+ /**
2938
+ * Replace the API key every later request sends as `X-API-Key`.
2939
+ *
2940
+ * It reaches every request this client makes from now on, including those of a `content()`,
2941
+ * `asset()` or other sub-client taken before the call, and `getConfig().apiKey` reports it. A key
2942
+ * given in `headers`, under any spelling, is replaced with it and does not come back on a later
2943
+ * `updateConfig()`. A client `with()` made from this one sends the new key too: the two share one
2944
+ * connection.
2945
+ *
2946
+ * An empty or blank key is no key, as it is when passed to `createClient`: the next request
2947
+ * rejects with `MissingApiKeyError` and nothing is sent.
2948
+ *
2949
+ * Under an active `relyio dev` sandbox the sandbox's scoped key stands, as it does over the key
2950
+ * passed to `createClient` — replacing the configured key does not retarget the client at the
2951
+ * sandbox's dataset with a key the sandbox did not issue.
2952
+ *
2953
+ * @example
2954
+ * ```typescript
2955
+ * const relyio = createClient({ apiKey: process.env.RELYIO_API_KEY! })
2956
+ * relyio.setApiKey(rotatedKey) // the next read sends `X-API-Key: <rotatedKey>`
2957
+ * ```
2958
+ */
2959
+ setApiKey(apiKey: string): void;
2960
+ /**
2961
+ * Get the HTTP client instance (for advanced usage)
2962
+ */
2963
+ getHttpClient(): HttpClient;
2964
+ /**
2965
+ * Get the current default locale
2966
+ */
2967
+ getLocale(): string | undefined;
2968
+ /**
2969
+ * A client whose reads and writes are in `locale` and `dataset` unless a call names its own, and
2970
+ * which otherwise **follows this one** (REL-2417, Q9): it sends this client's key, to this
2971
+ * client's base URL, under this client's configuration as it is when each request is sent — so a
2972
+ * later `setApiKey()` or `updateConfig()` here reaches it. A default it does not name is this
2973
+ * client's, as this client changes it.
2974
+ *
2975
+ * The two share one connection, so a `setApiKey()` or `updateConfig()` called on the client made
2976
+ * here changes this one too. A call's own `locale` or `dataset` wins over the defaults.
2977
+ *
2978
+ * @example
2979
+ * ```typescript
2980
+ * const staging = relyio.with({ dataset: 'staging' })
2981
+ * const { data } = await staging.content('post').list({ limit: 10 })
2982
+ *
2983
+ * const french = relyio.with({ locale: 'fr-FR' }) // ← autocompletes the schema's locales
2984
+ * ```
2985
+ */
2986
+ with(defaults: ClientDefaults<TSchema>): TypedRelyIOClient<TSchema>;
2987
+ /** Make this client one `with()` made from `parent`: see {@link with}. */
2988
+ private follow;
2989
+ /**
2990
+ * The configuration as given, before the `relyio dev` handshake — this client's own, or for a
2991
+ * client `with()` made, its parent's with its defaults laid over it.
2992
+ */
2993
+ private configuredNow;
2994
+ /**
2995
+ * Every locale the key's branch has deployed, the default marked — `GET /v1/meta/locales`.
2996
+ *
2997
+ * In the order the schema declares them. Exactly one has `default: true`, the locale a read or
2998
+ * write that names none is in; every other has `default: false`. `fallback`, `direction` and
2999
+ * `active` are as the schema declares them, absent where it does not.
3000
+ *
3001
+ * @example
3002
+ * ```typescript
3003
+ * const locales = await relyio.locales()
3004
+ * const defaultLocale = locales.find(locale => locale.default)?.code
3005
+ *
3006
+ * // Build a language switcher
3007
+ * const options = locales.map(locale => ({ code: locale.code, label: locale.displayName }))
3008
+ * ```
3009
+ */
3010
+ locales(): Promise<DeployedLocale[]>;
3011
+ }
3012
+ /**
3013
+ * Create a RelyIO client
3014
+ *
3015
+ * @example Without schema (dynamic usage)
3016
+ * ```typescript
3017
+ * import { createClient } from '@relyio/sdk'
3018
+ *
3019
+ * const relyio = createClient({
3020
+ * apiKey: process.env.RELYIO_API_KEY!,
3021
+ * })
3022
+ *
3023
+ * // Works but no autocomplete for content types
3024
+ * const { data: products } = await relyio.content('product').list()
3025
+ * ```
3026
+ *
3027
+ * @example With schema (fully typed)
3028
+ * ```typescript
3029
+ * import { createClient } from '@relyio/sdk'
3030
+ * import { schema } from './schema'
3031
+ *
3032
+ * const relyio = createClient({
3033
+ * apiKey: process.env.RELYIO_API_KEY!,
3034
+ * schema,
3035
+ * })
3036
+ *
3037
+ * // Content type names autocomplete!
3038
+ * const { data: products } = await relyio.content('product').list()
3039
+ * ```
3040
+ *
3041
+ * @example After `relyio generate-types` (inferred, `INV-SS-TYPES-INFERRED`)
3042
+ * ```typescript
3043
+ * import { createClient } from '@relyio/sdk'
3044
+ *
3045
+ * // The generated `schema.ts` augments `Register`, so no generic and no `schema:` are needed
3046
+ * const relyio = createClient({ apiKey: process.env.RELYIO_API_KEY! })
3047
+ * ```
3048
+ *
3049
+ * The overload without a schema comes first on purpose. Behind the generic one it was never
3050
+ * chosen: with no `schema` to infer from, `TSchema` fell back to its constraint and the call
3051
+ * resolved to `TypedRelyIOClient<AnySchemaDefinition>`. First, it takes every call that passes no
3052
+ * schema, and returns the registered schema's client — or the untyped one when nothing registered.
3053
+ * A `schema:` value fails its `schema?: undefined`, and an explicit generic skips it because it has
3054
+ * no type parameter, so both reach the generic overload unchanged.
3055
+ */
3056
+ declare function createClient(config: RelyIOConfig & {
3057
+ schema?: undefined;
3058
+ }): TypedRelyIOClient<RegisteredSchema>;
3059
+ declare function createClient<TSchema extends AnySchemaDefinition>(config: TypedClientConfig<TSchema>): TypedRelyIOClient<TSchema>;
3060
+ /**
3061
+ * Helper type to extract schema from client
3062
+ */
3063
+ type ClientSchema<T extends TypedRelyIOClient<AnySchemaDefinition>> = T extends TypedRelyIOClient<infer TSchema> ? TSchema : never;
3064
+
3065
+ export { type ListOptions as $, type AnySchemaDefinition as A, type EntryWriteOptions as B, type Change as C, type DeployedLocale as D, type EntryPlacement as E, type FeedChange as F, SingletonClient as G, type Health as H, SettingsClient as I, type DocumentFields as J, type DocumentVersion as K, type LocaleCodeOf as L, type MatchRouteOptions as M, type DocumentVersionsOptions as N, type DocumentWriteOptions as O, type Project as P, type ChildrenOptions as Q, type Register as R, type SandboxState as S, TypedRelyIOClient as T, type DocumentReadOptions as U, type EntryList as V, type Wire as W, type FieldKey as X, type FreshOption as Y, type GetOptions as Z, type ListAllOptions as _, ChangesExpiredError as a, type PresignInput as a$, type LocaleOption as a0, type Paging as a1, type ReadAs as a2, type ReadLocale as a3, type SortOption as a4, type VersionOptions as a5, type WrittenReadScope as a6, type WrittenSort as a7, type SearchResultItem as a8, type SearchHitMetadata as a9, type BatchReplace as aA, type BatchDelete as aB, type BatchSingletonWrite as aC, type BatchTarget as aD, type BatchDeleteTarget as aE, type AggregateMetric as aF, type QueryMetadata as aG, type AdhocQuery as aH, type AdhocRecord as aI, type NamedQueryOptions as aJ, type NamedQueryParams as aK, type QueryAggregate as aL, type QueryAnswer as aM, type QueryCall as aN, type QueryName as aO, type QueryTypeName as aP, AssetClient as aQ, AssetFileBuilder as aR, AssetUploadBuilder as aS, isAssetEntry as aT, type AssetVisibility as aU, type FileStatus as aV, type FileBackend as aW, type AssetFileExtracted as aX, type FileStatusResult as aY, type AssetEntryResponse as aZ, type PopulatedAssetReference as a_, type SearchResponse as aa, type AutocompleteSuggestion as ab, type AutocompleteResponse as ac, type SimilarResult as ad, type SimilarResponse as ae, type SearchTypeName as af, type SearchEntry as ag, type AutocompleteOptions as ah, type SearchFacet as ai, type SearchFieldKey as aj, type SearchHighlight as ak, type SearchOptions as al, type SearchPage as am, type SearchScope as an, type SearchSelected as ao, type SearchSortOption as ap, type SearchWhere as aq, type SimilarOptions as ar, type BatchOperationResult as as, type BatchSummary as at, type BatchResponse as au, type BatchWrite as av, type BatchWriteOptions as aw, type BatchBody as ax, type BatchCreate as ay, type BatchUpdate as az, type ChangesExpiredReason as b, type PresignResult as b0, type ConfirmInput as b1, type ConfirmResult as b2, type ResolveUrlInput as b3, type ResolveUrlResult as b4, type CreateAssetOptions as b5, type AssetPlacement as b6, type UpdateAssetOptions as b7, type WaitUntilReadyOptions as b8, type MultipartCreateResult as b9, type MultipartPart as ba, type MultipartTarget as bb, type UploadProgress as bc, ResourceNotFoundError as bd, ResourceConflictError as be, ResourceExpiredError as bf, isResourceNotFound as bg, isMissingEntry as bh, type FileErrorCode as bi, type FileUploadErrorCode as bj, FileUploadError as bk, FileValidationError as bl, type FileValidationConstraints as bm, validateFile as bn, createClient as c, type TypedClientConfig as d, type ClientDefaults as e, type ClientSchema as f, type DeployedContentType as g, type RegisteredSchema as h, type ActiveSandbox as i, HttpClient as j, type SendOptions as k, type ApiPath as l, type ApiMethod as m, type ApiSuccessBody as n, matchRoute as o, type RouteMatch as p, type ReadEntry as q, type RetryableWriteOptions as r, type ChangesHeadOptions as s, type ChangesOptions as t, LIVE_KEEPALIVE_INTERVAL_MS as u, versionCheckOf as v, LIVE_SOCKET_OPEN_TIMEOUT_MS as w, type LiveChangesOptions as x, ContentClient as y, type CreateEntryOptions as z };