@foir/sdk 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +857 -4
  2. package/README.md +46 -15
  3. package/dist/auth/auth.d.ts +104 -1
  4. package/dist/auth/hosted.d.ts +22 -0
  5. package/dist/auth/index.d.ts +13 -7
  6. package/dist/auth/operations.d.ts +85 -64
  7. package/dist/auth/ops.d.ts +99 -0
  8. package/dist/auth.js +786 -106
  9. package/dist/chunk-IGNLWT3H.js +35 -0
  10. package/dist/chunk-OHYVKTGB.js +45 -0
  11. package/dist/chunk-TAL5H6VA.js +39 -0
  12. package/dist/client.js +25 -31
  13. package/dist/codegen/block-registry.d.ts +13 -19
  14. package/dist/codegen/block-registry.js +3 -31
  15. package/dist/codegen/block-scaffold.d.ts +12 -21
  16. package/dist/codegen/block-scaffold.js +3 -41
  17. package/dist/codegen/index.d.ts +9 -4
  18. package/dist/codegen/index.js +7 -11
  19. package/dist/codegen/item-registry.d.ts +28 -0
  20. package/dist/codegen/item-registry.js +7 -0
  21. package/dist/codegen/item-scaffold.d.ts +29 -0
  22. package/dist/codegen/item-scaffold.js +7 -0
  23. package/dist/codegen/preset.d.ts +14 -7
  24. package/dist/codegen/shared.d.ts +20 -9
  25. package/dist/content/content-renderer.d.ts +90 -24
  26. package/dist/content/index.d.ts +13 -7
  27. package/dist/content/richtext.d.ts +66 -18
  28. package/dist/content.js +227 -80
  29. package/dist/data/builtins.d.ts +369 -92
  30. package/dist/data/client.d.ts +97 -25
  31. package/dist/data/document.d.ts +251 -0
  32. package/dist/data/index.d.ts +38 -19
  33. package/dist/data/manifest.d.ts +290 -8
  34. package/dist/data/schema.d.ts +78 -8
  35. package/dist/data/selection.d.ts +231 -37
  36. package/dist/data/subjects.d.ts +221 -0
  37. package/dist/data/subscription-document.d.ts +75 -0
  38. package/dist/data/subscriptions.d.ts +181 -0
  39. package/dist/data/transport.d.ts +255 -37
  40. package/dist/data/ws-transport.d.ts +197 -0
  41. package/dist/data.js +1419 -45
  42. package/dist/graphql/cdn-route.d.ts +12 -28
  43. package/dist/media/index.d.ts +6 -2
  44. package/dist/media/media.d.ts +2 -2
  45. package/dist/media/screen-capture.d.ts +81 -0
  46. package/dist/media.js +97 -0
  47. package/dist/next.js +663 -182
  48. package/dist/push.js +700 -90
  49. package/dist/react/FoirFeedback.d.ts +62 -0
  50. package/dist/react/feedback-notes.d.ts +101 -0
  51. package/dist/react/index.d.ts +7 -1
  52. package/dist/react.js +1272 -194
  53. package/dist/realtime.js +1 -1
  54. package/dist/server/events-client.d.ts +279 -0
  55. package/dist/server/index.d.ts +4 -0
  56. package/dist/server.js +298 -0
  57. package/dist/transport/foir-http.d.ts +23 -6
  58. package/dist/transport/persisted-query.d.ts +68 -0
  59. package/dist/transport/retry.d.ts +1 -1
  60. package/package.json +13 -1
  61. package/dist/chunk-ADO5FJNM.js +0 -30
@@ -3,12 +3,12 @@
3
3
  *
4
4
  * ```ts
5
5
  * import { createFoirClient } from '@foir/sdk/data';
6
- * import { type FoirSchema, foirSchemaVersion } from './foir.generated';
6
+ * import * as generated from './foir.generated';
7
7
  *
8
- * const foir = createFoirClient<FoirSchema>({
8
+ * const foir = createFoirClient({
9
9
  * apiUrl: process.env.PLATFORM_API_URL!,
10
10
  * apiKey: process.env.PLATFORM_API_KEY!,
11
- * schemaVersion: foirSchemaVersion,
11
+ * generated,
12
12
  * });
13
13
  *
14
14
  * const block = await foir.tilly_block.findUnique({
@@ -18,6 +18,19 @@
18
18
  * });
19
19
  * ```
20
20
  *
