@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.
- package/CHANGELOG.md +325 -0
- package/README.md +63 -0
- package/dist/auth/auth.d.ts +140 -0
- package/dist/auth/hosted.d.ts +40 -0
- package/dist/auth/index.d.ts +15 -0
- package/dist/auth/operations.d.ts +123 -0
- package/dist/auth/session-client.d.ts +76 -0
- package/dist/auth-session.js +137 -0
- package/dist/auth.js +238 -26
- package/dist/client/platform-client.d.ts +29 -0
- package/dist/client.js +281 -37
- package/dist/codegen/block-registry.d.ts +4 -8
- package/dist/codegen/block-scaffold.d.ts +4 -8
- package/dist/codegen/index.d.ts +8 -62
- package/dist/codegen/preset.d.ts +60 -0
- package/dist/codegen/shared.d.ts +25 -0
- package/dist/components/EditorProvider.d.ts +36 -0
- package/dist/content/content-renderer.d.ts +47 -0
- package/dist/content/index.d.ts +12 -0
- package/dist/content/richtext.d.ts +70 -0
- package/dist/data/builtins.d.ts +296 -0
- package/dist/data/client.d.ts +94 -0
- package/dist/data/index.d.ts +54 -0
- package/dist/data/manifest.d.ts +161 -0
- package/dist/data/schema.d.ts +243 -0
- package/dist/data/selection.d.ts +330 -0
- package/dist/data/transport.d.ts +107 -0
- package/dist/data.js +309 -0
- package/dist/graphql/cdn-route.d.ts +42 -0
- package/dist/graphql/client.d.ts +30 -0
- package/dist/graphql/content-client.d.ts +66 -0
- package/dist/graphql/foir-request.d.ts +47 -0
- package/dist/hooks/useAutoResize.d.ts +22 -0
- package/dist/hooks/useEditor.d.ts +16 -0
- package/dist/index.d.ts +10 -394
- package/dist/media/BlurhashCanvas.d.ts +10 -0
- package/dist/media/ProgressiveMedia.d.ts +56 -0
- package/dist/media/index.d.ts +14 -0
- package/dist/media/media.d.ts +139 -0
- package/dist/media/useProgressiveLoad.d.ts +23 -0
- package/dist/media.js +5 -0
- package/dist/next/cookieAdapter.d.ts +26 -0
- package/dist/next/index.d.ts +15 -0
- package/dist/next/middleware.d.ts +49 -0
- package/dist/next/session.d.ts +53 -0
- package/dist/next.js +230 -26
- package/dist/protocol/constants.d.ts +6 -0
- package/dist/protocol/messages.d.ts +155 -0
- package/dist/protocol/validation.d.ts +8 -0
- package/dist/push/index.d.ts +14 -0
- package/dist/push/operations.d.ts +22 -0
- package/dist/push/web-push.d.ts +39 -0
- package/dist/push.js +230 -26
- package/dist/react/FoirProvider.d.ts +116 -0
- package/dist/react/OAuthButtons.d.ts +20 -0
- package/dist/react/SignIn.d.ts +22 -0
- package/dist/react/UserButton.d.ts +13 -0
- package/dist/react/index.d.ts +19 -0
- package/dist/react/localStorageAdapter.d.ts +10 -0
- package/dist/react.js +230 -26
- package/dist/{realtime.d.ts → realtime/index.d.ts} +35 -16
- package/dist/server/callback-client.d.ts +92 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/operation-types.d.ts +134 -0
- package/dist/server/require-scoped-token.d.ts +76 -0
- package/dist/server/server-client.d.ts +34 -0
- package/dist/server/types.d.ts +54 -0
- package/dist/server/verify-scoped-token.d.ts +100 -0
- package/dist/server/webhook-helpers.d.ts +36 -0
- package/dist/transport/foir-http.d.ts +217 -0
- package/dist/transport/retry.d.ts +61 -0
- package/dist/types/index.d.ts +124 -0
- package/package.json +23 -13
- package/dist/auth.d.ts +0 -382
- package/dist/client.d.ts +0 -135
- package/dist/content.d.ts +0 -96
- package/dist/media.d.ts +0 -234
- package/dist/next.d.ts +0 -178
- package/dist/push.d.ts +0 -109
- package/dist/react.d.ts +0 -319
- package/dist/server.d.ts +0 -534
- 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>;
|