@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.
- package/CHANGELOG.md +857 -4
- package/README.md +46 -15
- package/dist/auth/auth.d.ts +104 -1
- package/dist/auth/hosted.d.ts +22 -0
- package/dist/auth/index.d.ts +13 -7
- package/dist/auth/operations.d.ts +85 -64
- package/dist/auth/ops.d.ts +99 -0
- package/dist/auth.js +786 -106
- package/dist/chunk-IGNLWT3H.js +35 -0
- package/dist/chunk-OHYVKTGB.js +45 -0
- package/dist/chunk-TAL5H6VA.js +39 -0
- package/dist/client.js +25 -31
- package/dist/codegen/block-registry.d.ts +13 -19
- package/dist/codegen/block-registry.js +3 -31
- package/dist/codegen/block-scaffold.d.ts +12 -21
- package/dist/codegen/block-scaffold.js +3 -41
- package/dist/codegen/index.d.ts +9 -4
- package/dist/codegen/index.js +7 -11
- package/dist/codegen/item-registry.d.ts +28 -0
- package/dist/codegen/item-registry.js +7 -0
- package/dist/codegen/item-scaffold.d.ts +29 -0
- package/dist/codegen/item-scaffold.js +7 -0
- package/dist/codegen/preset.d.ts +14 -7
- package/dist/codegen/shared.d.ts +20 -9
- package/dist/content/content-renderer.d.ts +90 -24
- package/dist/content/index.d.ts +13 -7
- package/dist/content/richtext.d.ts +66 -18
- package/dist/content.js +227 -80
- package/dist/data/builtins.d.ts +369 -92
- package/dist/data/client.d.ts +97 -25
- package/dist/data/document.d.ts +251 -0
- package/dist/data/index.d.ts +38 -19
- package/dist/data/manifest.d.ts +290 -8
- package/dist/data/schema.d.ts +78 -8
- package/dist/data/selection.d.ts +231 -37
- package/dist/data/subjects.d.ts +221 -0
- package/dist/data/subscription-document.d.ts +75 -0
- package/dist/data/subscriptions.d.ts +181 -0
- package/dist/data/transport.d.ts +255 -37
- package/dist/data/ws-transport.d.ts +197 -0
- package/dist/data.js +1419 -45
- package/dist/graphql/cdn-route.d.ts +12 -28
- package/dist/media/index.d.ts +6 -2
- package/dist/media/media.d.ts +2 -2
- package/dist/media/screen-capture.d.ts +81 -0
- package/dist/media.js +97 -0
- package/dist/next.js +663 -182
- package/dist/push.js +700 -90
- package/dist/react/FoirFeedback.d.ts +62 -0
- package/dist/react/feedback-notes.d.ts +101 -0
- package/dist/react/index.d.ts +7 -1
- package/dist/react.js +1272 -194
- package/dist/realtime.js +1 -1
- package/dist/server/events-client.d.ts +279 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server.js +298 -0
- package/dist/transport/foir-http.d.ts +23 -6
- package/dist/transport/persisted-query.d.ts +68 -0
- package/dist/transport/retry.d.ts +1 -1
- package/package.json +13 -1
- package/dist/chunk-ADO5FJNM.js +0 -30
package/dist/data/selection.d.ts
CHANGED
|
@@ -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
|
|
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.
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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<
|
|
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<
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
*
|
|
160
|
-
*
|
|
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
|
|
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]?:
|
|
182
|
-
|
|
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 — `
|
|
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]
|
|
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]
|
|
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]
|
|
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
|
|
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;
|