@foir/sdk 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +805 -4
  2. package/README.md +46 -15
  3. package/dist/auth/auth.d.ts +104 -1
  4. package/dist/auth/index.d.ts +12 -6
  5. package/dist/auth/operations.d.ts +85 -64
  6. package/dist/auth/ops.d.ts +99 -0
  7. package/dist/auth.js +775 -106
  8. package/dist/chunk-IGNLWT3H.js +35 -0
  9. package/dist/chunk-OHYVKTGB.js +45 -0
  10. package/dist/chunk-TAL5H6VA.js +39 -0
  11. package/dist/client.js +25 -31
  12. package/dist/codegen/block-registry.d.ts +13 -19
  13. package/dist/codegen/block-registry.js +3 -31
  14. package/dist/codegen/block-scaffold.d.ts +12 -21
  15. package/dist/codegen/block-scaffold.js +3 -41
  16. package/dist/codegen/index.d.ts +9 -4
  17. package/dist/codegen/index.js +7 -11
  18. package/dist/codegen/item-registry.d.ts +28 -0
  19. package/dist/codegen/item-registry.js +7 -0
  20. package/dist/codegen/item-scaffold.d.ts +29 -0
  21. package/dist/codegen/item-scaffold.js +7 -0
  22. package/dist/codegen/preset.d.ts +14 -7
  23. package/dist/codegen/shared.d.ts +20 -9
  24. package/dist/content/content-renderer.d.ts +90 -24
  25. package/dist/content/index.d.ts +13 -7
  26. package/dist/content/richtext.d.ts +66 -18
  27. package/dist/content.js +227 -80
  28. package/dist/data/builtins.d.ts +302 -92
  29. package/dist/data/client.d.ts +97 -25
  30. package/dist/data/document.d.ts +251 -0
  31. package/dist/data/index.d.ts +38 -19
  32. package/dist/data/manifest.d.ts +266 -3
  33. package/dist/data/schema.d.ts +78 -8
  34. package/dist/data/selection.d.ts +231 -37
  35. package/dist/data/subjects.d.ts +221 -0
  36. package/dist/data/subscription-document.d.ts +75 -0
  37. package/dist/data/subscriptions.d.ts +181 -0
  38. package/dist/data/transport.d.ts +255 -37
  39. package/dist/data/ws-transport.d.ts +197 -0
  40. package/dist/data.js +1419 -45
  41. package/dist/graphql/cdn-route.d.ts +12 -28
  42. package/dist/media/index.d.ts +6 -2
  43. package/dist/media/media.d.ts +2 -2
  44. package/dist/media/screen-capture.d.ts +81 -0
  45. package/dist/media.js +97 -0
  46. package/dist/next.js +663 -182
  47. package/dist/push.js +700 -90
  48. package/dist/react/FoirFeedback.d.ts +62 -0
  49. package/dist/react/feedback-notes.d.ts +101 -0
  50. package/dist/react/index.d.ts +7 -1
  51. package/dist/react.js +1272 -194
  52. package/dist/realtime.js +1 -1
  53. package/dist/server/events-client.d.ts +279 -0
  54. package/dist/server/index.d.ts +4 -0
  55. package/dist/server.js +298 -0
  56. package/dist/transport/foir-http.d.ts +23 -6
  57. package/dist/transport/persisted-query.d.ts +68 -0
  58. package/dist/transport/retry.d.ts +1 -1
  59. package/package.json +13 -1
  60. package/dist/chunk-ADO5FJNM.js +0 -30
@@ -11,9 +11,20 @@
11
11
  * What the generator emits
12
12
  * ---------------------------------------------------------------------------
13
13
  *
14
- * A single module, `foir.generated.ts`. A `.ts` and not a `.d.ts` for exactly
15
- * one reason: `foirSchemaVersion` has to survive to runtime for drift
16
- * detection. Everything else in the file is types and erases to nothing.
14
+ * A single module, `foir.generated.ts`, PASSED WHOLE:
15
+ *
16
+ * ```ts
17
+ * import * as generated from './foir.generated';
18
+ * const foir = createFoirClient({ apiUrl, apiKey, generated });
19
+ * ```
20
+ *
21
+ * A `.ts` and not a `.d.ts` because the file has a runtime half — the selection
22
+ * trees, `foirSchemaVersion`, and `foirSchemaType`, which is what carries
23
+ * `FoirSchema` across the type/value boundary so the client can infer it (a
24
+ * namespace import carries values; an interface erases). `FoirGeneratedModule`
25
+ * in `transport.ts` is the shape those exports have to satisfy, and it requires
26
+ * all of them: there is no case where a subset is wanted, and accepting one
27
+ * quietly meant a client that fetched the manifest on every cold start instead.
17
28
  *
