@foir/sdk 0.7.0 → 0.9.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 (61) hide show
  1. package/CHANGELOG.md +857 -4
  2. package/README.md +46 -15
  3. package/dist/auth/auth.d.ts +104 -1
  4. package/dist/auth/hosted.d.ts +22 -0
  5. package/dist/auth/index.d.ts +13 -7
  6. package/dist/auth/operations.d.ts +85 -64
  7. package/dist/auth/ops.d.ts +99 -0
  8. package/dist/auth.js +786 -106
  9. package/dist/chunk-IGNLWT3H.js +35 -0
  10. package/dist/chunk-OHYVKTGB.js +45 -0
  11. package/dist/chunk-TAL5H6VA.js +39 -0
  12. package/dist/client.js +25 -31
  13. package/dist/codegen/block-registry.d.ts +13 -19
  14. package/dist/codegen/block-registry.js +3 -31
  15. package/dist/codegen/block-scaffold.d.ts +12 -21
  16. package/dist/codegen/block-scaffold.js +3 -41
  17. package/dist/codegen/index.d.ts +9 -4
  18. package/dist/codegen/index.js +7 -11
  19. package/dist/codegen/item-registry.d.ts +28 -0
  20. package/dist/codegen/item-registry.js +7 -0
  21. package/dist/codegen/item-scaffold.d.ts +29 -0
  22. package/dist/codegen/item-scaffold.js +7 -0
  23. package/dist/codegen/preset.d.ts +14 -7
  24. package/dist/codegen/shared.d.ts +20 -9
  25. package/dist/content/content-renderer.d.ts +90 -24
  26. package/dist/content/index.d.ts +13 -7
  27. package/dist/content/richtext.d.ts +66 -18
  28. package/dist/content.js +227 -80
  29. package/dist/data/builtins.d.ts +369 -92
  30. package/dist/data/client.d.ts +97 -25
  31. package/dist/data/document.d.ts +251 -0
  32. package/dist/data/index.d.ts +38 -19
  33. package/dist/data/manifest.d.ts +290 -8
  34. package/dist/data/schema.d.ts +78 -8
  35. package/dist/data/selection.d.ts +231 -37
  36. package/dist/data/subjects.d.ts +221 -0
  37. package/dist/data/subscription-document.d.ts +75 -0
  38. package/dist/data/subscriptions.d.ts +181 -0
  39. package/dist/data/transport.d.ts +255 -37
  40. package/dist/data/ws-transport.d.ts +197 -0
  41. package/dist/data.js +1419 -45
  42. package/dist/graphql/cdn-route.d.ts +12 -28
  43. package/dist/media/index.d.ts +6 -2
  44. package/dist/media/media.d.ts +2 -2
  45. package/dist/media/screen-capture.d.ts +81 -0
  46. package/dist/media.js +97 -0
  47. package/dist/next.js +663 -182
  48. package/dist/push.js +700 -90
  49. package/dist/react/FoirFeedback.d.ts +62 -0
  50. package/dist/react/feedback-notes.d.ts +101 -0
  51. package/dist/react/index.d.ts +7 -1
  52. package/dist/react.js +1272 -194
  53. package/dist/realtime.js +1 -1
  54. package/dist/server/events-client.d.ts +279 -0
  55. package/dist/server/index.d.ts +4 -0
  56. package/dist/server.js +298 -0
  57. package/dist/transport/foir-http.d.ts +23 -6
  58. package/dist/transport/persisted-query.d.ts +68 -0
  59. package/dist/transport/retry.d.ts +1 -1
  60. package/package.json +13 -1
  61. package/dist/chunk-ADO5FJNM.js +0 -30
@@ -50,14 +50,39 @@
50
50
  * the pipeline to do it reds all six union tests plus the rest-operator
51
51
  * one by name.
52
52
  *
53
- * So: never reach inside a field's value type when that type may be a UNION.
53
+ * So: never reach inside a field's value type WITH A MAPPED TYPE OVER THE UNION.
54
54
  *
55
55
  * Nested selection (`pick: { seo: { jsonLd: true } }`) does reach one level in,
56
56
  * and is safe only because `Descendable` refuses unions before it gets there —
