@foir/sdk 0.10.0 → 0.12.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 (52) hide show
  1. package/CHANGELOG.md +353 -2
  2. package/README.md +10 -2
  3. package/dist/app/client.d.ts +371 -0
  4. package/dist/app/contract/index.d.ts +23 -0
  5. package/dist/app/contract/proto/apps/v1/apps_pb.d.ts +1465 -0
  6. package/dist/app/contract/proto/apps/v1/discovery_pb.d.ts +678 -0
  7. package/dist/app/dispatch.d.ts +64 -0
  8. package/dist/app/errors.d.ts +50 -0
  9. package/dist/app/index.d.ts +40 -0
  10. package/dist/app/manifest.d.ts +56 -0
  11. package/dist/app/naming.d.ts +31 -0
  12. package/dist/app/record-key.d.ts +17 -0
  13. package/dist/app/ticket.d.ts +37 -0
  14. package/dist/app/vocabulary.generated.d.ts +20 -0
  15. package/dist/app-contract.js +124 -0
  16. package/dist/app.js +998 -0
  17. package/dist/auth/auth.d.ts +24 -6
  18. package/dist/auth/index.d.ts +2 -1
  19. package/dist/auth/ops.d.ts +8 -0
  20. package/dist/auth.js +230 -60
  21. package/dist/client.js +80 -15
  22. package/dist/codegen/preset.d.ts +3 -2
  23. package/dist/data/builtins.d.ts +17 -1
  24. package/dist/data/client.d.ts +10 -8
  25. package/dist/data/document.d.ts +13 -0
  26. package/dist/data/index.d.ts +15 -5
  27. package/dist/data/manifest.d.ts +31 -3
  28. package/dist/data/schema.d.ts +16 -9
  29. package/dist/data/selection.d.ts +15 -0
  30. package/dist/data/transport.d.ts +277 -23
  31. package/dist/data/ws-transport.d.ts +8 -2
  32. package/dist/data.js +313 -72
  33. package/dist/graphql/client.d.ts +1 -1
  34. package/dist/graphql/foir-request.d.ts +1 -1
  35. package/dist/index.js +0 -9
  36. package/dist/live/drift.d.ts +9 -10
  37. package/dist/live.js +1 -1
  38. package/dist/next.js +158 -37
  39. package/dist/placement/index.d.ts +129 -0
  40. package/dist/placement.js +144 -0
  41. package/dist/push.js +212 -50
  42. package/dist/react.js +158 -37
  43. package/dist/server/callback-client.d.ts +30 -9
  44. package/dist/server/continue-run.d.ts +56 -0
  45. package/dist/server/index.d.ts +6 -3
  46. package/dist/server/operation-types.d.ts +114 -65
  47. package/dist/server/read-dispatch.d.ts +53 -0
  48. package/dist/server/require-scoped-token.d.ts +71 -7
  49. package/dist/server/verify-scoped-token.d.ts +109 -3
  50. package/dist/server.js +566 -138
  51. package/dist/transport/foir-http.d.ts +103 -5
  52. package/package.json +24 -6
