@foir/sdk 0.5.0 → 0.7.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 (82) hide show
  1. package/CHANGELOG.md +325 -0
  2. package/README.md +63 -0
  3. package/dist/auth/auth.d.ts +140 -0
  4. package/dist/auth/hosted.d.ts +40 -0
  5. package/dist/auth/index.d.ts +15 -0
  6. package/dist/auth/operations.d.ts +123 -0
  7. package/dist/auth/session-client.d.ts +76 -0
  8. package/dist/auth-session.js +137 -0
  9. package/dist/auth.js +238 -26
  10. package/dist/client/platform-client.d.ts +29 -0
  11. package/dist/client.js +281 -37
  12. package/dist/codegen/block-registry.d.ts +4 -8
  13. package/dist/codegen/block-scaffold.d.ts +4 -8
  14. package/dist/codegen/index.d.ts +8 -62
  15. package/dist/codegen/preset.d.ts +60 -0
  16. package/dist/codegen/shared.d.ts +25 -0
  17. package/dist/components/EditorProvider.d.ts +36 -0
  18. package/dist/content/content-renderer.d.ts +47 -0
  19. package/dist/content/index.d.ts +12 -0
  20. package/dist/content/richtext.d.ts +70 -0
  21. package/dist/data/builtins.d.ts +296 -0
  22. package/dist/data/client.d.ts +94 -0
  23. package/dist/data/index.d.ts +54 -0
  24. package/dist/data/manifest.d.ts +161 -0
  25. package/dist/data/schema.d.ts +243 -0
  26. package/dist/data/selection.d.ts +330 -0
  27. package/dist/data/transport.d.ts +107 -0
  28. package/dist/data.js +309 -0
  29. package/dist/graphql/cdn-route.d.ts +42 -0
  30. package/dist/graphql/client.d.ts +30 -0
  31. package/dist/graphql/content-client.d.ts +66 -0
  32. package/dist/graphql/foir-request.d.ts +47 -0
  33. package/dist/hooks/useAutoResize.d.ts +22 -0
  34. package/dist/hooks/useEditor.d.ts +16 -0
  35. package/dist/index.d.ts +10 -394
  36. package/dist/media/BlurhashCanvas.d.ts +10 -0
  37. package/dist/media/ProgressiveMedia.d.ts +56 -0
  38. package/dist/media/index.d.ts +14 -0
  39. package/dist/media/media.d.ts +139 -0
  40. package/dist/media/useProgressiveLoad.d.ts +23 -0
  41. package/dist/media.js +5 -0
  42. package/dist/next/cookieAdapter.d.ts +26 -0
  43. package/dist/next/index.d.ts +15 -0
  44. package/dist/next/middleware.d.ts +49 -0
  45. package/dist/next/session.d.ts +53 -0
  46. package/dist/next.js +230 -26
  47. package/dist/protocol/constants.d.ts +6 -0
  48. package/dist/protocol/messages.d.ts +155 -0
  49. package/dist/protocol/validation.d.ts +8 -0
  50. package/dist/push/index.d.ts +14 -0
  51. package/dist/push/operations.d.ts +22 -0
  52. package/dist/push/web-push.d.ts +39 -0
  53. package/dist/push.js +230 -26
  54. package/dist/react/FoirProvider.d.ts +116 -0
  55. package/dist/react/OAuthButtons.d.ts +20 -0
  56. package/dist/react/SignIn.d.ts +22 -0
  57. package/dist/react/UserButton.d.ts +13 -0
  58. package/dist/react/index.d.ts +19 -0
  59. package/dist/react/localStorageAdapter.d.ts +10 -0
  60. package/dist/react.js +230 -26
  61. package/dist/{realtime.d.ts → realtime/index.d.ts} +35 -16
  62. package/dist/server/callback-client.d.ts +92 -0
  63. package/dist/server/index.d.ts +8 -0
  64. package/dist/server/operation-types.d.ts +134 -0
  65. package/dist/server/require-scoped-token.d.ts +76 -0
  66. package/dist/server/server-client.d.ts +34 -0
  67. package/dist/server/types.d.ts +54 -0
  68. package/dist/server/verify-scoped-token.d.ts +100 -0
  69. package/dist/server/webhook-helpers.d.ts +36 -0
  70. package/dist/transport/foir-http.d.ts +217 -0
  71. package/dist/transport/retry.d.ts +61 -0
  72. package/dist/types/index.d.ts +124 -0
  73. package/package.json +23 -13
  74. package/dist/auth.d.ts +0 -382
  75. package/dist/client.d.ts +0 -135
  76. package/dist/content.d.ts +0 -96
  77. package/dist/media.d.ts +0 -234
  78. package/dist/next.d.ts +0 -178
  79. package/dist/push.d.ts +0 -109
  80. package/dist/react.d.ts +0 -319
  81. package/dist/server.d.ts +0 -534
  82. package/dist/shared-DkWlEd2Z.d.ts +0 -18