57
- * see the guard below and the two tests that pin it. The list form of a union
58
- * is caught earlier still by the array check, which is why the guard needs a
59
- * NON-list union to be tested at all; a mutation disabling it left every test
60
- * green until one was added.
57
+ * see the guard below and the two tests that pin it.
58
+ *
59
+ * ---------------------------------------------------------------------------
60
+ * Per-variant selection, and why it is not a hole in the above
61
+ * ---------------------------------------------------------------------------
62
+ *
63
+ * `pick: { blocks: { on: { ImageBlock: ['height'] } } }` DOES narrow inside a
64
+ * polymorphic union, and it is safe for a reason the rule above does not cover:
65
+ * it distributes FIRST. `U extends { __typename: infer T } ? … : never` on a
66
+ * naked type parameter splits the union into its members, and every `Pick` or
67
+ * `Omit` below that line applies to ONE member. The union is reassembled from
68
+ * the branches, never mapped over whole, so `keyof (A | B | C)` is never taken
69
+ * and there is nothing to collapse.
70
+ *
71
+ * The distinction is the whole design, and it is worth stating as a rule
72
+ * because the wrong version of this change is a one-line widening of
73
+ * `Descendable`:
74
+ *
75
+ * - reaching into a union with the VERB is Sanity's failure mode and stays
76
+ * refused — `Descendable` still returns false for every union, and it
77
+ * checks `IsUnion` rather than `IsPolymorphic` so that widening the
78
+ * polymorphic branch cannot widen this one by accident;
79
+ * - reaching into it through `on`, per member, keyed on `__typename`, is the
80
+ * supported form.
81
+ *
82
+ * What DID change is that a LIST of composites is now descendable. Refusing
83
+ * lists was never about union safety — a union in a list is caught by the union
84
+ * check either way — and it made `pageContent.sections.blocks` untypeable end to
85
+ * end, since a list is the only thing between a page and its blocks.
61
86
  */
62
87
  import type { FoirConnection, FoirModelDefinition, FoirNamespaceOpDefinition, FoirOpDefinition, FoirRelationDefinition, FoirSchemaDefinition } from './schema';
63
88
  /**
@@ -104,7 +129,7 @@ export type FoirOmittableKey<M extends FoirModelDefinition> = Exclude<Extract<ke
104
129
  type OmittedKeys<Q> = Q extends {
105
130
  omit: readonly (infer K)[];
106
131
  } ? Extract<K, string> : never;
107
- type ApplyOmit<M extends FoirModelDefinition, Q> = [OmittedKeys<Q>] extends [never] ? M['fields'] : Omit<M['fields'], OmittedKeys<Q>>;
132
+ type ApplyOmit<F, Q> = [OmittedKeys<Q>] extends [never] ? F : Omit<F, OmittedKeys<Q>>;
108
133
  /**
109
134
  * Every scalar key, including `_id`.
110
135
  *
@@ -126,7 +151,32 @@ type PickedKeys<Q> = Q extends {
126
151
  * This is the one place `pick` is not literally what was asked for, which is
127
152
  * why it is stated in the docs rather than left to be discovered.
128
153
  */
129
- type ApplyPick<M extends FoirModelDefinition, Q> = Pick<M['fields'], Extract<PickedKeys<Q> | '_id', keyof M['fields']>>;
154
+ type ApplyPick<F, Q> = Pick<F, Extract<PickedKeys<Q> | '_id', keyof F>>;
155
+ /**
156
+ * The level a call asked for. `1` — the complete-by-default document — when
157
+ * `depth` is absent, which is what makes the default result type reach
158
+ * `M['fields']` unchanged.
159
+ *
160
+ * A non-literal `depth` (a variable typed `number`) infers `D` as `number`,
161
+ * which matches no key below and falls back to the default shape. That
162
+ * under-promises rather than guessing, and is why `depth` can stay `number` at
163
+ * the call site instead of a literal union that would have to be regenerated.
164
+ */
165
+ type DepthOf<Q> = Q extends {
166
+ depth: infer D extends number;
167
+ } ? D : 1;
168
+ /**
169
+ * The model's field shape at this call's depth.
170
+ *
171
+ * `M['fields']` BY IDENTITY whenever the model publishes no shape for the
172
+ * level — the default, every model without a stopping point, and any depth
173
+ * past what the generator emitted. `FoirRecordResult`'s identity property
174
+ * (a no-argument call hovers as `Product`, not as an anonymous object) is that
175
+ * fallback, so it holds for exactly the calls it held for before.
176
+ */
177
+ type FieldsAtDepth<M extends FoirModelDefinition, Depth extends number> = M extends {
178
+ atDepth: infer A;
179
+ } ? Depth extends keyof A ? A[Depth & keyof A] : M['fields'] : M['fields'];
130
180
  /**
131
181
  * One narrowing step for both verbs.
132
182
  *
@@ -134,7 +184,16 @@ type ApplyPick<M extends FoirModelDefinition, Q> = Pick<M['fields'], Extract<Pic
134
184
  * to decide which is present — `pick` first, and `omit` is then reached exactly
135
185
  * when `pick` is absent.
136
186
  */