21
+ * One import and one call. `FoirSchema` is inferred from the module rather than
22
+ * restated, and the four runtime exports the client reads are read off it rather
23
+ * than named one at a time at every call site — see `resolveGeneratedConfig`
24
+ * below for why that indirection is worth having.
25
+ *
26
+ * The individual fields (`schemaVersion`, `selections`, `namespaceSelections`,
27
+ * `selectionUnions`) still work, and still win where both are given.
28
+ *
29
+ * The call is the same call it always was. What changed underneath is what goes
30
+ * on the wire: the client loads that op's selection tree, applies
31
+ * `omit`/`pick`/`include`, prints a GraphQL document, hashes it and sends the
32
+ * hash. See `document.ts` for the printer and `transport.ts` for the routing.
33
+ *
21
34
  * ---------------------------------------------------------------------------
22
35
  * Why this is a Proxy
23
36
  * ---------------------------------------------------------------------------
@@ -29,28 +42,54 @@
29
42
  * reached. A two-level `Proxy` does that in a few lines and costs one property
30
43
  * lookup per call.
31
44
  *
32
- * The alternative — fetching `/v1/manifest` at construction — would put a
33
- * round trip in front of the first query on every cold edge invocation, to
34
- * learn something the types already know.
45
+ * ---------------------------------------------------------------------------
46
+ * Why `selections` is a config field and not a fetch
47
+ * ---------------------------------------------------------------------------
48
+ *
49
+ * The trees ARE runtime data — that is the one thing the generated module has to
50
+ * carry across the type/value boundary, alongside `foirSchemaVersion`. Passing
51
+ * them in costs nothing and is the intended path.
52
+ *
53
+ * When they are absent the client fetches `GET /v1/manifest` once, at first use,
54
+ * and reads the same trees off it. That is a FALLBACK and never the default,
55
+ * for the reason this file has always given: it puts a round trip in front of
56
+ * the first query on every cold edge invocation, to learn something the
57
+ * generated file already knows. What it buys is that `@foir/sdk/data` still
58
+ * works with zero codegen, which keeps the getting-started path honest.
59
+ *
60
+ * It has to stay reachable, and it must not be reachable by ACCIDENT. Those
61
+ * pull in opposite directions and the split is: a client built with no generated
62
+ * module and no `selections` has chosen the fallback, and says so once; a client
63
+ * built with a `generated` module that cannot serve the printer is refused where
64
+ * it was constructed. There is no input that quietly produces the round trip.
35
65
  *
36
66
  * ---------------------------------------------------------------------------
37
67
  * Why namespaces live under `.ns`
38
68
  * ---------------------------------------------------------------------------
39
69
  *
40
- * `POST /v1/query` takes `model` OR `namespace`, never both, and refuses the
41
- * request if you send both. Since there is no runtime schema (above), the
42
- * client cannot tell which one `foir.search` means. So the addressing carries
43
- * it: `foir.<model>.<op>()` sends `model`, `foir.ns.<namespace>.<op>()` sends
44
- * `namespace`. `ns` and `raw` are therefore reserved top-level names.
70
+ * Since there is no runtime schema (above), the client cannot tell whether
71
+ * `foir.search` means a model or a namespace. So the addressing carries it:
72
+ * `foir.<model>.<op>()` addresses a model, `foir.ns.<namespace>.<op>()` a
73
+ * namespace. `ns` and `raw` are therefore reserved top-level names.
45
74
  */
46
75
  import type { FoirModelDefinition, FoirNamespaceOpDefinition, FoirOpDefinition, FoirSchemaDefinition } from './schema';
47
76
  import type { FoirModelOperation, FoirNamespaceOperation } from './selection';
48
77
  import { type FoirEnv } from '../transport/foir-http';