18
29
  * ```ts
19
30
  * // foir.generated.ts — DO NOT EDIT. Generated by `foir types`.
@@ -23,14 +34,26 @@
23
34
  * export const foirSchemaVersion = 317;
24
35
  * export const foirSchemaChannel = 'published';
25
36
  *
26
- * export interface TillyBlock {
37
+ * // `<D>` ONLY on the types between the record and a union stopping point —
38
+ * // see rule 10. Everything else is emitted without it.
39
+ * export interface TillyBlock<D extends FoirDepthLevel = 1> {
27
40
  * _id: string;
28
41
  * _createdAt: string;
29
42
  * text: string | null;
30
43
  * // Composite/polymorphic fields are emitted as a STATIC discriminated
31
44
  * // union on `__typename`. The client never infers these; it only has to
32
45
  * // avoid destroying them. See `selection.ts`.
33
- * body: Array<HeroBlock | TextBlock> | null;
46
+ * body: Array<HeroBlock<D> | TextBlock> | null;
47
+ * }
48
+ *
49
+ * export interface HeroBlock<D extends FoirDepthLevel = 1> {
50
+ * __typename: 'HeroBlock';
51
+ * headline: string;
52
+ * // The stopping point: a union reached from inside a union member is
53
+ * // already on the op compiler's path, so it degrades — unless the caller
54
+ * // bought a level. Mirrors the printer's `depth - 1` at this position.
55
+ * body: Array<(D extends 1 ? TillyBlockBodyDegraded
56
+ * : TillyBlockBody<FoirDepthBelow[D]>) | null> | null;
34
57
  * }
35
58
  *
36
59
  * export interface TillyBlockFindUniqueArgs {
@@ -54,6 +77,8 @@
54
77
  * tilly_block: {
55
78
  * typeName: 'TillyBlock';
56
79
  * fields: TillyBlock;
80
+ * // Rule 10. Absent on a model with no stopping point, which is most.
81
+ * atDepth: { 2: TillyBlock<2>; 3: TillyBlock<3>; 4: TillyBlock<4> };
57
82
  * relations: {
58
83
  * parentBlockId: { target: 'tilly_block'; list: false; nullable: true };
59
84
  * };
@@ -80,6 +105,10 @@
80
105
  * };
81
106
  * };
82
107
  * }
108
+ *
109
+ * // The type carrier. `FoirSchema` is an interface and erases, so this is the
110
+ * // only thing a namespace import can infer it from. Runtime value: undefined.
111
+ * export const foirSchemaType = undefined as unknown as FoirSchema;
83
112
  * ```
84
113
  *
85
114
  * ---------------------------------------------------------------------------
@@ -87,9 +116,9 @@
87
116
  * ---------------------------------------------------------------------------
88
117
  *
89
118
  * 1. **Model keys are the manifest's model keys verbatim** (`tilly_block`, not
90
- * `tillyBlock`). They are what `POST /v1/query` takes in `model`, and the
91
- * client forwards the property name straight onto the wire. Renaming them
92
- * for aesthetics breaks the request.
119
+ * `tillyBlock`). They are the key the client looks the model's selection
120
+ * tree up by, and the key a refusal names when there is no tree to look up.
121
+ * Renaming them for aesthetics breaks the lookup.
93
122
  *
94
123
  * 2. **Op names are the manifest's op names verbatim** (`findUnique`,
95
124
  * `findMany`, `search`, …) — the canonical names, NOT the GraphQL root
@@ -144,6 +173,27 @@
144
173
  * (as the sketch above does) is the safe shape; if the generator ever
145
174
  * factors these out for readability, they have to be `type X = {…}`.
146
175
  * `__tests__/fixture-schema.ts` is the compiling reference.
176
+ *
177
+ * 10. **The `<D>` level parameter goes on the path to a stopping point, and
178
+ * nowhere else.** A union reached from inside a union member is already on
179
+ * the op compiler's path, so it degrades to `__typename` — and `depth: 2`
180
+ * buys one such level back, on the wire AND in the type. The parameter is
181
+ * what carries the caller's level from the record down to that position,
182
+ * so it belongs on exactly the interfaces, union aliases and member
183
+ * projections between the two, and the model names its shape at each level
184
+ * under `atDepth`.
185
+ *
186
+ * Both halves of "and nowhere else" matter. A parameter on a type that
187
+ * reaches no stopping point is a generic instantiation at every reference
188
+ * that can only ever resolve to the default — measured at 7 interfaces of
189
+ * 50 on the live dev project, which is why the change costs +1.6% types
190
+ * rather than a multiple. And `= 1` on every one of them is what makes a
191
+ * call passing no `depth` resolve to the type it always did; a parameter
192
+ * without a default would be a breaking change to every generated name.
193
+ *
194
+ * The generator computes the set (`depthReach`); it is not a judgement
195
+ * call. See `builtins.ts` for `FoirDepthLevel` / `FoirDepthBelow`, and
196
+ * `selection.ts` for the single lookup that reads `atDepth`.
147
197
  */
148
198
  /**
149
199
  * A model's complete scalar shape. Deliberately loose — the generator writes a
@@ -191,6 +241,26 @@ export interface FoirModelDefinition {
191
241
  readonly typeName: string;
192
242
  /** Complete scalar shape. See contract rule 4. */
193
243
  readonly fields: FoirFieldsShape;
244
+ /**
245
+ * The same shape at a depth ABOVE the default, keyed by the level a caller
246
+ * would pass as `depth`.
247
+ *
248
+ * Present only on models whose tree reaches a union stopping point — a
249
+ * position the op compiler refuses to expand because the union is already on
250
+ * its path. `depth: 2` expands one such level on the wire, and the entry
251
+ * under `2` is the model type describing what then arrives.
252
+ *
253
+ * OPTIONAL, and absent is the common case. A model with no stopping point
254
+ * reads identically at every depth, so generating an entry for it would emit
255
+ * a second name for one type. A LEVEL with no entry — a depth past what the
256
+ * generator emitted, or a `depth` that is not a literal at the call site —
257
+ * falls back to `fields`, which under-promises: the caller gets the values
258
+ * and narrows by hand, exactly as they did before this key existed. That is
259
+ * the safe direction, and the reason absence never has to be an error.
260
+ *
261
+ * Contract rule 9 applies: emit it as an object type LITERAL.
262
+ */
263
+ readonly atDepth?: Readonly<Record<number, FoirFieldsShape>>;
194
264
  readonly relations: Readonly<Record<string, FoirRelationDefinition>>;
195
265
  readonly ops: Readonly<Record<string, FoirOpDefinition>>;
196
266
  }
@@ -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
  /**