137
- type ApplySelection<M extends FoirModelDefinition, Q> = [PickedKeys<Q>] extends [never] ? [SelectorOf<Q, 'pick'>] extends [never] ? [SelectorOf<Q, 'omit'>] extends [never] ? ApplyOmit<M, Q> : ApplyOmitSelector<M['fields'], SelectorOf<Q, 'omit'>> : ApplyPickSelector<M['fields'], SelectorOf<Q, 'pick'>> : ApplyPick<M, Q>;
187
+ type ApplySelection<M extends FoirModelDefinition, Q, Depth extends number> = ApplySelectionOn<FieldsAtDepth<M, Depth>, Q>;
188
+ /**
189
+ * The same two verbs, over an already-resolved field shape.
190
+ *
191
+ * Split out from `ApplySelection` only so the depth lookup is written ONCE. A
192
+ * conditional type has no `let`, so the alternative was `FieldsAtDepth<M, Depth>`
193
+ * spelled out in all four branches — four places for the two to drift apart,
194
+ * and the verbs stop being about `M` at all once the shape is chosen.
195
+ */
196
+ type ApplySelectionOn<F, Q> = [PickedKeys<Q>] extends [never] ? [SelectorOf<Q, 'pick'>] extends [never] ? [SelectorOf<Q, 'omit'>] extends [never] ? ApplyOmit<F, Q> : ApplyOmitSelector<F, SelectorOf<Q, 'omit'>> : ApplyPickSelector<F, SelectorOf<Q, 'pick'>> : ApplyPick<F, Q>;
138
197
  /**
139
198
  * True when `T` is a union.
140
199
  *
@@ -145,21 +204,111 @@ type ApplySelection<M extends FoirModelDefinition, Q> = [PickedKeys<Q>] extends
145
204
  type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
146
205
  type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
147
206
  /**
148
- * Whether a field's value may be descended into.
207
+ * A field's value with its list wrapper and nullability stripped — the thing a
208
+ * selection actually addresses.
209
+ *
210
+ * A block field is `Array<Block | null> | null`; what a caller narrows is
211
+ * `Block`. Every classifier below asks about this rather than about `V`, so a
212
+ * union in a list and a union on its own reach the same answer instead of the
213
+ * list check shadowing the union check.
214
+ */
215
+ type ElementOf<V> = NonNullable<V> extends readonly (infer E)[] ? NonNullable<E> : NonNullable<V>;
216
+ /**
217
+ * Put back the wrapper `ElementOf` took off.
218
+ *
219
+ * Narrowing changes the ELEMENT and must not change the shape around it: a
220
+ * pick inside `Array<Block | null> | null` still arrives as an array, still
221
+ * nullable, still with nullable elements. Reconstructing it here is what keeps
222
+ * the result type describing the response rather than describing the part of it
223
+ * the narrowing was about.
224
+ */
225
+ type Rewrap<V, T> = NonNullable<V> extends readonly (infer E)[] ? null extends E ? null extends V ? Array<T | null> | null : Array<T | null> : null extends V ? T[] | null : T[] : null extends V ? T | null : T;
226
+ /**
227
+ * A `__typename`-discriminated union — a polymorphic block field, in practice.
228
+ *
229
+ * This is the ONE union shape that can be narrowed safely, because
230
+ * `__typename` gives a closed set of names to key the narrowing on and
231
+ * `Extract<U, { __typename: T }>` recovers a member without ever reaching
232
+ * inside the union itself. Any other union stays undescendable; see
233
+ * `Descendable`.
234
+ */
235
+ type IsPolymorphic<V> = ElementOf<V> extends {
236
+ __typename: string;
237
+ } ? IsUnion<ElementOf<V>> : false;
238
+ /**
239
+ * Whether a field's value may be descended into with the verb.
240
+ *
241
+ * Objects yes — including a LIST of them, which this used to refuse. Refusing
242
+ * lists made `pageContent.sections.blocks` untypeable end to end even though the
243
+ * wire had always accepted it: a list is the only thing between a page and its
244
+ * blocks, so a list that could not be descended meant no block union could be
245
+ * reached at all.
246
+ *
247
+ * **Unions still no**, and the exclusion is unchanged in force even though the
248
+ * shape moved. `Pick` and `Omit` compile to `{ [P in K]: … }`, which is not
249
+ * homomorphic and does not distribute, and `keyof (A | B | C)` is the
250
+ * INTERSECTION of their keys — so narrowing inside a polymorphic block union
251
+ * with the verb collapses it to `{ __typename: 'A' | 'B' | 'C' }` and every
252
+ * branch's own fields vanish. That is Sanity's documented failure mode, and the
253
+ * union tests in `__tests__/selection.test-d.ts` go red the moment anything
254
+ * reaches inside a field's value type WITH A MAPPED TYPE OVER THE UNION.
255
+ *
256
+ * What changed is that a polymorphic union now has somewhere else to go: the
257
+ * `on` branch in `FoirSelector`, which distributes first and picks per member,
258
+ * so no mapped type ever sees the union whole. A union that is NOT
259
+ * `__typename`-discriminated has no such handle and is still `true`-only — the
260
+ * check here is `IsUnion`, not `IsPolymorphic`, precisely so that widening the
261
+ * polymorphic branch can never widen this one.
262
+ */
263
+ type Descendable<V> = IsUnion<ElementOf<V>> extends true ? false : ElementOf<V> extends object ? true : false;
264
+ /** The `__typename` literals of a polymorphic union. Closed, and generated. */
265
+ type MemberNames<U> = U extends {
266
+ __typename: infer T extends string;
267
+ } ? T : never;
268
+ /** The one member of `U` whose `__typename` is `T`. */
269
+ type MemberOf<U, T> = Extract<U, {
270
+ __typename: T;
271
+ }>;
272
+ /**
273
+ * The keys of one member a caller may name.
149
274
  *
150
- * Objects yes, **unions no**, and the exclusion is the whole point. `Pick` and
151
- * `Omit` compile to `{ [P in K]: … }`, which is not homomorphic and does not
152
- * distribute, and `keyof (A | B | C)` is the INTERSECTION of their keys — so
153
- * narrowing inside a polymorphic block union collapses it to
154
- * `{ __typename: 'A' | 'B' | 'C' }` and every branch's own fields vanish. That
155
- * is Sanity's documented failure mode, and the union tests in
156
- * `__tests__/selection.test-d.ts` go red the moment anything reaches inside a
157
- * field's value type.
275
+ * `__typename` is excluded because it is not optional: the printer emits it at
276
+ * the union level whatever the selection says, so offering it would suggest a
277
+ * choice that does not exist. Block-meta keys (`_id`, `_key`, `_type`,
278
+ * `_label`) ARE offered — unlike a record's `_id` the server does not force
279
+ * them inside a fragment, so naming them is the only way to keep them.
280
+ */
281
+ type MemberKeys<U, T> = Exclude<Extract<keyof MemberOf<U, T>, string>, '__typename'>;
282
+ /**
283
+ * The per-variant selector: name members, and for each either its keys or
284
+ * `true` for the whole member.
158
285
  *
159
- * The server will happily narrow inside a union; the types must not offer it.
160
- * Use GraphQL for that, which is what the transport-choice docs say.
286
+ * Every member is OPTIONAL, and what an absent one means is the verb's to
287
+ * decide — `pick` drops it, `omit` leaves it alone — matching what the printer
288
+ * does with a member the trie does not name.
161
289
  */