49
- import { type FoirClientConfig, type FoirRawOp } from './transport';
50
- /** The ops available on one model for this credential. */
78
+ import { type FoirClientConfig, type FoirGeneratedModule } from './transport';
79
+ import { type FoirModelSubscriptions, type FoirNamespaceSubscriptions } from './subscriptions';
80
+ /**
81
+ * The ops available on one model for this credential, plus its subscriptions.
82
+ *
83
+ * `onChange` is on every model rather than gated on a generated flag, and that
84
+ * is correct rather than lazy: `recordChanged` charges
85
+ * `records:read:<modelKey>`, which is the SAME scope that put the model in
86
+ * `FoirSchema` at all. A model this credential cannot watch is a model it
87
+ * cannot read, so it is already absent from `S['models']` and the accessor
88
+ * cannot be reached.
89
+ */
51
90
  export type FoirModelClient<S extends FoirSchemaDefinition, M extends FoirModelDefinition> = {
52
91
  [OK in keyof M['ops']]: M['ops'][OK] extends FoirOpDefinition ? FoirModelOperation<S, M, M['ops'][OK]> : never;
53
- };
92
+ } & FoirModelSubscriptions;
54
93
  export type FoirNamespaceClient<N extends {
55
94
  ops: object;
56
95
  }> = {
@@ -59,21 +98,44 @@ export type FoirNamespaceClient<N extends {
59
98
  export interface FoirClientCore<S extends FoirSchemaDefinition> {
60
99
  /**
61
100
  * Non-model root namespaces (`search`, `customers`, `notifications`).
62
- * Separate accessor because the wire distinguishes `model` from `namespace`
63
- * and the client has no runtime schema to disambiguate with.
101
+ * Separate accessor because the addressing has to distinguish a model from a
102
+ * namespace and the client has no runtime schema to disambiguate with.
64
103
  */
65
104
  readonly ns: {
66
- [NK in keyof S['namespaces']]: FoirNamespaceClient<S['namespaces'][NK]>;
105
+ [NK in keyof S['namespaces']]: FoirNamespaceClient<S['namespaces'][NK]> & FoirNamespaceSubscriptions<NK>;
67
106
  };
68
107
  /**
69
- * Untyped passthrough to `POST /v1/query`.
108
+ * Untyped passthrough — a raw GraphQL document, executed on this credential.
109
+ *
110
+ * It changed meaning with the transport, and deliberately so. It used to take
111
+ * a JSON op because that was the wire; now the wire is GraphQL, and an escape
112
+ * hatch that could only say what the JSON op could say would be an escape from
113
+ * nothing. As a document it reaches everything the credential can reach —
114
+ * including the root fields no model or namespace claims (`designTokens`,
115
+ * `record`, `locales`, `resolvedLocale`, the whole customer-auth mutation
116
+ * set), which the JSON op could not address at all.
117
+ *
118
+ * Returns the whole `data` object rather than a single root field, because a
119
+ * hand-written document may legitimately have several. Routing, the persisted
120
+ * -query handshake and `FoirRequestError` are exactly as on the typed path.
121
+ *
122
+ * ```ts
123
+ * const { pages } = await foir.raw<{ pages: { totalCount: number } }>(
124
+ * 'query Recent($n: Int) { pages(first: $n, orderBy: { createdAt: DESC }) { totalCount } }',
125
+ * { n: 5 },
126
+ * );
127
+ * ```
128
+ */
129
+ raw<T = unknown>(document: string, variables?: Record<string, unknown>): Promise<T>;
130
+ /**
131
+ * Close the subscription socket and drop every live subscription.
70
132
  *
71
- * Every generated-type system needs an escape hatch; shipping one
72
- * deliberately is better than people forking the client when the generated
73
- * types cannot express something. Returns the single root field, same as a
74
- * typed call, and throws the same `FoirRequestError`.
133
+ * Only needed by a long-lived process that discards a client — a browser tab
134
+ * or an edge invocation just goes away. The socket is lazy and self-closing
135
+ * (it opens on the first subscription and closes when the last is cancelled),
136
+ * so a client that only queries never has one to dispose of.
75
137
  */
76
- raw<T = unknown>(op: FoirRawOp): Promise<T>;
138
+ dispose(): void;
77
139
  }
78
140
  /**
79
141
  * The client, shaped by the credential's manifest. An op that is not in
@@ -83,12 +145,22 @@ export interface FoirClientCore<S extends FoirSchemaDefinition> {
83
145
  export type FoirClient<S extends FoirSchemaDefinition> = FoirClientCore<S> & {
84
146
  [MK in keyof S['models']]: FoirModelClient<S, S['models'][MK]>;
85
147
  };
86
- export declare function createFoirClient<S extends FoirSchemaDefinition>(config: FoirClientConfig): FoirClient<S>;
148
+ export declare function createFoirClient<S extends FoirSchemaDefinition = FoirSchemaDefinition>(clientConfig: FoirClientConfig<S>): FoirClient<S>;
87
149
  /**
88
150
  * `createFoirClient` from the `PLATFORM_API_URL` / `PLATFORM_API_KEY` /
89
151
  * `FOIR_PREVIEW` convention `@foir/sdk/client` already uses, so a storefront
90
152
  * that has an env wired for GraphQL can add the data client without a second
91
153
  * set of variables. Preview picks the `sk_` `PLATFORM_PREVIEW_API_KEY` and the
92
154
  * draft schema channel, exactly as `foirRequest` does.
155
+ *
156
+ * ```ts
157
+ * import * as generated from './foir.generated';
158
+ * const foir = createFoirClientFromEnv(env, { generated });
159
+ * ```
160
+ *
161
+ * `generated` is spelled out in the overrides type rather than left inside the
162
+ * `Omit<…>` so `S` is inferred from it here exactly as it is on `createFoirClient`.
93
163
  */
94
- export declare function createFoirClientFromEnv<S extends FoirSchemaDefinition>(env: FoirEnv, overrides?: Omit<FoirClientConfig, 'apiUrl' | 'apiKey' | 'channel'>): FoirClient<S>;
164
+ export declare function createFoirClientFromEnv<S extends FoirSchemaDefinition = FoirSchemaDefinition>(env: FoirEnv, overrides?: Omit<FoirClientConfig, 'apiUrl' | 'apiKey' | 'channel' | 'generated'> & {
165
+ generated?: FoirGeneratedModule<S>;
166
+ }): FoirClient<S>;
@@ -0,0 +1,251 @@
1
+ /**
2
+ * From a resolved selection tree to a GraphQL document string.
3
+ *
4
+ * ---------------------------------------------------------------------------
5
+ * Why there is no `graphql` dependency here, and never can be
6
+ * ---------------------------------------------------------------------------
7
+ *
8
+ * The tree arrives PRE-RESOLVED. The server walked the built schema once per
9
+ * schema version — unions expanded per member, response-shape aliases assigned,
10
+ * cycles broken, the field budget counted — and published the answer on
11
+ * `GET /v1/manifest`: `models.<key>.selection` for a model's own tree, and
12
+ * `ops.<op>.payload` for the shape of what each op actually returns. What is
13
+ * left at runtime is a filter and a printer, which is string building. That is
14
+ * the whole reason this client shrinks rather than inheriting
15
+ * `query_compiler.go` in TypeScript.
16
+ *
17
+ * The rule that keeps it that way: **nothing below may need to know something
18
+ * about the schema that the manifest does not publish.** Every decision here is
19
+ * made from a flag the tree carries (`alias`, `model`, `union`, `relation`,
20
+ * `truncated`), from a payload's stated `kind`, or from a published argument
21
+ * type. There is no table of op names, no inferred wrapper shape, and no type
22
+ * guessed from a JavaScript value.
23
+ *
24
+ * ---------------------------------------------------------------------------
25
+ * Arguments are variables, not literals
26
+ * ---------------------------------------------------------------------------
27
+ *
28
+ * The manifest publishes each argument's declared GraphQL type
29
+ * (`ops.<op>.argTypes`), so an argument is emitted as a variable definition and
30
+ * its value travels in `variables`:
31
+ *
32
+ * query PageFindMany($orderBy: [PageOrderByInput]) { pages(orderBy: $orderBy) { … } }
33
+ *
34
+ * That is not a nicety. A printer working from argument NAMES alone has to
35
+ * infer each value's literal form from the JavaScript value it holds, which is
36
+ * exact for strings, numbers, booleans and objects and **ambiguous for enums**:
37
+ * `orderBy: { createdAt: DESC }` needs `DESC` unquoted and the client holds the
38
+ * string `'DESC'`, indistinguishable from `naturalKey: "DESC"`. Putting the
39
+ * value in `variables` does not resolve the ambiguity, it removes it — the
40
+ * executor coerces against the declared input type, which is the same path the
41
+ * server's own `compileArgs` took so that no caller byte is spliced into query
42
+ * text.
43
+ *
44
+ * The second-order win is that the document text no longer varies with the
45
+ * argument VALUE, so a persisted-query registry holds one entry per op instead
46
+ * of one per argument tuple. Argument names are emitted in sorted order for the
47
+ * same reason: two callers passing the same arguments in a different object
48
+ * literal order must hash to one entry.
49
+ *
50
+ * An argument the manifest publishes no type for is REFUSED. Falling back to
51
+ * inferring a literal would reinstate the defect above under a condition no
52
+ * caller can see.
53
+ */
54
+ import type { FoirManifestOp, FoirManifestRelation, FoirManifestSelection, FoirManifestSelectionField, FoirManifestSelectionUnion } from './manifest';
55
+ /**
56
+ * One model, as the printer needs it.
57
+ *
58
+ * A structural subset of `FoirManifestModel`, deliberately — so the two ways of
59
+ * getting here are the same shape and there is one code path, not two:
60
+ *
61
+ * - `foir types` bakes it into `foirSelections`, the runtime half of the
62
+ * generated module, and the client never fetches anything.
63
+ * - the fallback fetches `GET /v1/manifest` at construction and passes
64
+ * `manifest.models` straight in, because a `FoirManifestModel` already
65
+ * satisfies this interface.
66
+ */
67
+ export interface FoirSelectionModel {
68
+ /** GraphQL type name, e.g. `'Page'`. Used when a polymorphic relation needs fragments. */
69
+ typeName: string;
70
+ /** Op name → root field, argument types and the resolved shape of the result. */
71
+ ops?: Readonly<Record<string, FoirManifestOp>>;
72
+ /** The complete-by-default selection for this model's own record payload. */
73
+ selection?: FoirManifestSelection;
74
+ /** Relations a caller may `include`. Needed to resolve an include's target. */
75
+ relations?: readonly FoirManifestRelation[];
76
+ }
77
+ /**
78
+ * One namespace, as the printer needs it.
79
+ *
80
+ * A namespace op is a `FoirManifestOp` exactly as a model op is, which is what
81
+ * makes `foir.ns.search.searchRecords()` and `foir.page.findMany()` the same
82
+ * code path below rather than two printers that agree by inspection.
83
+ */
84
+ export interface FoirSelectionNamespace {
85
+ /** Op token → root field, argument types and the resolved shape of the result. */
86
+ ops?: Readonly<Record<string, FoirManifestOp>>;
87
+ }
88
+ /** Model key → its selection. What `foir types` emits as `foirSelections`. */
89
+ export type FoirSelections = Readonly<Record<string, FoirSelectionModel>>;
90
+ /** Namespace name → its ops. What `foir types` emits as `foirNamespaceSelections`. */
91
+ export type FoirNamespaceSelections = Readonly<Record<string, FoirSelectionNamespace>>;
92
+ /** Union type name → its expansion. What `foir types` emits as `foirSelectionUnions`. */
93
+ export type FoirSelectionUnions = Readonly<Record<string, FoirManifestSelectionUnion>>;
94
+ /** Everything the printer reads. `FoirManifest` satisfies it as-is. */
95
+ export interface FoirSelectionSource {
96
+ models: FoirSelections;
97
+ namespaces?: FoirNamespaceSelections;
98
+ selectionUnions?: FoirSelectionUnions;
99
+ }
100
+ /**
101
+ * `_id` is emitted whatever the caller asked for — the server's own
102
+ * `protectedSelectionFields` does the same, and a record you cannot address is
103
+ * not a useful answer. Named here so `pick` adds it back and `omit` refuses it.
104
+ *
105
+ * Exported because the subject registry reads it back off the response: every
106
+ * record in a response carries this key precisely because no narrowing can
107
+ * remove it, and that guarantee is this constant's, not the registry's.
108
+ */
109
+ export declare const PROTECTED_ROOT_FIELD = "_id";
110
+ /**
111
+ * Print a complete selection body from a resolved tree, with no narrowing.
112
+ *
113
+ * The one entry point for a caller that has a `ManifestSelection` and wants the
114
+ * whole of it — today, the subscription transport, whose payload trees the
115
+ * manifest publishes exactly as it publishes a model's.
116
+ *
117
+ * It exists so there is ONE implementation of "walk a resolved tree and emit
118
+ * braces". Every rule a reader would otherwise have to re-derive lives in
119
+ * `printFields`: relations are include-governed and skipped, a `truncated` field
120
+ * degrades to `__typename` rather than an illegal empty body, a union emits
121
+ * `__typename` plus one inline fragment per member, a `model` reference expands
122
+ * to that model's published tree, and an alias prints as `alias: key`. A second
123
+ * walk elsewhere in this package would be correct on the day it was written and
124
+ * wrong on the day any of those changed.
125
+ *
126
+ * `isRoot` is false: the `_id` protection is a property of a MODEL's root
127
+ * selection, and an event payload has no records in it to address.
128
+ */
129
+ export declare function printFoirSelectionBody(fields: readonly FoirManifestSelectionField[], unions: FoirSelectionUnions, models?: FoirSelections): string;
130
+ /**
131
+ * Where a payload puts a record, and which model it is.
132
+ *
133
+ * `record` names one outright, AT THE PAYLOAD ROOT — an empty path. An `object`
134
+ * payload reaches one through a `model` reference at some depth (`edges.node`,
135
+ * `record`), and the path is the RESPONSE keys to it, so an aliased field
136
+ * contributes its alias rather than its key.
137
+ *
138
+ * The empty path is therefore not merely a position: it is the manifest saying
139
+ * this op's result IS one record, which is what the subject registry reads as
140
+ * "the caller addressed this record by name".
141
+ */
142
+ export interface FoirRecordPath {
143
+ /** Response keys from the root field's value down to the record. */
144
+ path: readonly string[];
145
+ /** Model key whose published `selection` is printed at that position. */
146
+ modelKey: string;
147
+ }
148
+ /** One printed operation, and what the transport needs to route it. */
149
+ export interface FoirDocumentPlan {
150
+ /** The document text. Also the thing that gets hashed for APQ. */
151
+ document: string;
152
+ /** The argument values, sent beside the document. Empty when the op takes none. */
153
+ variables: Record<string, unknown>;
154
+ /** True when the op lives on the Mutation root. Decides the route and the keyword. */
155
+ mutation: boolean;
156
+ /** The single root field the response is keyed by. */
157
+ rootField: string;
158
+ /**
159
+ * Fields this document selects, in the unit the server's complexity ceiling is
160
+ * denominated in — the same number `CompiledQuery.Fields` carries on the
161
+ * server side.
162
+ *
163
+ * Measured while printing rather than derived afterwards, and exposed rather
164
+ * than kept private so it is TESTABLE against the document it describes: a
165
+ * counter nobody can compare to the artefact is a counter that drifts from it.
166
+ *
167
+ * OPTIONAL because a plan is not only something this file returns — a
168
+ * hand-built one is a real pattern for executing a document the printer did
169
+ * not produce, and requiring the count there would mean requiring a caller to
170
+ * invent a number about a document this code never measured. Everything
171
+ * `planFoirModelCall` / `planFoirNamespaceCall` return sets it.
172
+ */
173
+ fields?: number;
174
+ /**
175
+ * Where the records this document selects sit in the response, and which
176
+ * model each one is.
177
+ *
178
+ * Read by the subject registry (`subjects.ts`) to answer "what did this page
179
+ * fetch" without inspecting the response's shape — the manifest already said
180
+ * where the records would be, so hunting for objects carrying an `_id` would
181
+ * be re-deriving something published.
182
+ *
183
+ * OPTIONAL for the same reason `fields` is: a hand-built plan for a document
184
+ * this printer never produced has no published payload to read positions off,
185
+ * and inventing them would put subjects against records nobody can vouch for.
186
+ * Absent means "this call records nothing", never "this call fetched nothing".
187
+ */
188
+ records?: readonly FoirRecordPath[];
189
+ }
190
+ /** What a model call carries, already split out of the caller's flat query object. */
191
+ export interface FoirCall {
192
+ model: string;
193
+ op: string;
194
+ args: Record<string, unknown>;
195
+ include?: Record<string, unknown>;
196
+ omit?: readonly string[];
197
+ pick?: readonly string[];
198
+ /** Union levels to expand. See `FoirNamespaceCall.depth`. */
199
+ depth?: number;
200
+ }
201
+ /** What a namespace call carries. Same shape, addressed by namespace. */
202
+ export interface FoirNamespaceCall {
203
+ namespace: string;
204
+ op: string;
205
+ args: Record<string, unknown>;
206
+ include?: Record<string, unknown>;
207
+ omit?: readonly string[];
208
+ pick?: readonly string[];
209
+ /**
210
+ * How many union levels to expand, default `1`.
211
+ *
212
+ * A block union's members can themselves carry block unions, and the server
213
+ * refuses to expand a union already on the current path — so the complete
214
+ * document stops one level in and says so, publishing the union name it
215
+ * stopped in front of. Each level above 1 spends one of those stopping points.
216
+ *
217
+ * It is a BUDGET rather than a setting, though a much larger one since
218
+ * TTS-1448: on a real project the same op measures 318 fields at level 1, 468
219
+ * at level 2 and 618 at level 3, against a server ceiling of 10,000. Those
220
+ * were 837 / 3,513 / 14,217 while a union position was charged every member it
221
+ * could hold rather than the one a value can be.
222
+ *
223
+ * What a level costs is one more expansion at each STOPPING POINT inside the
224
+ * expansion, so it still compounds where a member carries several union-typed
225
+ * fields — and it no longer grows at all with how many members the union has.
226
+ * Where a level does cross the ceiling, `planCall` refuses and names the
227
+ * count, rather than letting the server answer with a number the caller
228
+ * cannot act on.
229
+ */
230
+ depth?: number;
231
+ }
232
+ /**
233
+ * Turn a model call into a document.
234
+ *
235
+ * Every op is printable or REFUSED. There is no third answer: an op the
236
+ * manifest describes no payload for is one this client would have to invent a
237
+ * shape for, and inventing one is what this design exists to stop.
238
+ */
239
+ export declare function planFoirModelCall(source: FoirSelectionSource, call: FoirCall): FoirDocumentPlan;
240
+ /**
241
+ * Turn a namespace call into a document.
242
+ *
243
+ * Identical to `planFoirModelCall` below the addressing, and deliberately the
244
+ * same function underneath: a namespace op is a `FoirManifestOp` carrying a
245
+ * payload exactly as a model op is, so `foir.ns.search.searchRecords()` is
246
+ * printed by the code that prints `foir.page.findMany()` rather than by a
247
+ * second printer that agrees with it by inspection.
248
+ */
249
+ export declare function planFoirNamespaceCall(source: FoirSelectionSource, call: FoirNamespaceCall): FoirDocumentPlan;
250
+ /** True when a hand-written document is a mutation, so it never takes the shared lane. */
251
+ export declare function isFoirMutationDocument(documentText: string): boolean;
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * `@foir/sdk/data` — the typed data client.
3
3
  *
4
- * A second app-runtime transport alongside `@foir/sdk/client`'s GraphQL one,
5
- * shaped by the credential you hand it. `POST /v1/query` takes a JSON op and
6
- * the server compiles it to a GraphQL document, so both surfaces run through
7
- * the same executor, the same scope checks, the same response cache — the SDK
8
- * surface is a projection of `SchemaForCaller(scopes)`, never a parallel
9
- * construction.
10
- *
11
- * What you get over the GraphQL path: no `.graphql` documents, no
12
- * `graphql-codegen` step, and **no `graphql` dependency at all**. This subpath
13
- * imports neither `graphql` nor `@graphql-typed-document-node/core`; the import
14
- * graph is asserted graphql-free by `__tests__/data.test.ts`
4
+ * A typed client over the same GraphQL endpoint `@foir/sdk/client` posts to,
5
+ * shaped by the credential you hand it. It sends a GraphQL document like every
6
+ * other consumer — one execution surface, the same executor, the same scope
7
+ * checks, the same response cache — but you never write one: the document is
8
+ * printed from the selection tree the manifest publishes for that op.
9
+ *
10
+ * What you get over the hand-written GraphQL path: no `.graphql` documents, no
11
+ * `graphql-codegen` step, and **no `graphql` dependency at all**. Printing a
12
+ * pre-resolved tree is string building. This subpath imports neither `graphql`
13
+ * nor `@graphql-typed-document-node/core`; the import graph is asserted
14
+ * graphql-free by `__tests__/data.test.ts`
15
15
  * ("`@foir/sdk/data` has zero graphql dependency").
16
16
  *
17
17
  * `./data` and not `./client` because `./client` is the editor-iframe/GraphQL
@@ -19,12 +19,12 @@
19
19
  *
20
20
  * ```ts
21
21
  * import { createFoirClient } from '@foir/sdk/data';
22
- * import { type FoirSchema, foirSchemaVersion } from './foir.generated';
22
+ * import * as generated from './foir.generated';
23
23
  *
24
- * const foir = createFoirClient<FoirSchema>({
24
+ * const foir = createFoirClient({
25
25
  * apiUrl: 'https://api.foir.dev',
26
26
  * apiKey: process.env.PLATFORM_API_KEY!,
27
- * schemaVersion: foirSchemaVersion,
27
+ * generated,
28
28
  * });
29
29
  *
30
30
  * const page = await foir.page.findUnique({
@@ -34,7 +34,7 @@
34
34
  * });
35
35
  *
36
36
  * const results = await foir.ns.search.searchRecords({ query: 'shoes' });
37
- * const anything = await foir.raw({ model: 'page', op: 'findMany', args: { first: 5 } });
37
+ * const anything = await foir.raw('{ designTokens { name value } }');
38
38
  * ```
39
39
  *
40
40
  * `FoirSchema` comes from `foir types` (TTS-875). The contract it generates
@@ -44,10 +44,29 @@
44
44
  export { createFoirClient, createFoirClientFromEnv } from './client';
45
45
  export type { FoirClient, FoirClientCore, FoirModelClient, FoirNamespaceClient } from './client';
46
46
  export { fetchFoirManifest } from './manifest';
47
- export type { FoirManifest, FoirManifestComposite, FoirManifestModel, FoirManifestNamespace, FoirManifestOp, FoirManifestRelation, FoirManifestScalar, FoirManifestUnclassifiedField, } from './manifest';
48
- export type { CollabInfo, FileBlock, FileValue, FlexibleBlock, FoirAccessGrant, FoirAccessLevel, FoirAccessSource, FoirAccessor, FoirBlockMeta, FoirDegraded, FoirDeviceType, FoirGranteeKind, FoirJsonValue, FoirMyAccess, FoirPlatformType, FoirResolveContext, FoirResourceAccess, FoirUnexpandedRecord, FoirUpdateManyPayload, FoirVisibility, GeoPointValue, ImageBlock, ImageFormat, ImageValue, ImageVariant, ImageVariantSet, MediaBlock, MediaValue, ReferenceBlock, ReferenceValue, RichtextBlock, RichtextValue, TextBlock, VideoBlock, VideoValue, } from './builtins';
49
- export { foirDataEndpoint } from './transport';
50
- export type { FoirClientConfig, FoirRawOp, FoirSchemaDrift } from './transport';
47
+ export type { FoirManifest, FoirManifestComposite, FoirManifestModel, FoirManifestNamespace, FoirManifestOp, FoirManifestOpPayload, FoirManifestRelation, FoirManifestScalar, FoirManifestSelection, FoirManifestSelectionField, FoirManifestSelectionMember, FoirManifestSelectionUnion, FoirManifestStructural, FoirManifestSubscription, FoirManifestSubscriptionArgScope, FoirManifestUnclassifiedField, } from './manifest';
48
+ export { planFoirModelCall, planFoirNamespaceCall } from './document';
49
+ export type { FoirCall, FoirDocumentPlan, FoirNamespaceCall, FoirNamespaceSelections, FoirRecordPath, FoirSelectionModel, FoirSelectionNamespace, FoirSelections, FoirSelectionSource, FoirSelectionUnions, } from './document';
50
+ export { createFoirSubjectRegistry, recordFoirSubjects } from './subjects';
51
+ export type { FoirSubject, FoirSubjectEntry, FoirSubjectOptions, FoirSubjectOrigin, FoirSubjectRegistry, } from './subjects';
52
+ export type { CollabInfo, FileValue, FoirAccessGrant, FoirAccessLevel, FoirAccessSource, FoirAccessor, FoirCreateMyNoteArgs, FoirCustomerNote, FoirCustomerNoteAttachment, FoirDegraded, FoirDepthBelow, FoirDepthLevel, FoirDeviceType, FoirGranteeKind, FoirJsonValue, FoirMyAccess, FoirMyNotesArgs, FoirMyNotesResult, FoirNoteConfig, FoirNoteSubjectType, FoirPlatformType, FoirResolveContext, FoirResourceAccess, FoirUnexpandedRecord, FoirUpdateManyPayload, FoirVisibility, GeoPoint, Image, ImageFormat, ImageVariant, ImageVariantSet, MediaValue, Passage, Prose, ProseSpan, RecordAncestor, Reference, Richtext, Video, } from './builtins';
53
+ /**
54
+ * Pre-TTS-1391 spellings. The `<Name>Block` wrappers and the `<Name>Value`
55
+ * split are the same thing as the types above — an item IS its value — so each
56
+ * of these is an alias, not a separate shape.
57
+ *
58
+ * `MediaBlock`, `TextBlock`, `FlexibleBlock` and `FoirBlockMeta` are NOT here:
59
+ * they described a wrapper, a member and an envelope the platform no longer
60
+ * emits, so there is nothing for them to alias. See `builtins.ts`.
61
+ */
62
+ export type { FileBlock, GeoPointValue, ImageBlock, ImageValue, ReferenceBlock, ReferenceValue, RichtextBlock, RichtextValue, VideoBlock, VideoValue, } from './builtins';
63
+ export { foirDataEndpoint, foirRoute } from './transport';
64
+ export type { FoirClientConfig, FoirGeneratedModule, FoirLane, FoirLaneReason, FoirRouteDecision, FoirRouteInput, FoirSchemaDrift, } from './transport';
65
+ export { foirSocketEndpoint, FoirSocketTransport } from './ws-transport';
66
+ export type { FoirReconnectPolicy, FoirSubscribeRequest, FoirSubscription, FoirSubscriptionHandlers, FoirWebSocketFactory, FoirWebSocketLike, } from './ws-transport';
67
+ export { printSubscriptionDocument } from './subscription-document';
68
+ export type { FoirSubscriptionArgument, PrintSubscriptionDocumentInput, } from './subscription-document';
69
+ export type { FoirExecutionSubscribeOptions, FoirJobProgressEvent, FoirModelSubscriptions, FoirNamespaceSubscriptions, FoirNotificationEvent, FoirOperationExecutionEvent, FoirRecordChangedEvent, } from './subscriptions';
51
70
  export type { FoirConnection, FoirFieldsShape, FoirModelDefinition, FoirNamespaceDefinition, FoirNamespaceOpDefinition, FoirOpDefinition, FoirRelationDefinition, FoirReturnDefinition, FoirSchemaDefinition, } from './schema';
52
71
  export type { FoirInclude, FoirModelOperation, FoirNamespaceOperation, FoirOmittableKey, FoirOpResult, FoirQuery, FoirRecordResult, } from './selection';
53
72
  export { FoirRequestError } from '../transport/foir-http';