@@ -0,0 +1,243 @@
1
+ /**
2
+ * THE `FoirSchema` CONTRACT.
3
+ *
4
+ * `createFoirClient<FoirSchema>()` is parameterised by a type that `foir types`
5
+ * (TTS-875) emits from `GET /v1/manifest`. This file is that contract: the
6
+ * shape the generator writes and the shape the client reads. It is the only
7
+ * coupling between the two, so it is written down here rather than living in
8
+ * the generator's head.
9
+ *
10
+ * ---------------------------------------------------------------------------
11
+ * What the generator emits
12
+ * ---------------------------------------------------------------------------
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.
17
+ *
18
+ * ```ts
19
+ * // foir.generated.ts — DO NOT EDIT. Generated by `foir types`.
20
+ * import type { FoirSchemaDefinition } from '@foir/sdk/data';
21
+ *
22
+ * /** `schemaVersion` from the manifest these types were generated from. *\/
23
+ * export const foirSchemaVersion = 317;
24
+ * export const foirSchemaChannel = 'published';
25
+ *
26
+ * export interface TillyBlock {
27
+ * _id: string;
28
+ * _createdAt: string;
29
+ * text: string | null;
30
+ * // Composite/polymorphic fields are emitted as a STATIC discriminated
31
+ * // union on `__typename`. The client never infers these; it only has to
32
+ * // avoid destroying them. See `selection.ts`.
33
+ * body: Array<HeroBlock | TextBlock> | null;
34
+ * }
35
+ *
36
+ * export interface TillyBlockFindUniqueArgs {
37
+ * id?: string;
38
+ * naturalKey?: string;
39
+ * locale?: string;
40
+ * preview?: boolean;
41
+ * // NOT a list, despite the plural: `ResolveContextInput`, an object of four
42
+ * // optional fields. This sketch said `ReadonlyArray<string>` until 2026-07-28,
43
+ * // when the docs lane caught it by RUNNING the call. A wrong argument type
44
+ * // compiles at every call site and fails at the server, which is the exact
45
+ * // lie generated types exist to remove — so `foir types` imports the shape
46
+ * // from `builtins.ts` rather than restating it.
47
+ * contexts?: FoirResolveContext;
48
+ * }
49
+ *
50
+ * export interface FoirSchema extends FoirSchemaDefinition {
51
+ * schemaVersion: typeof foirSchemaVersion;
52
+ * channel: 'published';
53
+ * models: {
54
+ * tilly_block: {
55
+ * typeName: 'TillyBlock';
56
+ * fields: TillyBlock;
57
+ * relations: {
58
+ * parentBlockId: { target: 'tilly_block'; list: false; nullable: true };
59
+ * };
60
+ * ops: {
61
+ * findUnique: {
62
+ * args: TillyBlockFindUniqueArgs;
63
+ * returns: { kind: 'record'; nullable: true };
64
+ * };
65
+ * findMany: {
66
+ * args: TillyBlockFindManyArgs;
67
+ * returns: { kind: 'connection' };
68
+ * };
69
+ * };
70
+ * };
71
+ * };
72
+ * namespaces: {
73
+ * search: {
74
+ * ops: {
75
+ * searchRecords: {
76
+ * args: SearchRecordsArgs;
77
+ * result: SearchRecordsResult;
78
+ * };
79
+ * };
80
+ * };
81
+ * };
82
+ * }
83
+ * ```
84
+ *
85
+ * ---------------------------------------------------------------------------
86
+ * Rules the generator must hold to
87
+ * ---------------------------------------------------------------------------
88
+ *
89
+ * 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.
93
+ *
94
+ * 2. **Op names are the manifest's op names verbatim** (`findUnique`,
95
+ * `findMany`, `search`, …) — the canonical names, NOT the GraphQL root
96
+ * field (`tillyBlock`, `tillyBlocks`). Same reason: they go on the wire.
97
+ * For namespaces the op name IS the root field name, as the manifest lists
98
+ * it, because that is what the server matches on.
99
+ *
100
+ * 3. **Emit only what the manifest returned.** The manifest is already the
101
+ * per-caller projection of `SchemaForCaller(scopes)`, so "only the ops this
102
+ * credential can perform" needs no extra filtering — it is what arrives. A
103
+ * `pk_` key's generated schema simply has no `create`, and
104
+ * `foir.tilly_block.create(...)` is then a compile error, which is the
105
+ * entire point of per-key shaping.
106
+ *
107
+ * 4. **`fields` is the COMPLETE scalar shape**, every scalar the manifest
108
+ * lists, none optional. This is what makes the model type unconditionally
109
+ * true: the server always returns every scalar (the selection model is
110
+ * complete-by-default), so no field can be typed present and arrive
111
+ * `undefined`. Nullable scalars are `T | null`, never `T | undefined` and
112
+ * never `?:`.
113
+ *
114
+ * 5. **Relations are NOT in `fields`.** A relation key only exists on the
115
+ * result when the caller asked for it via `include`. Putting it in `fields`
116
+ * would type it as always-present, which is the exact footgun rule 4 is
117
+ * there to remove.
118
+ *
119
+ * 6. **`returns.kind` classifies the wrapper, not the model.**
120
+ * - `record` — the model itself; `nullable` when the field can resolve null
121
+ * (`findUnique`, `findMine`).
122
+ * - `connection` — a Relay connection over the model (`findMany`).
123
+ * - `list` — a plain array of the model.
124
+ * - `opaque` — the op does not return the model at all (`myAccess`). Its
125
+ * `type` is used verbatim and `include`/`omit` are not offered.
126
+ *
127
+ * 7. **Unions are emitted statically.** A polymorphic field's type is written
128
+ * out as a discriminated union on `__typename` by the generator. There is
129
+ * no inference step, so there is nothing for the client to get wrong — see
130
+ * `selection.ts` for why the selection machinery cannot flatten it.
131
+ *
132
+ * 8. **`raw` and `ns` are reserved top-level names.** They are the escape hatch
133
+ * and the namespace accessor on the client object. A model whose key is
134
+ * `raw` or `ns` would collide; the server should reject those model keys at
135
+ * create time (noted in TTS-868's "Known limits").
136
+ *
137
+ * 9. **`models` and `namespaces` must be object type LITERALS, not named
138
+ * `interface`s.** TypeScript gives an implicit index signature to object
139
+ * type literals and to `type` aliases of them, but never to an interface. So
140
+ * `models: { tilly_block: {…} }` written inline satisfies
141
+ * `Readonly<Record<string, FoirModelDefinition>>`, and
142
+ * `interface Models { tilly_block: {…} }` + `models: Models` does not — it
143
+ * fails with "Index signature for type 'string' is missing". Emitting inline
144
+ * (as the sketch above does) is the safe shape; if the generator ever
145
+ * factors these out for readability, they have to be `type X = {…}`.
146
+ * `__tests__/fixture-schema.ts` is the compiling reference.
147
+ */
148
+ /**
149
+ * A model's complete scalar shape. Deliberately loose — the generator writes a
150
+ * real interface here and the client only ever `Omit`s from it or reads its
151
+ * keys, so constraining it further buys nothing and makes generated interfaces
152
+ * with index signatures or unusual key types fail to assign.
153
+ */
154
+ export type FoirFieldsShape = object;
155
+ /** One relation the caller may `include`. */
156
+ export interface FoirRelationDefinition {
157
+ /** Key in `FoirSchemaDefinition['models']` this relation resolves to. */
158
+ readonly target: string;
159
+ /** True when the relation resolves to a list of the target model. */
160
+ readonly list: boolean;
161
+ /** True when the relation itself can resolve null. */
162
+ readonly nullable: boolean;
163
+ }
164
+ /** How an op's result wraps (or does not wrap) the model. */
165
+ export type FoirReturnDefinition = {
166
+ readonly kind: 'record';
167
+ readonly nullable: boolean;
168
+ } | {
169
+ readonly kind: 'connection';
170
+ } | {
171
+ readonly kind: 'list';
172
+ readonly nullable: boolean;
173
+ } | {
174
+ readonly kind: 'opaque';
175
+ readonly type: unknown;
176
+ };
177
+ /** One operation available on a model for this credential. */
178
+ export interface FoirOpDefinition {
179
+ /**
180
+ * The op's arguments, as an object type. These are spread at the CALL SITE
181
+ * alongside `include`/`omit` (`{ where, first, include }`) and split back out
182
+ * into the wire's `args` object by the client — `include`/`omit` are
183
+ * selection, not arguments, and the server takes them as siblings of `args`.
184
+ */
185
+ readonly args: object;
186
+ readonly returns: FoirReturnDefinition;
187
+ }
188
+ /** A model as the per-caller manifest describes it. */
189
+ export interface FoirModelDefinition {
190
+ /** GraphQL type name, e.g. `'TillyBlock'`. Documentation + tooling. */
191
+ readonly typeName: string;
192
+ /** Complete scalar shape. See contract rule 4. */
193
+ readonly fields: FoirFieldsShape;
194
+ readonly relations: Readonly<Record<string, FoirRelationDefinition>>;
195
+ readonly ops: Readonly<Record<string, FoirOpDefinition>>;
196
+ }
197
+ /**
198
+ * One op on a non-model namespace (`search`, `customers`, `notifications`).
199
+ * Namespaces have no model behind them, so there is no `include`/`omit` and
200
+ * the result type is used verbatim.
201
+ */
202
+ export interface FoirNamespaceOpDefinition {
203
+ readonly args: object;
204
+ readonly result: unknown;
205
+ }
206
+ export interface FoirNamespaceDefinition {
207
+ readonly ops: Readonly<Record<string, FoirNamespaceOpDefinition>>;
208
+ }
209
+ /**
210
+ * The type `createFoirClient` is parameterised by. `foir types` emits an
211
+ * interface extending this; everything above documents what goes in it.
212
+ */
213
+ export interface FoirSchemaDefinition {
214
+ /**
215
+ * `schemaVersion` from the manifest these types were generated from. Kept as
216
+ * a literal so a human reading the generated file sees it, and mirrored by a
217
+ * runtime `foirSchemaVersion` const for drift detection — the type cannot be
218
+ * read at runtime.
219
+ */
220
+ readonly schemaVersion: number;
221
+ readonly channel: 'published' | 'draft';
222
+ readonly models: Readonly<Record<string, FoirModelDefinition>>;
223
+ readonly namespaces: Readonly<Record<string, FoirNamespaceDefinition>>;
224
+ }
225
+ /**
226
+ * A Relay connection, as `findMany` returns it. Not flattened to an array on
227
+ * purpose: the compiled document is still the wire format, and hiding
228
+ * `pageInfo` behind a convenience array would make pagination unreachable
229
+ * without dropping to `raw`.
230
+ */
231
+ export interface FoirConnection<TNode> {
232
+ edges: Array<{
233
+ cursor: string;
234
+ node: TNode;
235
+ }>;
236
+ pageInfo: {
237
+ hasNextPage: boolean;
238
+ hasPreviousPage: boolean;
239
+ startCursor: string | null;
240
+ endCursor: string | null;
241
+ };
242
+ totalCount: number;
243
+ }
@@ -0,0 +1,330 @@
1
+ /**
2
+ * The type-level selection machinery — deliberately shallow.
3
+ *
4
+ * Two operations and nothing else:
5
+ *
6
+ * 1. `Omit<Fields, K>` for `omit`.
7
+ * 2. One conditional per `include`d relation key.
8
+ *
9
+ * No template-literal parsing (Supabase's approach, and the source of its
10
+ * `tsc` perf complaints), no deep recursive mapped types over field values, no
11
+ * build-time query parser. The complete-by-default selection model is what
12
+ * makes that enough: scalars are ALWAYS returned, so the model type is
13
+ * unconditionally true and there is nothing to narrow. If this file ever
14
+ * misbehaves the failure mode is a missing relation key, not a mis-parsed
15
+ * string.
16
+ *
17
+ * ---------------------------------------------------------------------------
18
+ * Why unions survive
19
+ * ---------------------------------------------------------------------------
20
+ *
21
+ * Sanity's typegen — a mature, shipped implementation of the same
22
+ * architecture — is documented to lose discriminated-union information on
23
+ * rest-operator array projections. That is the fragile part of any generated
24
+ * type system, so it is worth being explicit about why it cannot happen here:
25
+ *
26
+ * every transformation below operates on the OUTER object's keys only. `Omit`,
27
+ * `Simplify` and the include-shape mapped type all iterate `keyof Fields` and
28
+ * copy `Fields[K]` through untouched. A polymorphic field's value type — a
29
+ * union of `__typename`-discriminated members, written out statically by the
30
+ * generator — is a property VALUE. It is never mapped over, never distributed,
31
+ * never re-inferred, so it arrives at the call site byte-identical to what the
32
+ * generator emitted and narrows under `switch` exactly as it would have.
33
+ *
34
+ * `__tests__/selection.test-d.ts` pins this, including the rest-operator and
35
+ * `.map()` shapes that break Sanity.
36
+ *
37
+ * Two notes for whoever edits this file next, both found by mutating it and
38
+ * watching what the tests did:
39
+ *
40
+ * - A homomorphic mapped type over a NAKED type parameter — the classic
41
+ * `type Simplify<T> = { [K in keyof T]: T[K] }` applied to an array's
42
+ * element type — is SAFE. It distributes over unions, so the discriminated
43
+ * union comes out the other side intact. Mutating the pipeline to do this
44
+ * left all six union tests green.
45
+ * - `Omit<U, …>` or `Pick<U, …>` on that element type is NOT safe. Both
46
+ * compile to `{ [P in K]: … }`, which is not homomorphic and does not
47
+ * distribute, and `keyof (A | B | C)` is the INTERSECTION of their keys —
48
+ * so the union collapses to `{ __typename: 'A' | 'B' | 'C' }` and every
49
+ * branch's own fields vanish. That is Sanity's failure mode, and mutating
50
+ * the pipeline to do it reds all six union tests plus the rest-operator
51
+ * one by name.
52
+ *
53
+ * So: never reach inside a field's value type when that type may be a UNION.
54
+ *
55
+ * Nested selection (`pick: { seo: { jsonLd: true } }`) does reach one level in,
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.
61
+ */
62
+ import type { FoirConnection, FoirModelDefinition, FoirNamespaceOpDefinition, FoirOpDefinition, FoirRelationDefinition, FoirSchemaDefinition } from './schema';
63
+ /**
64
+ * How deep `include` may nest. Finite by necessity, not by taste: relations are
65
+ * routinely self-referential (`tilly_block.parentBlockId -> tilly_block`), and
66
+ * an uncapped recursive mapped type over that is an immediate
67
+ * "Type instantiation is excessively deep and possibly infinite".
68
+ */
69
+ type IncludeDepth = 3;
70
+ /** `Decrement[3] === 2`. */
71
+ type Decrement = [never, 0, 1, 2, 3];
72
+ /**
73
+ * The model a relation resolves to. Falls back to a relation-free, opaque model
74
+ * when the generated schema names a target that is not in `models` — a
75
+ * malformed generated file should surface as a useless-but-quiet type, not as a
76
+ * mapped type over `keyof never`.
77
+ */
78
+ type TargetModel<S extends FoirSchemaDefinition, R extends FoirRelationDefinition> = R['target'] extends keyof S['models'] ? S['models'][R['target']] : OpaqueModel;
79
+ type OpaqueModel = {
80
+ typeName: string;
81
+ fields: Record<string, unknown>;
82
+ relations: Record<never, FoirRelationDefinition>;
83
+ ops: Record<never, FoirOpDefinition>;
84
+ };
85
+ /**
86
+ * The keys a caller may `omit`.
87
+ *
88
+ * `_id` is excluded because the server refuses it — verified against deployed
89
+ * dev: `omit "_id": "_id" is required for correctness and cannot be removed`
90
+ * (HTTP 400). Excluding it here turns a runtime 400 into a compile error.
91
+ *
92
+ * Relations are absent by construction: they are not in `fields`, and omitting
93
+ * something you never asked for is meaningless.
94
+ */
95
+ export type FoirOmittableKey<M extends FoirModelDefinition> = Exclude<Extract<keyof M['fields'], string>, '_id'>;
96
+ /**
97
+ * The `omit` entries actually present in a call.
98
+ *
99
+ * `never` when `omit` was not passed. That matters: an OPTIONAL `omit?:` on the
100
+ * inferred query type does not satisfy the required-property check below, so
101
+ * the no-argument case (where `Q` falls back to its own constraint) correctly
102
+ * omits nothing rather than omitting every omittable key.
103
+ */
104
+ type OmittedKeys<Q> = Q extends {
105
+ omit: readonly (infer K)[];
106
+ } ? Extract<K, string> : never;
107
+ type ApplyOmit<M extends FoirModelDefinition, Q> = [OmittedKeys<Q>] extends [never] ? M['fields'] : Omit<M['fields'], OmittedKeys<Q>>;
108
+ /**
109
+ * Every scalar key, including `_id`.
110
+ *
111
+ * Unlike `FoirOmittableKey` this does NOT exclude `_id`: naming it is harmless
112
+ * because it survives either way, and excluding it would make the obvious
113
+ * `pick: ['_id', 'title']` a type error for no reason.
114
+ */
115
+ export type FoirPickableKey<M extends FoirModelDefinition> = Extract<keyof M['fields'], string>;
116
+ /** The `pick` entries actually present in a call. `never` when absent — see OmittedKeys. */
117
+ type PickedKeys<Q> = Q extends {
118
+ pick: readonly (infer K)[];
119
+ } ? Extract<K, string> : never;
120
+ /**
121
+ * `_id` is added back unconditionally, because the server emits it whatever the
122
+ * caller asked for (`protectedSelectionFields`). Typing the result without it
123
+ * would describe a response the server does not send, and a caller would lose
124
+ * the ability to address the record they just read.
125
+ *
126
+ * This is the one place `pick` is not literally what was asked for, which is
127
+ * why it is stated in the docs rather than left to be discovered.
128
+ */
129
+ type ApplyPick<M extends FoirModelDefinition, Q> = Pick<M['fields'], Extract<PickedKeys<Q> | '_id', keyof M['fields']>>;
130
+ /**
131
+ * One narrowing step for both verbs.
132
+ *
133
+ * They are mutually exclusive on the wire and in `FoirQuery`, so this only has
134
+ * to decide which is present — `pick` first, and `omit` is then reached exactly
135
+ * when `pick` is absent.
136
+ */
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>;
138
+ /**
139
+ * True when `T` is a union.
140
+ *
141
+ * Load-bearing for the guard below rather than a curiosity: a union distributed
142
+ * through `(U extends any ? (k: U) => void : never)` intersects to something
143
+ * that is not assignable back to the original, which a single type always is.
144
+ */
145
+ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
146
+ type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
147
+ /**
148
+ * Whether a field's value may be descended into.
149
+ *
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.
158
+ *
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.
161
+ */
162
+ type Descendable<V> = IsUnion<NonNullable<V>> extends true ? false : NonNullable<V> extends readonly unknown[] ? false : NonNullable<V> extends object ? true : false;
163
+ /**
164
+ * The nested selector: `true` for a leaf, or the verb again to descend.
165
+ *
166
+ * The verb repeats at each level — `pick: { seo: { pick: { jsonLd: true } } }` —
167
+ * which is Prisma's shape and, more to the point here, the shape `include`
168
+ * already uses (`include: { author: { include: { tags: true } } }`). One idiom
169
+ * in the call rather than two.
170
+ *
171
+ * Repeating the keyword also leaves a slot: a future `include: { author: { pick:
172
+ * {…} } }` has somewhere to live. A bare child map — the first cut of this —
173
+ * had nowhere to put it and, worse, could not express more than one level, so
174
+ * `image.variants.thumbnail` was reachable on the wire and untypeable here.
175
+ *
176
+ * `D` bounds the recursion for the same reason `IncludeDepth` does: composites
177
+ * can reference each other, and an uncapped recursive mapped type over that is
178
+ * an immediate "Type instantiation is excessively deep and possibly infinite".
179
+ */
180
+ 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]>;
183
+ } : true;
184
+ };
185
+ /**
186
+ * How deep a selector may nest. Three, matching `IncludeDepth`, and comfortably
187
+ * past what real composites do — `ImageValue.variants.thumbnail` is the deepest
188
+ * shape on any live model at two.
189
+ */
190
+ type SelectorDepth = 3;
191
+ /** The selector actually present under `verb`, or `never` when absent or an array. */
192
+ type SelectorOf<Q, Verb extends 'pick' | 'omit'> = Q extends Record<Verb, infer S> ? S extends readonly unknown[] ? never : S : never;
193
+ /**
194
+ * Apply a nested `pick` selector. `_id` survives regardless, for the same
195
+ * reason it does in the flat form — the server always emits it.
196
+ */
197
+ 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];
201
+ };
202
+ /**
203
+ * The same walk one level in. Separate from the root because `_id` is forced at
204
+ * the ROOT only — a composite has no `_id`, and adding one would describe a
205
+ * response shape the server never sends.
206
+ */
207
+ 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];
211
+ };
212
+ /**
213
+ * Apply a nested `omit` selector.
214
+ *
215
+ * `true` at a key drops the whole field. An object at a key drops only the
216
+ * named children, which is what makes `omit: { description: { markdown: true } }`
217
+ * able to shed one richtext encoding of three without losing the field.
218
+ */
219
+ 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];
223
+ };
224
+ /**
225
+ * The `include` argument for a model: every relation key, opt-in, optionally
226
+ * carrying a nested `include` for the relation's own relations.
227
+ *
228
+ * `true` only (not `boolean`) — a `false` would go on the wire and the server
229
+ * validates `include` keys strictly, so a conditional include has to be built
230
+ * by conditionally spreading the key, not by passing `false`.
231
+ */
232
+ export type FoirInclude<S extends FoirSchemaDefinition, M extends FoirModelDefinition, D extends number = IncludeDepth> = {
233
+ [K in keyof M['relations']]?: D extends 0 ? true : true | {
234
+ include?: FoirInclude<S, TargetModel<S, M['relations'][K]>, Decrement[D]>;
235
+ };
236
+ };
237
+ /** 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]>>;
240
+ };
241
+ 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
+ /**
243
+ * A model as this call will receive it: the complete scalar shape, minus
244
+ * `omit`, plus the `include`d relations.
245
+ *
246
+ * When neither was passed this resolves to `M['fields']` BY IDENTITY — not a
247
+ * structurally-equal copy — so `foir.tilly_block.findUnique({ id })` hovers as
248
+ * `TillyBlock` rather than as an anonymous 40-property object literal.
249
+ *
250
+ * `include` produces an INTERSECTION rather than a flattened object, on
251
+ * purpose. A `{ [K in keyof T]: T[K] }` flattening pass would read a little
252
+ * tidier in a hover, but `TillyBlock & { noteId: TillyNote | null }` already
253
+ * reads well and names the generated interface — and, more to the point, it
254
+ * means the only mapped type anywhere near a result is `Omit`. One less thing
255
+ * between the generator's union and the call site.
256
+ */
257
+ export type FoirRecordResult<S extends FoirSchemaDefinition, M extends FoirModelDefinition, Q, D extends number = IncludeDepth> = Q extends {
258
+ include: infer I;
259
+ } ? ApplySelection<M, Q> & IncludedRelations<S, M, I, D> : ApplySelection<M, Q>;
260
+ /** Apply an op's return wrapper to the shaped record. */
261
+ export type FoirOpResult<S extends FoirSchemaDefinition, M extends FoirModelDefinition, O extends FoirOpDefinition, Q> = O['returns'] extends {
262
+ kind: 'opaque';
263
+ type: infer T;
264
+ } ? T : O['returns'] extends {
265
+ kind: 'connection';
266
+ } ? FoirConnection<FoirRecordResult<S, M, Q>> : O['returns'] extends {
267
+ kind: 'list';
268
+ nullable: infer N;
269
+ } ? N extends true ? Array<FoirRecordResult<S, M, Q>> | null : Array<FoirRecordResult<S, M, Q>> : O['returns'] extends {
270
+ kind: 'record';
271
+ nullable: infer N;
272
+ } ? N extends true ? FoirRecordResult<S, M, Q> | null : FoirRecordResult<S, M, Q> : never;
273
+ /**
274
+ * What a model op accepts: the op's own args with `include`/`omit` as siblings.
275
+ * They are split back apart onto the wire (`{ model, op, args, include, omit }`)
276
+ * — `include`/`omit` are selection, not arguments — but at the call site a
277
+ * single flat object is what reads well.
278
+ *
279
+ * An `opaque` op returns something that is not the model, so selection does not
280
+ * apply and is not offered.
281
+ */
282
+ export type FoirQuery<S extends FoirSchemaDefinition, M extends FoirModelDefinition, O extends FoirOpDefinition> = O['returns'] extends {
283
+ kind: 'opaque';
284
+ } ? O['args'] : O['args'] & ({
285
+ include?: FoirInclude<S, M>;
286
+ omit?: readonly FoirOmittableKey<M>[] | FoirSelector<M['fields'], 'omit'>;
287
+ /**
288
+ * Mutually exclusive with `omit`, mirroring the server's 400. Typed as
289
+ * `never?` on this branch rather than left out, because omitting the key
290
+ * would let an object carrying BOTH satisfy this branch and reach the
291
+ * wire — the excess-property check does not fire on a value inferred
292
+ * into a generic parameter, which is the same hole `NoExtraKeys` exists
293
+ * to close.
294
+ */
295
+ pick?: never;
296
+ } | {
297
+ include?: FoirInclude<S, M>;
298
+ pick?: readonly FoirPickableKey<M>[] | FoirSelector<M['fields'], 'pick'>;
299
+ omit?: never;
300
+ });
301
+ /**
302
+ * Reject keys the op does not declare.
303
+ *
304
+ * TypeScript's excess-property check does NOT fire when an object literal is
305
+ * inferred into a generic type parameter — `Q` is inferred as the literal's own
306
+ * shape and then merely has to be *assignable* to its constraint, which an
307
+ * object with extra keys is. So `findUnique({ id, first: 10 })` compiled
308
+ * silently and sent `first` to a server that does not take it. Typing every
309
+ * undeclared key as `never` puts the check back: `first: 10` is not assignable
310
+ * to `never`, and the error lands on the offending key.
311
+ *
312
+ * Pinned by "rejects an arg the op does not declare" in
313
+ * `__tests__/selection.test-d.ts`.
314
+ */
315
+ type NoExtraKeys<Q, T> = Record<Exclude<keyof Q, keyof T>, never>;
316
+ /**
317
+ * One model operation.
318
+ *
319
+ * `const Q` is what makes `omit: ['bodyRichtext']` infer as the tuple
320
+ * `readonly ['bodyRichtext']` instead of `string[]`; without it `Omit` would
321
+ * have nothing to remove. It also pins `include: { author: true }` to the
322
+ * literal `true`.
323
+ */
324
+ export type FoirModelOperation<S extends FoirSchemaDefinition, M extends FoirModelDefinition, O extends FoirOpDefinition> = <const Q extends FoirQuery<S, M, O>>(query?: Q & NoExtraKeys<Q, FoirQuery<S, M, O>>) => Promise<FoirOpResult<S, M, O, Q>>;
325
+ /**
326
+ * One namespace operation. No model behind it, so no `include`/`omit` and the
327
+ * generated result type is used verbatim.
328
+ */
329
+ export type FoirNamespaceOperation<O extends FoirNamespaceOpDefinition> = (args?: O['args']) => Promise<O['result']>;
330
+ export {};
@@ -0,0 +1,107 @@
1
+ /**
2
+ * `POST /v1/query` transport.
3
+ *
4
+ * Everything Foir-specific about the request — which key, the preview schema
5
+ * channel, the customer bearer, the error shape — comes from
6
+ * `../transport/foir-http`, the same module `@foir/sdk/client`'s `foirRequest`
7
+ * uses. This file adds only what is specific to the JSON op endpoint: the URL,
8
+ * the body, and reading the single root field back out.
9
+ *
10
+ * Edge-safe: `fetch` only, no Node built-ins, and no module-level state of any
11
+ * kind. A shared cache on a stateless multi-tenant edge worker is a
12
+ * cross-tenant hazard (see `../graphql/foir-request.ts:1-18`), and that applies
13
+ * with more force here — this client is per-credential, so anything cached
14
+ * across instances is cached across tenants.
15
+ */
16
+ import { type FoirTokenProvider } from '../transport/foir-http';
17
+ /** The wire body `POST /v1/query` accepts. Exactly one of model/namespace. */
18
+ export type FoirRawOp = {
19
+ op: string;
20
+ args?: Record<string, unknown>;
21
+ include?: Record<string, unknown>;
22
+ /** Remove named paths. Mutually exclusive with `pick` — the server 400s on both. */
23
+ omit?: readonly string[];
24
+ /** Keep only named paths. Mutually exclusive with `omit`. `_id` is always returned. */
25
+ pick?: readonly string[];
26
+ } & ({
27
+ model: string;
28
+ namespace?: never;
29
+ } | {
30
+ namespace: string;
31
+ model?: never;
32
+ });
33
+ export interface FoirClientConfig {
34
+ /**
35
+ * Foir public API origin, e.g. `https://api.foir.dev`.
36
+ *
37
+ * A full GraphQL endpoint is accepted too — `PLATFORM_API_URL` is
38
+ * conventionally `https://api.foir.dev/graphql` and people will paste it — so
39
+ * a trailing `/graphql` is stripped before `/v1/query` is appended.
40
+ */
41
+ apiUrl: string;
42
+ /** `pk_…` or `sk_…`. The manifest, and therefore the generated types, are shaped by it. */
43
+ apiKey: string;
44
+ /**
45
+ * Read the draft schema channel (`X-Foir-Schema-Channel: draft`). Needs a
46
+ * `sk_` key with `drafts:read`; the header is ignored under public keys.
47
+ * Defaults to `'published'`.
48
+ */
49
+ channel?: 'published' | 'draft';
50
+ /**
51
+ * Customer bearer for self-scoped reads (`myProfile`, `myTillyNotes`). A
52
+ * function is resolved before every request so a cookie session can be read
53
+ * per-request; a bare string is used as-is.
54
+ */
55
+ bearerToken?: string | FoirTokenProvider;
56
+ /** Extra headers, applied last so they can override the ones above. */
57
+ headers?: Readonly<Record<string, string>>;
58
+ /**
59
+ * `fetch` implementation. Defaults to `globalThis.fetch`. Injectable for
60
+ * tests and for runtimes that hand you a bound `fetch` (Hydrogen/Oxygen).
61
+ */
62
+ fetch?: typeof globalThis.fetch;
63
+ /**
64
+ * Merged into every `fetch` call — `{ cache: 'no-store' }`,
65
+ * `{ next: { revalidate: 60 } }`, an `AbortSignal`, and so on. Applied after
66
+ * method/headers/body, so those cannot be clobbered by accident.
67
+ */
68
+ requestInit?: Omit<RequestInit, 'method' | 'headers' | 'body'>;
69
+ /**
70
+ * `foirSchemaVersion` from the generated module. When set, the client
71
+ * compares it against the server's manifest once and warns on mismatch.
72
+ * Omitted — e.g. types generated as a bare `.d.ts` — disables the check.
73
+ */
74
+ schemaVersion?: number;
75
+ /** Set `false` to suppress the drift check even when `schemaVersion` is set. */
76
+ checkSchemaDrift?: boolean;
77
+ /** Called instead of `console.warn` when drift is detected. */
78
+ onSchemaDrift?: (drift: FoirSchemaDrift) => void;
79
+ }
80
+ export interface FoirSchemaDrift {
81
+ /** Manifest version the local types were generated from. */
82
+ generated: number;
83
+ /** Manifest version the server is serving now. */
84
+ server: number;
85
+ channel: string;
86
+ }
87
+ /**
88
+ * Build a `/v1/...` URL from whatever the caller put in `apiUrl`.
89
+ *
90
+ * Tolerates the three things people actually pass: an origin, an origin with a
91
+ * trailing slash, and the conventional `PLATFORM_API_URL` value
92
+ * (`https://api.foir.dev/graphql`).
93
+ */
94
+ export declare function foirDataEndpoint(apiUrl: string, path: `/${string}`): string;
95
+ /** Resolve `config.bearerToken`, whether it is a string, a function, or absent. */
96
+ export declare function resolveBearerToken(config: Pick<FoirClientConfig, 'bearerToken'>): Promise<string | undefined>;
97
+ /**
98
+ * Execute one op and return the single root field's value.
99
+ *
100
+ * The response is a normal GraphQL envelope keyed by the compiled document's
101
+ * root field (`{"data":{"tillyBlocks":{…}}}`), and the client has no manifest at
102
+ * runtime so it does not know that name. It does not need to: one op compiles
103
+ * to exactly one root field, so "the only key of `data`" is unambiguous. A
104
+ * response with a different number of keys means the server changed shape, and
105
+ * guessing would be worse than saying so.
106
+ */
107
+ export declare function postFoirOp<T>(config: FoirClientConfig, body: FoirRawOp): Promise<T>;