162
- type Descendable<V> = IsUnion<NonNullable<V>> extends true ? false : NonNullable<V> extends readonly unknown[] ? false : NonNullable<V> extends object ? true : false;
290
+ export type FoirVariantSelector<U> = {
291
+ [T in MemberNames<U>]?: true | readonly MemberKeys<U, T>[];
292
+ };
293
+ /**
294
+ * Apply a per-variant `pick`.
295
+ *
296
+ * A member the selector does not name arrives as `{ __typename }` alone rather
297
+ * than disappearing: the printer drops its FRAGMENT, but `__typename` is
298
+ * selected once at the union level, so the element is still there and still
299
+ * discriminates. Typing it as absent would describe a response with fewer
300
+ * elements than the server sends.
301
+ */
302
+ type ApplyVariantPick<U, S> = U extends {
303
+ __typename: infer T;
304
+ } ? T extends keyof S ? S[T] extends true ? U : S[T] extends readonly (infer K)[] ? Pick<U, Extract<K | '__typename', keyof U>> : Pick<U, Extract<'__typename', keyof U>> : Pick<U, Extract<'__typename', keyof U>> : never;
305
+ /**
306
+ * Apply a per-variant `omit`. The mirror of the above: an unnamed member is
307
+ * untouched, and `true` drops the whole fragment, leaving `{ __typename }`.
308
+ */
309
+ type ApplyVariantOmit<U, S> = U extends {
310
+ __typename: infer T;
311
+ } ? T extends keyof S ? S[T] extends true ? Pick<U, Extract<'__typename', keyof U>> : S[T] extends readonly (infer K)[] ? Omit<U, Extract<K, keyof U>> : U : U : never;
163
312
  /**
164
313
  * The nested selector: `true` for a leaf, or the verb again to descend.
165
314
  *
@@ -178,13 +327,15 @@ type Descendable<V> = IsUnion<NonNullable<V>> extends true ? false : NonNullable
178
327
  * an immediate "Type instantiation is excessively deep and possibly infinite".
179
328
  */