@@ -21,9 +21,10 @@ export interface FoirCodegenOptions {
21
21
  */
22
22
  apiKey?: string;
23
23
  /**
24
- * Preview (`sk_`) key with `drafts:read`. Reads the DRAFT schema channel so
24
+ * Preview key holding `drafts:read`. Reads the DRAFT schema channel so
25
25
  * freshly pushed (un-promoted) model changes are visible to codegen.
26
- * Strongly preferred — a `pk_` key only sees the published schema.
26
+ * Strongly preferred: a key without `drafts:read` only sees the published
27
+ * schema.
27
28
  */
28
29
  previewKey?: string;
29
30
  /** Output directory for the generated client types. Default `./app/gql/foir/`. */
@@ -423,10 +423,26 @@ export interface FoirResourceAccess {
423
423
  public: boolean;
424
424
  visibility: FoirVisibility;
425
425
  }
426
- /** `updateMany` — how many rows moved, and which. */
426
+ /**
427
+ * `updateMany`: how many rows moved, and which. `replayed` is true when the
428
+ * call's `idempotencyKey` was already used by an earlier attempt of the same
429
+ * update: nothing moved this time, and `count` and `ids` are that attempt's.
430
+ */
427
431
  export interface FoirUpdateManyPayload {
428
432
  count: number;
429
433
  ids: string[];
434
+ replayed: boolean;
435
+ }
436
+ /**
437
+ * `setOwner`: who the record belongs to after an operation handed it to
438
+ * another customer. It carries the owner and not the record, because the
439
+ * operation may have been acting for the previous holder, who can no longer
440
+ * read it. `ownerKind` is always `'customer'` from this op.
441
+ */
442
+ export interface FoirRecordOwner {
443
+ id: string;
444
+ ownerId: string;
445
+ ownerKind: string;
430
446
  }
431
447
  /**
432
448
  * What a customer's note can be ABOUT: the customer-plane subset of the closed
@@ -23,8 +23,9 @@
23
23
  * than named one at a time at every call site — see `resolveGeneratedConfig`
24
24
  * below for why that indirection is worth having.
25
25
  *
26
- * The individual fields (`schemaVersion`, `selections`, `namespaceSelections`,
27
- * `selectionUnions`) still work, and still win where both are given.
26
+ * The individual fields (`surfaceVersion`, `surfaceHashes`, `selections`,
27
+ * `namespaceSelections`, `selectionUnions`) still work, and still win where
28
+ * both are given.
28
29
  *
29
30
  * The call is the same call it always was. What changed underneath is what goes
30
31
  * on the wire: the client loads that op's selection tree, applies
@@ -47,7 +48,7 @@
47
48
  * ---------------------------------------------------------------------------
48
49
  *
49
50
  * 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
+ * carry across the type/value boundary, alongside `foirSurfaceVersion`. Passing
51
52
  * them in costs nothing and is the intended path.
52
53
  *
53
54
  * When they are absent the client fetches `GET /v1/manifest` once, at first use,
@@ -75,7 +76,7 @@
75
76
  import type { FoirModelDefinition, FoirNamespaceOpDefinition, FoirOpDefinition, FoirSchemaDefinition } from './schema';
76
77
  import type { FoirModelOperation, FoirNamespaceOperation } from './selection';
77
78
  import { type FoirEnv } from '../transport/foir-http';
78
- import { type FoirClientConfig, type FoirGeneratedModule } from './transport';
79
+ import { type FoirClientConfig, type FoirKeyGeneratedModule, type FoirKeyClientConfig } from './transport';
79
80
  import { type FoirModelSubscriptions, type FoirNamespaceSubscriptions } from './subscriptions';
80
81
  /**
81
82
  * The ops available on one model for this credential, plus its subscriptions.
@@ -230,8 +231,9 @@ export declare function createFoirClient<S extends FoirSchemaDefinition = FoirSc
230
231
  * `createFoirClient` from the `PLATFORM_API_URL` / `PLATFORM_API_KEY` /
231
232
  * `FOIR_PREVIEW` convention `@foir/sdk/client` already uses, so a storefront
232
233
  * that has an env wired for GraphQL can add the data client without a second
233
- * set of variables. Preview picks the `sk_` `PLATFORM_PREVIEW_API_KEY` and the
234
- * draft schema channel, exactly as `foirRequest` does.
234
+ * set of variables. Preview picks `PLATFORM_PREVIEW_API_KEY`, the preview
235
+ * environment's own key holding `drafts:read`, and the draft schema channel,
236
+ * exactly as `foirRequest` does.
235
237
  *
236
238
  * ```ts
237
239
  * import * as generated from './foir.generated';
@@ -241,7 +243,7 @@ export declare function createFoirClient<S extends FoirSchemaDefinition = FoirSc
241
243
  * `generated` is spelled out in the overrides type rather than left inside the
242
244
  * `Omit<…>` so `S` is inferred from it here exactly as it is on `createFoirClient`.
243
245
  */
244
- export declare function createFoirClientFromEnv<S extends FoirSchemaDefinition = FoirSchemaDefinition>(env: FoirEnv, overrides?: Omit<FoirClientConfig, 'apiUrl' | 'apiKey' | 'channel' | 'generated'> & {
245
- generated?: FoirGeneratedModule<S>;
246
+ export declare function createFoirClientFromEnv<S extends FoirSchemaDefinition = FoirSchemaDefinition>(env: FoirEnv, overrides?: Omit<FoirKeyClientConfig, 'apiUrl' | 'apiKey' | 'channel' | 'generated'> & {
247
+ generated?: FoirKeyGeneratedModule<S>;
246
248
  }): FoirClient<S>;
247
249
  export {};
@@ -237,6 +237,19 @@ export interface FoirNamespaceCall {
237
237
  * shape for, and inventing one is what this design exists to stop.
238
238
  */
239
239
  export declare function planFoirModelCall(source: FoirSelectionSource, call: FoirCall): FoirDocumentPlan;
240
+ /**
241
+ * A create's arguments with an idempotency key, when the server's create takes
242
+ * one and the caller sent none.
243
+ *
244
+ * The key is made here, before the first attempt, so a retry of the same call
245
+ * carries the same key and the platform answers it with the record the first
246
+ * attempt made rather than making a second. A key made on the server would be
247
+ * new on every attempt, and one derived from the request would merge two
248
+ * deliberate, identical creates into one. A caller that sends its own key keeps
249
+ * it, and an update is left alone: an increment's key is the caller's to
250
+ * choose, since only the caller knows which calls are the same one.
251
+ */
252
+ export declare function withCreateIdempotencyKey(source: FoirSelectionSource, model: string, op: string, args: Record<string, unknown>): Record<string, unknown>;
240
253
  /**
241
254
  * Turn a namespace call into a document.
242
255
  *
@@ -37,6 +37,15 @@
37
37
  * const anything = await foir.raw('{ designTokens { name value } }');
38
38
  * ```
39
39
  *
40
+ * Inside an operation endpoint the client is built from the dispatch instead of
41
+ * a key, and calls Foir as the operation, with the module `foir types
42
+ * --operation <key>` generated for that operation:
43
+ *
44
+ * ```ts
45
+ * const { dispatch } = await requireScopedToken(request, { expectedProjectId });
46
+ * const foir = createFoirClient({ apiUrl, dispatch, generated });
47
+ * ```
48
+ *
40
49
  * `FoirSchema` comes from `foir types`. The contract it generates
41
50
  * against is written down in `schema.ts` — read that file before changing
42
51
  * anything here, because a generator is written against it.
@@ -44,12 +53,13 @@
44
53
  export { createFoirClient, createFoirClientFromEnv, graphql } from './client';
45
54
  export type { FoirClient, FoirClientCore, FoirModelClient, FoirNamespaceClient, FoirRawOptions, TypedFoirDocument, } from './client';
46
55
  export { fetchFoirManifest } from './manifest';
56
+ export type { FoirManifestRequestOptions } from './manifest';
47
57
  export type { FoirManifest, FoirManifestComposite, FoirManifestModel, FoirManifestNamespace, FoirManifestOp, FoirManifestOpPayload, FoirManifestRelation, FoirManifestScalar, FoirManifestSelection, FoirManifestSelectionField, FoirManifestSelectionMember, FoirManifestSelectionUnion, FoirManifestStructural, FoirManifestSubscription, FoirManifestSubscriptionArgScope, FoirManifestUnclassifiedField, } from './manifest';
48
58
  export { planFoirModelCall, planFoirNamespaceCall } from './document';
49
59
  export type { FoirCall, FoirDocumentPlan, FoirNamespaceCall, FoirNamespaceSelections, FoirRecordPath, FoirSelectionModel, FoirSelectionNamespace, FoirSelections, FoirSelectionSource, FoirSelectionUnions, } from './document';
50
60
  export { createFoirSubjectRegistry, recordFoirSubjects } from './subjects';
51
61
  export type { FoirSubject, FoirSubjectEntry, FoirSubjectOptions, FoirSubjectOrigin, FoirSubjectRegistry, } from './subjects';
52
- export type { CollabInfo, FileValue, FoirAccessGrant, FoirAccessLevel, FoirAccessSource, FoirAccessor, FoirCreateMyNoteArgs, FoirSubmitAnonymousNoteArgs, FoirAnonymousNoteReceipt, 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';
62
+ export type { CollabInfo, FileValue, FoirAccessGrant, FoirAccessLevel, FoirAccessSource, FoirAccessor, FoirCreateMyNoteArgs, FoirSubmitAnonymousNoteArgs, FoirAnonymousNoteReceipt, FoirCustomerNote, FoirCustomerNoteAttachment, FoirDegraded, FoirDepthBelow, FoirDepthLevel, FoirDeviceType, FoirGranteeKind, FoirJsonValue, FoirMyAccess, FoirMyNotesArgs, FoirMyNotesResult, FoirNoteConfig, FoirNoteSubjectType, FoirPlatformType, FoirRecordOwner, FoirResolveContext, FoirResourceAccess, FoirUnexpandedRecord, FoirUpdateManyPayload, FoirVisibility, GeoPoint, Image, ImageFormat, ImageVariant, ImageVariantSet, MediaValue, Passage, Prose, ProseSpan, RecordAncestor, Reference, Richtext, Video, } from './builtins';
53
63
  /**
54
64
  * Pre-rename spellings. The `<Name>Block` wrappers and the `<Name>Value`
55
65
  * split are the same thing as the types above — an item IS its value — so each
@@ -61,15 +71,15 @@ export type { CollabInfo, FileValue, FoirAccessGrant, FoirAccessLevel, FoirAcces
61
71
  */
62
72
  export type { FileBlock, GeoPointValue, ImageBlock, ImageValue, ReferenceBlock, ReferenceValue, RichtextBlock, RichtextValue, VideoBlock, VideoValue, } from './builtins';
63
73
  export { FoirSchemaDriftError, foirDataEndpoint, foirRoute } from './transport';
64
- export type { FoirClientConfig, FoirGeneratedModule, FoirLane, FoirLaneReason, FoirRouteDecision, FoirRouteInput, FoirSchemaDrift, } from './transport';
74
+ export type { FoirClientConfig, FoirDispatchClientConfig, FoirDispatchCredential, FoirGeneratedModule, FoirKeyClientConfig, FoirKeyGeneratedModule, FoirLane, FoirLaneReason, FoirOperationCredential, FoirOperationGeneratedModule, FoirRequestConfig, FoirRouteDecision, FoirRouteInput, FoirSchemaDrift, } from './transport';
65
75
  export { foirSocketEndpoint, FoirSocketTransport } from './ws-transport';
66
76
  export type { FoirReconnectPolicy, FoirSubscribeRequest, FoirSubscription, FoirSubscriptionHandlers, FoirWebSocketFactory, FoirWebSocketLike, } from './ws-transport';
67
77
  export { printSubscriptionDocument } from './subscription-document';
68
78
  export type { FoirSubscriptionArgument, PrintSubscriptionDocumentInput, } from './subscription-document';
69
79
  export type { FoirChangeSubscribeOptions, FoirExecutionSubscribeOptions, FoirJobProgressEvent, FoirModelSubscriptions, FoirNamespaceSubscriptions, FoirNotificationEvent, FoirOperationExecutionEvent, FoirRecordChangedEvent, } from './subscriptions';
70
80
  export type { FoirConnection, FoirFieldsShape, FoirModelDefinition, FoirNamespaceDefinition, FoirNamespaceOpDefinition, FoirOpDefinition, FoirRelationDefinition, FoirReturnDefinition, FoirSchemaDefinition, } from './schema';
71
- export type { FoirInclude, FoirModelOperation, FoirNamespaceOperation, FoirOmittableKey, FoirOpResult, FoirQuery, FoirRecordResult, } from './selection';
81
+ export type { FoirInclude, FoirModelOperation, FoirNamespaceOperation, FoirOmittableKey, FoirOpResult, FoirQuery, FoirRecordResult, FoirWriteResult, } from './selection';
72
82
  export { FoirDecimal, decimal } from './decimal';
73
83
  export type { FoirDecimalDivOptions, FoirDecimalInput, FoirDecimalRounding, } from './decimal';
74
- export { FoirRequestError } from '../transport/foir-http';
75
- export type { FoirEnv, FoirGraphQLErrorItem, FoirTokenProvider, FoirAuthDowngrade, } from '../transport/foir-http';
84
+ export { FoirRequestError, FoirAuthDowngradedError, FoirSessionExpiredError, FOIR_VISITOR_IP_HEADER, } from '../transport/foir-http';
85
+ export type { FoirEnv, FoirGraphQLErrorItem, FoirTokenProvider, FoirAuthDowngrade, FoirVisitorSource, } from '../transport/foir-http';
@@ -14,7 +14,7 @@
14
14
  * (`api-public/internal/schema/manifest.go`) field for field; where the two
15
15
  * disagree the platform is right and this file is the bug.
16
16
  */
17
- import { type FoirClientConfig } from './transport';
17
+ import { type FoirRequestConfig } from './transport';
18
18
  export interface FoirManifestScalar {
19
19
  key: string;
20
20
  /**
@@ -273,7 +273,25 @@ export interface FoirManifestNamespace {
273
273
  ops?: Record<string, FoirManifestOp>;
274
274
  }
275
275
  export interface FoirManifest {
276
+ /**
277
+ * The invalidation counter for this project and channel. Every write that
278
+ * could change a response moves it, including the many that change nothing
279
+ * here, so it keys the ETag and the server's caches and nothing on the client
280
+ * is pinned to it.
281
+ */
276
282
  schemaVersion: number;
283
+ /**
284
+ * A hash over exactly the sections `foir types` reads, so it moves if and only
285
+ * if regenerating the types would write a different file. The generated
286
+ * module records it, and the drift check compares the two.
287
+ */
288
+ surfaceVersion: string;
289
+ /**
290
+ * The components `surfaceVersion` is folded from, keyed by where each sits in
291
+ * this body: `models.<key>`, `namespaces.<name>`, `selectionUnions.<name>`,
292
+ * `subscriptions`, `composedFor`. A drift names what moved by comparing these.
293
+ */
294
+ surfaceHashes: Record<string, string>;
277
295
  channel: 'published' | 'draft';
278
296
  /**
279
297
  * The organisation whose composition this surface is, absent for the
@@ -441,11 +459,21 @@ export interface FoirManifestSelectionMember {
441
459
  typeName: string;
442
460
  fields: FoirManifestSelectionField[];
443
461
  }
462
+ /** What a manifest request asks for beyond the credential's own surface. */
463
+ export interface FoirManifestRequestOptions {
464
+ /**
465
+ * Describe the surface this operation's dispatch token is served instead of
466
+ * the credential's own (`GET /v1/manifest?operation=<key>`). Answered to a
467
+ * credential that can author the operation (one holding `operations:write`)
468
+ * and refused to any other. `foir types --operation` sends it.
469
+ */
470
+ operation?: string;
471
+ }
444
472
  /**
445
473
  * Fetch the manifest for the credential in `config`.
446
474
  *
447
475
  * Exported because two things outside this file need it: `foir types` reads it
448
476
  * to generate `FoirSchema`, and the client's drift check reads its
449
- * `schemaVersion`. Both would otherwise hand-roll the same fetch.
477
+ * `surfaceVersion`. Both would otherwise hand-roll the same fetch.
450
478
  */
451
- export declare function fetchFoirManifest(config: FoirClientConfig): Promise<FoirManifest>;
479
+ export declare function fetchFoirManifest(config: FoirRequestConfig, options?: FoirManifestRequestOptions): Promise<FoirManifest>;
@@ -19,7 +19,7 @@
19
19
  * ```
20
20
  *
21
21
  * A `.ts` and not a `.d.ts` because the file has a runtime half — the selection
22
- * trees, `foirSchemaVersion`, and `foirSchemaType`, which is what carries
22
+ * trees, `foirSurfaceVersion`, and `foirSchemaType`, which is what carries
23
23
  * `FoirSchema` across the type/value boundary so the client can infer it (a
24
24
  * namespace import carries values; an interface erases). `FoirGeneratedModule`
25
25
  * in `transport.ts` is the shape those exports have to satisfy, and it requires
@@ -30,8 +30,9 @@
30
30
  * // foir.generated.ts — DO NOT EDIT. Generated by `foir types`.
31
31
  * import type { FoirSchemaDefinition } from '@foir/sdk/data';
32
32
  *
33
- * /** `schemaVersion` from the manifest these types were generated from. *\/
34
- * export const foirSchemaVersion = 317;
33
+ * /** The surface these types were generated from. *\/
34
+ * export const foirSurfaceVersion = '6f1c0d2a9b8e4f7c3a5d1e0b2c4f6a8d';
35
+ * export const foirSurfaceHashes: Record<string, string> = { 'models.tilly_block': '…' };
35
36
  * export const foirSchemaChannel = 'published';
36
37
  *
37
38
  * // `<D>` ONLY on the types between the record and a union stopping point —
@@ -71,7 +72,7 @@
71
72
  * }
72
73
  *
73
74
  * export interface FoirSchema extends FoirSchemaDefinition {
74
- * schemaVersion: typeof foirSchemaVersion;
75
+ * surfaceVersion: typeof foirSurfaceVersion;
75
76
  * channel: 'published';
76
77
  * models: {
77
78
  * tilly_block: {
@@ -211,10 +212,16 @@ export interface FoirRelationDefinition {
211
212
  /** True when the relation itself can resolve null. */
212
213
  readonly nullable: boolean;
213
214
  }
214
- /** How an op's result wraps (or does not wrap) the model. */
215
+ /**
216
+ * How an op's result wraps (or does not wrap) the model. `write` is a record
217
+ * write's result: the record, and whether the answer is a replay (see
218
+ * `FoirWriteResult`).
219
+ */
215
220
  export type FoirReturnDefinition = {
216
221
  readonly kind: 'record';
217
222
  readonly nullable: boolean;
223
+ } | {
224
+ readonly kind: 'write';
218
225
  } | {
219
226
  readonly kind: 'connection';
220
227
  } | {
@@ -282,12 +289,12 @@ export interface FoirNamespaceDefinition {
282
289
  */
283
290
  export interface FoirSchemaDefinition {
284
291
  /**
285
- * `schemaVersion` from the manifest these types were generated from. Kept as
292
+ * `surfaceVersion` from the manifest these types were generated from. Kept as
286
293
  * a literal so a human reading the generated file sees it, and mirrored by a
287
- * runtime `foirSchemaVersion` const for drift detection — the type cannot be
288
- * read at runtime.
294
+ * runtime `foirSurfaceVersion` const for drift detection, because the type
295
+ * cannot be read at runtime.
289
296
  */
290
- readonly schemaVersion: number;
297
+ readonly surfaceVersion: string;
291
298
  readonly channel: 'published' | 'draft';
292
299
  readonly models: Readonly<Record<string, FoirModelDefinition>>;
293
300
  readonly namespaces: Readonly<Record<string, FoirNamespaceDefinition>>;
@@ -428,11 +428,26 @@ type WrapRelation<R extends FoirRelationDefinition, T> = R['list'] extends true
428
428
  export type FoirRecordResult<S extends FoirSchemaDefinition, M extends FoirModelDefinition, Q, D extends number = IncludeDepth, Depth extends number = DepthOf<Q>> = Q extends {
429
429
  include: infer I;
430
430
  } ? ApplySelection<M, Q, Depth> & IncludedRelations<S, M, I, D, Depth> : ApplySelection<M, Q, Depth>;
431
+ /**
432
+ * What a record write (`create`, `update`, `rename`) answers with.
433
+ *
434
+ * `replayed` is true when the call carried an `idempotencyKey` an earlier
435
+ * attempt of the same write already used: the platform wrote nothing this time
436
+ * and `record` is as that first attempt left it. A caller that acts on the
437
+ * result (a stock count read back after a take, say) checks it, because a
438
+ * replayed answer looks exactly like a fresh one.
439
+ */
440
+ export interface FoirWriteResult<R> {
441
+ record: R;
442
+ replayed: boolean;
443
+ }
431
444
  /** Apply an op's return wrapper to the shaped record. */
432
445
  export type FoirOpResult<S extends FoirSchemaDefinition, M extends FoirModelDefinition, O extends FoirOpDefinition, Q> = O['returns'] extends {
433
446
  kind: 'opaque';
434
447
  type: infer T;
435
448
  } ? T : O['returns'] extends {
449
+ kind: 'write';
450
+ } ? FoirWriteResult<FoirRecordResult<S, M, Q>> : O['returns'] extends {
436
451
  kind: 'connection';
437
452
  } ? FoirConnection<FoirRecordResult<S, M, Q>> : O['returns'] extends {
438
453
  kind: 'list';