180
329
  export type FoirSelector<T, Verb extends 'pick' | 'omit', D extends number = SelectorDepth> = {
181
- [K in keyof T]?: Descendable<T[K]> extends true ? D extends 0 ? true : true | {
182
- [P in Verb]: FoirSelector<NonNullable<T[K]>, Verb, Decrement[D]>;
330
+ [K in keyof T]?: IsPolymorphic<T[K]> extends true ? true | {
331
+ on: FoirVariantSelector<ElementOf<T[K]>>;
332
+ } : Descendable<T[K]> extends true ? D extends 0 ? true : true | {
333
+ [P in Verb]: FoirSelector<ElementOf<T[K]>, Verb, Decrement[D]>;
183
334
  } : true;
184
335
  };
185
336
  /**
186
337
  * How deep a selector may nest. Three, matching `IncludeDepth`, and comfortably
187
- * past what real composites do — `ImageValue.variants.thumbnail` is the deepest
338
+ * past what real composites do — `Image.variants.thumbnail` is the deepest
188
339
  * shape on any live model at two.
189
340
  */
190
341
  type SelectorDepth = 3;
@@ -195,9 +346,7 @@ type SelectorOf<Q, Verb extends 'pick' | 'omit'> = Q extends Record<Verb, infer
195
346
  * reason it does in the flat form — the server always emits it.
196
347
  */
197
348
  type ApplyPickSelector<F, S> = {
198
- [K in Extract<keyof S, keyof F> | Extract<'_id', keyof F>]: K extends keyof S ? S[K] extends {
199
- pick: infer Inner;
200
- } ? PickSelectorDeep<NonNullable<F[K]>, Inner> : F[K] : F[K];
349
+ [K in Extract<keyof S, keyof F> | Extract<'_id', keyof F>]: K extends keyof S ? NarrowPick<F[K], S[K]> : F[K];
201
350
  };
202
351
  /**
203
352
  * The same walk one level in. Separate from the root because `_id` is forced at
@@ -205,10 +354,28 @@ type ApplyPickSelector<F, S> = {
205
354
  * response shape the server never sends.
206
355
  */
207
356
  type PickSelectorDeep<F, S> = {
208
- [K in Extract<keyof S, keyof F>]: S[K] extends {
209
- pick: infer Inner;
210
- } ? PickSelectorDeep<NonNullable<F[K]>, Inner> : F[K];
357
+ [K in Extract<keyof S, keyof F>]: NarrowPick<F[K], S[K]>;
211
358
  };
359
+ /**
360
+ * One field's value under a `pick` selector: split a union, descend an object
361
+ * or a list of them, or leave a leaf alone.
362
+ *
363
+ * `Rewrap` is what makes the list case honest. Descending `sections` narrows
364
+ * each Section; the caller still receives the array, still nullable, with
365
+ * still-nullable elements, and reconstructing that is the difference between
366
+ * typing the response and typing the interesting part of it.
367
+ */
368
+ type NarrowPick<V, Sel> = Sel extends {
369
+ on: infer Variants;
370
+ } ? Rewrap<V, ApplyVariantPick<ElementOf<V>, Variants>> : Sel extends {
371
+ pick: infer Inner;
372
+ } ? Rewrap<V, PickSelectorDeep<ElementOf<V>, Inner>> : V;
373
+ /** The `omit` half of `NarrowPick`. */
374
+ type NarrowOmit<V, Sel> = Sel extends {
375
+ on: infer Variants;
376
+ } ? Rewrap<V, ApplyVariantOmit<ElementOf<V>, Variants>> : Sel extends {
377
+ omit: infer Inner;
378
+ } ? Rewrap<V, ApplyOmitSelector<ElementOf<V>, Inner>> : V;
212
379
  /**
213
380
  * Apply a nested `omit` selector.
214
381
  *
@@ -217,9 +384,7 @@ type PickSelectorDeep<F, S> = {
217
384
  * able to shed one richtext encoding of three without losing the field.
218
385
  */
219
386
  type ApplyOmitSelector<F, S> = {
220
- [K in keyof F as K extends keyof S ? (S[K] extends true ? never : K) : K]: K extends keyof S ? S[K] extends {
221
- omit: infer Inner;
222
- } ? ApplyOmitSelector<NonNullable<F[K]>, Inner> : F[K] : F[K];
387
+ [K in keyof F as K extends keyof S ? (S[K] extends true ? never : K) : K]: K extends keyof S ? NarrowOmit<F[K], S[K]> : F[K];
223
388
  };
224
389
  /**
225
390
  * The `include` argument for a model: every relation key, opt-in, optionally
@@ -235,8 +400,8 @@ export type FoirInclude<S extends FoirSchemaDefinition, M extends FoirModelDefin
235
400
  };
236
401
  };
237
402
  /** The relation keys a call asked for, shaped by list/nullable and nesting. */
238
- type IncludedRelations<S extends FoirSchemaDefinition, M extends FoirModelDefinition, I, D extends number> = {
239
- [K in Extract<keyof I, keyof M['relations']>]: WrapRelation<M['relations'][K], FoirRecordResult<S, TargetModel<S, M['relations'][K]>, I[K], Decrement[D]>>;
403
+ type IncludedRelations<S extends FoirSchemaDefinition, M extends FoirModelDefinition, I, D extends number, Depth extends number> = {
404
+ [K in Extract<keyof I, keyof M['relations']>]: WrapRelation<M['relations'][K], FoirRecordResult<S, TargetModel<S, M['relations'][K]>, I[K], Decrement[D], Depth>>;
240
405
  };
241
406
  type WrapRelation<R extends FoirRelationDefinition, T> = R['list'] extends true ? R['nullable'] extends true ? T[] | null : T[] : R['nullable'] extends true ? T | null : T;
242
407
  /**
@@ -253,10 +418,16 @@ type WrapRelation<R extends FoirRelationDefinition, T> = R['list'] extends true
253
418
  * reads well and names the generated interface — and, more to the point, it
254
419
  * means the only mapped type anywhere near a result is `Omit`. One less thing
255
420
  * between the generator's union and the call site.
421
+ *
422
+ * `Depth` is the call's union-level budget, defaulted FROM `Q` so every
423
+ * existing reference keeps working unchanged. It is passed to the relations
424
+ * UNDECREMENTED because the printer does the same: `printIncludes` carries the
425
+ * caller's depth into a relation's tree without spending any of it, so an
426
+ * included record's stopping points expand at exactly the level the root's do.
256
427
  */
257
- export type FoirRecordResult<S extends FoirSchemaDefinition, M extends FoirModelDefinition, Q, D extends number = IncludeDepth> = Q extends {
428
+ export type FoirRecordResult<S extends FoirSchemaDefinition, M extends FoirModelDefinition, Q, D extends number = IncludeDepth, Depth extends number = DepthOf<Q>> = Q extends {
258
429
  include: infer I;
259
- } ? ApplySelection<M, Q> & IncludedRelations<S, M, I, D> : ApplySelection<M, Q>;
430
+ } ? ApplySelection<M, Q, Depth> & IncludedRelations<S, M, I, D, Depth> : ApplySelection<M, Q, Depth>;
260
431
  /** Apply an op's return wrapper to the shaped record. */
261
432
  export type FoirOpResult<S extends FoirSchemaDefinition, M extends FoirModelDefinition, O extends FoirOpDefinition, Q> = O['returns'] extends {
262
433
  kind: 'opaque';
@@ -270,6 +441,29 @@ export type FoirOpResult<S extends FoirSchemaDefinition, M extends FoirModelDefi
270
441
  kind: 'record';
271
442
  nullable: infer N;
272
443
  } ? N extends true ? FoirRecordResult<S, M, Q> | null : FoirRecordResult<S, M, Q> : never;
444
+ /**
445
+ * The depth budget, as a call option.
446
+ *
447
+ * A sibling of `include`/`omit`/`pick` rather than an argument: it shapes the
448
+ * DOCUMENT the client prints, and no root field declares an argument by that
449
+ * name. `number` rather than a literal union of the levels that fit, because
450
+ * what fits is a property of the PROJECT — the same level is comfortable on one
451
+ * model and over the ceiling on another — so the bound is a runtime refusal
452
+ * carrying the arithmetic, not a type that would have to be regenerated to stay
453
+ * true and would be wrong between runs.
454
+ *
455
+ * The RESULT type follows it. A stopping point the level reaches is typed as
456
+ * the expanded union rather than `FoirDegraded`, so `depth: 2` reads a nested
457
+ * member's own field with a `__typename` narrowing and no cast. The generator
458
+ * threads a level parameter through the composites on the path and names the
459
+ * instantiations under `FoirModelDefinition.atDepth`; `FieldsAtDepth` picks
460
+ * one. A LITERAL is what makes that lookup work — a `depth` read from a
461
+ * variable is `number`, matches no level, and falls back to the default shape,
462
+ * which under-promises rather than guessing.
463
+ */
464
+ interface FoirDepthOption {
465
+ depth?: number;
466
+ }
273
467
  /**
274
468
  * What a model op accepts: the op's own args with `include`/`omit` as siblings.
275
469
  * They are split back apart onto the wire (`{ model, op, args, include, omit }`)
@@ -281,7 +475,7 @@ export type FoirOpResult<S extends FoirSchemaDefinition, M extends FoirModelDefi
281
475
  */
282
476
  export type FoirQuery<S extends FoirSchemaDefinition, M extends FoirModelDefinition, O extends FoirOpDefinition> = O['returns'] extends {
283
477
  kind: 'opaque';
284
- } ? O['args'] : O['args'] & ({
478
+ } ? O['args'] : O['args'] & FoirDepthOption & ({
285
479
  include?: FoirInclude<S, M>;
286
480
  omit?: readonly FoirOmittableKey<M>[] | FoirSelector<M['fields'], 'omit'>;
287
481
  /**
@@ -0,0 +1,221 @@
1
+ /**
2
+ * The subject registry — which records this client fetched.
3
+ *
4
+ * ---------------------------------------------------------------------------
5
+ * What it is for
6
+ * ---------------------------------------------------------------------------
7
+ *
8
+ * An annotation surface — a comment widget, a feedback button, a review prompt
9
+ * — has to say what it is about. Every tool that does this anchors to a URL or
10
+ * to a CSS selector, and both break the moment the page is redesigned, the
11
+ * route changes, or the reader is served a different locale or variant.
12
+ *
13
+ * A page rendered through this client does not have that problem, because the
14
+ * client already knows: it printed the document, it named the model, and every
15
+ * record in the response carries `_id` (the selection model protects it — see
16
+ * `PROTECTED_ROOT_FIELD`). So the anchor can be the RECORD, which survives all
17
+ * three, and it can be derived rather than configured.
18
+ *
19
+ * ```ts
20
+ * const subjects = createFoirSubjectRegistry();
21
+ * const foir = createFoirClient({ apiUrl, apiKey, generated, subjects });
22
+ *
23
+ * await foir.article.findUnique({ naturalKey: 'about-us' });
24
+ * subjects.subject(); // { modelKey: 'article', recordId: 'kx7p…' }
25
+ * ```
26
+ *
27
+ * Nothing here maps a URL, and nothing here may start to. A registry that fell
28
+ * back to the address bar would reintroduce exactly the fragility the record
29
+ * anchor exists to remove, and it would do so silently — the anchor would look
30
+ * identical and would point somewhere else after the next redesign.
31
+ *
32
+ * ---------------------------------------------------------------------------
33
+ * Why it is opt-in, and off unless you build one
34
+ * ---------------------------------------------------------------------------
35
+ *
36
+ * A list of the records a page fetched is a DISCLOSURE SURFACE. It names
37
+ * content by model key and id, including records a visitor never saw rendered —
38
+ * a draft pulled for an editor, a related record fetched and then filtered out
39
+ * in the template, the twelve nav pages behind a menu. Handing that to whatever
40
+ * else runs on the page (an analytics tag, an embedded widget, a browser
41
+ * extension) is a decision with consequences, and it is not this package's
42
+ * decision to make.
43
+ *
44
+ * So it is not a flag that defaults to false — it is an OBJECT the host
45
+ * constructs and owns. There is no configuration that turns it on by accident,
46
+ * the host holds the only reference, and its lifetime is the host's: a registry
47
+ * built per request on an edge runtime cannot outlive that request, which is the
48
+ * same reason nothing in this client keeps module-level state.
49
+ *
50
+ * The corollary is the one warning worth repeating: a registry built at MODULE
51
+ * scope on a multi-tenant edge worker is shared across tenants, exactly like a
52
+ * module-level response cache would be. Build one per client.
53
+ *
54
+ * ---------------------------------------------------------------------------
55
+ * Where the subjects come from
56
+ * ---------------------------------------------------------------------------
57
+ *
58
+ * From the plan, not from the response's shape. `planCall` derives the record
59
+ * positions from the payload the manifest published (`FoirDocumentPlan.records`)
60
+ * — the same walk that decides where `pick`/`omit` land — so the registry reads
61
+ * the response at the positions the server said records would be, rather than
62
+ * hunting for objects that happen to carry an `_id`.
63
+ */
64
+ import { type FoirDocumentPlan } from './document';
65
+ /**
66
+ * One thing an annotation can be about.
67
+ *
68
+ * `recordId` / `modelKey` and not `id` / `model`, matching
69
+ * `FoirRecordChangedEvent` in `subscriptions.ts`: a subscription event and a
70
+ * fetched subject name the same pair, and two spellings of one pair is how a
71
+ * consumer ends up writing an adapter between two halves of one SDK.
72
+ */
73
+ export interface FoirSubject {
74
+ /** The record's model key, e.g. `article`. */
75
+ modelKey: string;
76
+ /** The record's `_id`. */
77
+ recordId: string;
78
+ /**
79
+ * A human label for the record, when one is available. See
80
+ * `FoirSubjectOptions.label` for where it comes from and why the client does
81
+ * not pick a field on its own.
82
+ */
83
+ label?: string;
84
+ }
85
+ /**
86
+ * How a record reached the registry.
87
+ *
88
+ * NOT a heuristic: it is read off the op's published payload. A payload whose
89
+ * kind is `record` IS one record, which means the caller addressed one record by
90
+ * name (`findUnique`, `create`, a lookup); a record reached through a field of
91
+ * an `object` payload arrived inside something — a connection's `edges.node`, a
92
+ * search wrapper's `record`.
93
+ *
94
+ * That distinction is what lets `subject()` answer at all. A page renders FROM
95
+ * the record it addressed and merely LISTS the rest, and the manifest already
96
+ * says which is which, so the client does not have to guess from result counts
97
+ * or field names.
98
+ */
99
+ export type FoirSubjectOrigin = 'record' | 'collection';
100
+ /** What `FoirSubjectRegistry.add` takes. */
101
+ export interface FoirSubjectEntry {
102
+ modelKey: string;
103
+ recordId: string;
104
+ /**
105
+ * Defaults to `record`, which is what a hand-seeded entry means: you named
106
+ * this record deliberately. The data path passes the origin it read off the
107
+ * plan.
108
+ */
109
+ origin?: FoirSubjectOrigin;
110
+ /**
111
+ * The record as it arrived, for the labeller to read. Optional, because a
112
+ * host seeding a subject from an id has no record object to hand.
113
+ */
114
+ record?: Readonly<Record<string, unknown>>;
115
+ /** An explicit label. Wins over the labeller and over `_naturalKey`. */
116
+ label?: string;
117
+ }
118
+ export interface FoirSubjectOptions {
119
+ /**
120
+ * Turn a record into the label a widget shows: `"About Us"` rather than
121
+ * `article/kx7p…`.
122
+ *
123
+ * There is no default that reads one of your fields, and that is deliberate.
124
+ * The manifest publishes a model's scalars and their types; it does not
125
+ * publish which one is the title, because there is no such concept — one
126
+ * project calls it `title`, the next `name`, the next `headline`. A client
127
+ * that picked `title` when it saw one would label most projects correctly and
128
+ * mislabel the rest, silently, in the one string a reader actually reads.
129
+ *
130
+ * So the field is yours to name, in one line:
131
+ *
132
+ * ```ts
133
+ * createFoirSubjectRegistry({
134
+ * label: (record) => (record.title as string) ?? null,
135
+ * });
136
+ * ```
137
+ *
138
+ * Return `null` or `undefined` for "no label" — the subject is still
139
+ * recorded, just without one. It must not throw: see `recordFoirSubjects`.
140
+ */
141
+ label?: (record: Readonly<Record<string, unknown>>, subject: FoirSubject) => string | null | undefined;
142
+ }
143
+ export interface FoirSubjectRegistry {
144
+ /**
145
+ * Record one subject.
146
+ *
147
+ * The data path calls this for you. Call it yourself to seed a subject this
148
+ * client did not fetch — a server-rendered page whose record was read on the
149
+ * server, or a record that arrived through `foir.raw()`, which prints no
150
+ * published payload and therefore has no record positions to read.
151
+ */
152
+ add(entry: FoirSubjectEntry): void;
153
+ /**
154
+ * Every record recorded so far, de-duplicated on `modelKey` + `recordId` and
155
+ * ordered least-recently-fetched first — so the last entry is the last record
156
+ * this client saw, whether or not it was addressed by name.
157
+ *
158
+ * This is what a widget offers when it wants to let the reader choose, and it
159
+ * is the honest answer for a page whose data all came out of lists.
160
+ */
161
+ fetched(): readonly FoirSubject[];
162
+ /**
163
+ * The subject to annotate: your override if you set one, otherwise the last
164
+ * record this client addressed BY NAME.
165
+ *
166
+ * One rule, and no case analysis behind it. A record that only ever appeared
167
+ * inside a list is never promoted — a page that fetched twelve nav entries and
168
+ * nothing else has no subject, and saying so is better than picking one of the
169
+ * twelve. Use `fetched()` or `setSubject()` there.
170
+ */
171
+ subject(): FoirSubject | undefined;
172
+ /**
173
+ * Pin the subject, overriding whatever was fetched. `null` drops the override
174
+ * and goes back to the fetched answer.
175
+ */
176
+ setSubject(subject: FoirSubject | null): void;
177
+ /**
178
+ * Forget everything: the recorded records AND the override.
179
+ *
180
+ * A single-page app must call this when it navigates. Without it the registry
181
+ * accumulates every record the session ever touched and `subject()` keeps
182
+ * answering with a record from a page the reader has left.
183
+ */
184
+ clear(): void;
185
+ /**
186
+ * Run `listener` after every change, and return the function that stops it.
187
+ *
188
+ * For a widget mounted before the data resolves. Listeners run synchronously
189
+ * in registration order; one that throws does not stop the others, and the
190
+ * failure is reported rather than dropped (see `recordFoirSubjects`).
191
+ */
192
+ subscribe(listener: () => void): () => void;
193
+ }
194
+ /**
195
+ * Build a registry.
196
+ *
197
+ * Per client, never at module scope — see the file header. It is handed to
198
+ * `createFoirClient` as `subjects`, and the host keeps the same reference to
199
+ * read from.
200
+ */
201
+ export declare function createFoirSubjectRegistry(options?: FoirSubjectOptions): FoirSubjectRegistry;
202
+ /**
203
+ * Record what one call fetched.
204
+ *
205
+ * A no-op without a registry, which is what "off by default" means at runtime:
206
+ * a client built without `subjects` walks nothing and allocates nothing.
207
+ *
208
+ * # Errors are not swallowed here
209
+ *
210
+ * The only things that can fail are the host's own: a `label` function, or a
211
+ * `subscribe` listener. Both are re-thrown, wrapped in a message that says the
212
+ * REQUEST succeeded so the failure cannot be read as a transport fault.
213
+ *
214
+ * Catching them would be worse than it looks. A subject that silently failed to
215
+ * record does not degrade to "no annotation offered" — it degrades to the
216
+ * PREVIOUS subject still being the answer, so the widget offers to comment on
217
+ * the record the reader was looking at a moment ago. A loud failure in a
218
+ * function the host wrote is strictly better than an annotation filed against
219
+ * the wrong record.
220
+ */
221
+ export declare function recordFoirSubjects(registry: FoirSubjectRegistry | undefined, plan: Pick<FoirDocumentPlan, 'records'>, result: unknown): void;