@foir/sdk 0.9.0 → 0.11.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 +719 -27
- package/README.md +121 -2
- package/dist/auth/auth.d.ts +72 -1
- package/dist/auth/hosted.d.ts +71 -4
- package/dist/auth/index.d.ts +3 -3
- package/dist/auth/operations.d.ts +84 -0
- package/dist/auth/ops.d.ts +18 -0
- package/dist/auth.js +209 -47
- package/dist/client.js +101 -13
- package/dist/codegen/block-registry.d.ts +1 -1
- package/dist/codegen/block-scaffold.d.ts +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/codegen/item-registry.d.ts +1 -1
- package/dist/codegen/item-scaffold.d.ts +1 -1
- package/dist/codegen/preset.d.ts +3 -2
- package/dist/content/content-renderer.d.ts +4 -5
- package/dist/content/index.d.ts +1 -1
- package/dist/content/richtext.d.ts +1 -1
- package/dist/data/builtins.d.ts +81 -13
- package/dist/data/client.d.ts +92 -9
- package/dist/data/decimal.d.ts +212 -0
- package/dist/data/document.d.ts +14 -1
- package/dist/data/index.d.ts +22 -10
- package/dist/data/manifest.d.ts +40 -4
- package/dist/data/schema.d.ts +18 -11
- package/dist/data/selection.d.ts +15 -0
- package/dist/data/subscriptions.d.ts +36 -10
- package/dist/data/transport.d.ts +363 -24
- package/dist/data/ws-transport.d.ts +8 -2
- package/dist/data.js +591 -64
- package/dist/graphql/cdn-route.d.ts +1 -1
- package/dist/graphql/foir-request.d.ts +1 -1
- package/dist/index.js +0 -9
- package/dist/live/coalesce.d.ts +57 -0
- package/dist/live/drift.d.ts +58 -0
- package/dist/live/events.d.ts +13 -0
- package/dist/live/headers.d.ts +53 -0
- package/dist/live/index.d.ts +54 -0
- package/dist/live/keys.d.ts +72 -0
- package/dist/live/manager.d.ts +77 -0
- package/dist/live/react/index.d.ts +143 -0
- package/dist/live/reconcile.d.ts +82 -0
- package/dist/live/resync.d.ts +59 -0
- package/dist/live/store.d.ts +134 -0
- package/dist/live/tagIndex.d.ts +29 -0
- package/dist/live/tanstack.d.ts +127 -0
- package/dist/live/writetoken.d.ts +86 -0
- package/dist/live-react.js +395 -0
- package/dist/live.js +845 -0
- package/dist/next/cacheTags.d.ts +245 -0
- package/dist/next/index.d.ts +7 -0
- package/dist/next.js +260 -34
- package/dist/push.js +163 -36
- package/dist/react.js +157 -33
- package/dist/realtime/index.d.ts +4 -5
- package/dist/server/events-client.d.ts +54 -4
- package/dist/server/index.d.ts +4 -2
- package/dist/server/operation-types.d.ts +86 -58
- package/dist/server/read-dispatch.d.ts +53 -0
- package/dist/server/require-scoped-token.d.ts +20 -1
- package/dist/server.js +602 -10
- package/dist/transport/foir-http.d.ts +168 -15
- package/package.json +10 -2
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* The "switch" is a typed object lookup. Exhaustiveness is enforced where the
|
|
17
17
|
* registry is BUILT — against the exhaustive `ItemRegistry` type the codegen
|
|
18
|
-
* plugin
|
|
18
|
+
* plugin generates per project, so omitting an item type is a compile
|
|
19
19
|
* error there, not a runtime surprise here. The consumer can supply different
|
|
20
20
|
* registries per surface (e.g. web vs email).
|
|
21
21
|
*
|
|
@@ -24,8 +24,7 @@
|
|
|
24
24
|
* A prose field holds ENTRIES. A modeller may name the field `blocks`,
|
|
25
25
|
* `sections`, `modules` or anything else, and this renderer must not care:
|
|
26
26
|
* dispatch is on `__typename` and never on a field name. The `Block*` spellings
|
|
27
|
-
* below are deprecated aliases kept so existing consumers compile
|
|
28
|
-
* TTS-1391.
|
|
27
|
+
* below are deprecated aliases kept so existing consumers compile.
|
|
29
28
|
*/
|
|
30
29
|
import { type ReactNode } from 'react';
|
|
31
30
|
import { type PassageLike } from './richtext';
|
|
@@ -41,7 +40,7 @@ export interface ItemComponentProps<TItem, TCtx = unknown> {
|
|
|
41
40
|
/**
|
|
42
41
|
* The same value as `item`.
|
|
43
42
|
*
|
|
44
|
-
* @deprecated Use `item`. Kept so components scaffolded before
|
|
43
|
+
* @deprecated Use `item`. Kept so components scaffolded before the rename
|
|
45
44
|
* keep compiling; it is passed on every render and will be removed in a
|
|
46
45
|
* future major.
|
|
47
46
|
*/
|
|
@@ -67,7 +66,7 @@ export interface EntriesLike {
|
|
|
67
66
|
* The prose field's entries — the projection wrapper (`{ entries }`), or a
|
|
68
67
|
* plain array — plus the context threaded to every component.
|
|
69
68
|
*
|
|
70
|
-
* `items` is canonical. `blocks` is the pre-
|
|
69
|
+
* `items` is canonical. `blocks` is the pre-rename spelling and still
|
|
71
70
|
* accepted; passing both is a type error rather than a silent precedence rule,
|
|
72
71
|
* because a caller who set both cannot be told which one this honoured.
|
|
73
72
|
*/
|
package/dist/content/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
export { createContentRenderer } from './content-renderer';
|
|
10
10
|
export type { ItemLike, EntriesLike, ItemComponent, ItemComponentProps, ItemRegistry, ContentRendererProps, ContentRendererOptions, PassageRunProps, } from './content-renderer';
|
|
11
11
|
/**
|
|
12
|
-
* Pre-
|
|
12
|
+
* Pre-rename spellings. `Block` was one project's name for a flexible field,
|
|
13
13
|
* never a platform concept; these are kept so existing consumers compile and
|
|
14
14
|
* will be removed in a future major.
|
|
15
15
|
*/
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { type ReactNode, type JSX } from 'react';
|
|
26
26
|
/**
|
|
27
27
|
* safeHref allow-lists link schemes so stored prose can't smuggle a
|
|
28
|
-
* `javascript:` (or `data:`/`vbscript:`) URL that executes on click
|
|
28
|
+
* `javascript:` (or `data:`/`vbscript:`) URL that executes on click.
|
|
29
29
|
* Relative URLs, anchors, and the http(s)/mailto/tel schemes pass; anything
|
|
30
30
|
* else collapses to '#'. Editors are not fully trusted, so this runs on the
|
|
31
31
|
* structured render path, not only the html fallback.
|
package/dist/data/builtins.d.ts
CHANGED
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
* `models[key]` would skip them (an inexhaustive union, silently wrong at
|
|
21
21
|
* runtime) or emit `never`.
|
|
22
22
|
*
|
|
23
|
-
* # These are ONE set, not two
|
|
23
|
+
* # These are ONE set, not two
|
|
24
24
|
*
|
|
25
25
|
* They used to be two: a value object for a plain field, and a separate
|
|
26
|
-
* `<Name>Block` wrapper for the same thing inside a flexible field.
|
|
26
|
+
* `<Name>Block` wrapper for the same thing inside a flexible field. The rename
|
|
27
27
|
* removed the wrapper — an item IS its value — so a plain `image` field and an
|
|
28
28
|
* `image` inside a flexible field are the SAME GraphQL type, `Image`. There is
|
|
29
29
|
* no `ImageValue` in a real schema and never will be again; the only difference
|
|
@@ -78,7 +78,7 @@ export interface Image {
|
|
|
78
78
|
* `String` on the wire, not `String!`.
|
|
79
79
|
*
|
|
80
80
|
* It was non-null here and nullable on the flexible-field member until
|
|
81
|
-
*
|
|
81
|
+
* the rename, which is a §5.3.2 response-shape conflict the moment both are one
|
|
82
82
|
* type. The platform relaxed rather than tightened, because nullable is the
|
|
83
83
|
* honest form — its own public `File` asset type already declares
|
|
84
84
|
* `url: String` — so this follows.
|
|
@@ -215,7 +215,7 @@ export interface RecordAncestor {
|
|
|
215
215
|
* 193 stored reference values carry an id and 92 carry a natural key and no id
|
|
216
216
|
* — so a consumer must handle either.
|
|
217
217
|
*
|
|
218
|
-
* `_id` and `_type` are NOT here, and their absence is the
|
|
218
|
+
* `_id` and `_type` are NOT here, and their absence is the correction
|
|
219
219
|
* rather than an omission. They were the wrapper's own metadata: `_id` was the
|
|
220
220
|
* ITEM's id (never the target's, which is why `_targetId` exists separately) and
|
|
221
221
|
* `_type` was the union discriminator, reading `"reference"` on every reference
|
|
@@ -286,10 +286,10 @@ export interface CollabInfo {
|
|
|
286
286
|
* The three wrappers below have no replacement, because the things they
|
|
287
287
|
* described no longer exist:
|
|
288
288
|
*
|
|
289
|
-
* - `MediaBlock` — `media` stopped being a primitive
|
|
289
|
+
* - `MediaBlock` — `media` stopped being a primitive. It was
|
|
290
290
|
* never in the proto FieldType enum, and it is the one primitive name a
|
|
291
291
|
* project had also used as a model key.
|
|
292
|
-
* - `TextBlock` —
|
|
292
|
+
* - `TextBlock` — removed as well. GraphQL requires a union member to be
|
|
293
293
|
* an object type and a text item's value is a bare scalar, which is the
|
|
294
294
|
* whole reason a wrapper existed. An inline model with a text field is how
|
|
295
295
|
* a text slot is authored.
|
|
@@ -367,7 +367,7 @@ export interface FoirDegraded<TTypename extends string = string> {
|
|
|
367
367
|
* Four because depth is a budget against the server's 10,000-field ceiling and
|
|
368
368
|
* four is past what a generated module usefully describes: on the live dev
|
|
369
369
|
* project `product.findUnique` measures 318 fields at the default, 468 at 2 and
|
|
370
|
-
* 618 at 3. Those levels all fit
|
|
370
|
+
* 618 at 3. Those levels all fit now — they read 1,203 / 4,308 /
|
|
371
371
|
* 13,623 when a union position was charged for every member — so the bound here
|
|
372
372
|
* is about how much generated type surface is worth emitting, not about what
|
|
373
373
|
* the server will run. A call
|
|
@@ -423,13 +423,30 @@ export interface FoirResourceAccess {
|
|
|
423
423
|
public: boolean;
|
|
424
424
|
visibility: FoirVisibility;
|
|
425
425
|
}
|
|
426
|
-
/**
|
|
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
|
-
* What a note
|
|
448
|
+
* What a customer's note can be ABOUT: the customer-plane subset of the closed
|
|
449
|
+
* set the platform's write path enforces.
|
|
433
450
|
*
|
|
434
451
|
* On the wire it is `String!`, not a GraphQL enum, so the manifest publishes no
|
|
435
452
|
* `enumValues` for it and `foir types` cannot generate this union. It is here
|
|
@@ -440,10 +457,14 @@ export interface FoirUpdateManyPayload {
|
|
|
440
457
|
* with a model keyed `file` would make one value mean two things; a note on a
|
|
441
458
|
* record is `'record'` plus the model key on `subjectModelKey`.
|
|
442
459
|
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
460
|
+
* The platform set also holds `project` and `organization`, and this union
|
|
461
|
+
* deliberately does not. Both are operator constructs: the platform answers a
|
|
462
|
+
* note about either through a row policy no customer claim passes, so no note
|
|
463
|
+
* this surface reads or writes can carry them. This mirrors the part of the
|
|
464
|
+
* platform set a customer can reach, and the obligation that comes with a mirror
|
|
465
|
+
* (see the file header) applies: a kind the platform opens to customers widens
|
|
466
|
+
* this union in a minor release, and until then a value outside it is a row no
|
|
467
|
+
* customer writer could have produced.
|
|
447
468
|
*/
|
|
448
469
|
export type FoirNoteSubjectType = 'record' | 'file' | 'model' | 'operation' | 'schedule' | 'customer';
|
|
449
470
|
/**
|
|
@@ -550,6 +571,53 @@ export interface FoirCreateMyNoteArgs {
|
|
|
550
571
|
*/
|
|
551
572
|
attachmentFileIds?: string[];
|
|
552
573
|
}
|
|
574
|
+
/**
|
|
575
|
+
* `submitAnonymousNote` arguments — feedback from a visitor with no account.
|
|
576
|
+
*
|
|
577
|
+
* Present on the project's schema only while the project has opted in
|
|
578
|
+
* (Notes settings → "Accept anonymous notes"). The feedback widget does NOT
|
|
579
|
+
* use this: a signed-out visitor there is still asked to sign in, so this
|
|
580
|
+
* mutation is the only path a visitor's note can take.
|
|
581
|
+
*
|
|
582
|
+
* WRITE-ONLY. Nothing reads an anonymous note back on the public plane, which
|
|
583
|
+
* is why the result is a receipt rather than the note.
|
|
584
|
+
*/
|
|
585
|
+
export interface FoirSubmitAnonymousNoteArgs {
|
|
586
|
+
/**
|
|
587
|
+
* Must be `'record'` — the only thing a visitor may write a note about.
|
|
588
|
+
* Narrower than `FoirCreateMyNoteArgs`, where a customer may also address a
|
|
589
|
+
* file or a model.
|
|
590
|
+
*/
|
|
591
|
+
subjectType: 'record';
|
|
592
|
+
subjectId: string;
|
|
593
|
+
/**
|
|
594
|
+
* The record's model key. REQUIRED here, unlike on `createMyNote` where it is
|
|
595
|
+
* conditional: the subject is always a record on this lane, so the condition
|
|
596
|
+
* is always met.
|
|
597
|
+
*/
|
|
598
|
+
subjectModelKey: string;
|
|
599
|
+
body: string;
|
|
600
|
+
/**
|
|
601
|
+
* Optional. Where the operators may reply.
|
|
602
|
+
*
|
|
603
|
+
* Captured as typed and NEVER verified — it is not treated as proof of who
|
|
604
|
+
* wrote the note, and nothing is sent to it to confirm it. Do not build a
|
|
605
|
+
* flow that assumes the address is reachable.
|
|
606
|
+
*/
|
|
607
|
+
contactEmail?: string;
|
|
608
|
+
}
|
|
609
|
+
/** What `submitAnonymousNote` returns. */
|
|
610
|
+
export interface FoirAnonymousNoteReceipt {
|
|
611
|
+
/**
|
|
612
|
+
* A reference for this submission, to quote when following it up.
|
|
613
|
+
*
|
|
614
|
+
* NOT a handle: there is no read that takes it. Showing it to the visitor is
|
|
615
|
+
* the whole of its purpose.
|
|
616
|
+
*/
|
|
617
|
+
reference: string;
|
|
618
|
+
/** When the platform recorded the submission. */
|
|
619
|
+
submittedAt: string;
|
|
620
|
+
}
|
|
553
621
|
export type FoirDeviceType = 'DESKTOP' | 'MOBILE' | 'TABLET';
|
|
554
622
|
export type FoirPlatformType = 'IOS' | 'ANDROID' | 'WEB';
|
|
555
623
|
/**
|
package/dist/data/client.d.ts
CHANGED
|
@@ -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 (`
|
|
27
|
-
* `selectionUnions`) still work, and still win where
|
|
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 `
|
|
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
|
|
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.
|
|
@@ -95,6 +96,74 @@ export type FoirNamespaceClient<N extends {
|
|
|
95
96
|
}> = {
|
|
96
97
|
[OK in keyof N['ops']]: N['ops'][OK] extends FoirNamespaceOpDefinition ? FoirNamespaceOperation<N['ops'][OK]> : never;
|
|
97
98
|
};
|
|
99
|
+
declare const foirDocumentResult: unique symbol;
|
|
100
|
+
declare const foirDocumentVariables: unique symbol;
|
|
101
|
+
/**
|
|
102
|
+
* A GraphQL document string that carries its own result and variable types.
|
|
103
|
+
*
|
|
104
|
+
* `raw` returns `unknown` because a hand-written document is opaque to the
|
|
105
|
+
* client, so every call site hand-writes a result type that nothing checks. That
|
|
106
|
+
* is the cost the escape hatch was never meant to have: the sites that most need
|
|
107
|
+
* type safety, the ones the typed client could not express, are the ones with
|
|
108
|
+
* none.
|
|
109
|
+
*
|
|
110
|
+
* A branded document moves that annotation from the call site to the document,
|
|
111
|
+
* once, next to the selection it describes. It is still an assertion rather than
|
|
112
|
+
* a proof, because nothing validates it against the live schema, but it is an
|
|
113
|
+
* assertion made in ONE place that every caller then inherits, and a wrong one
|
|
114
|
+
* is wrong somewhere a reader is looking at the query.
|
|
115
|
+
*
|
|
116
|
+
* The brands are phantom: they exist only in the type system, so a
|
|
117
|
+
* `TypedFoirDocument` is a plain string at runtime and can be logged, hashed or
|
|
118
|
+
* sent anywhere a string goes.
|
|
119
|
+
*/
|
|
120
|
+
export type TypedFoirDocument<TResult, TVariables = Record<string, unknown>> = string & {
|
|
121
|
+
readonly [foirDocumentResult]: TResult;
|
|
122
|
+
readonly [foirDocumentVariables]: TVariables;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Brand a GraphQL document with the shape it returns.
|
|
126
|
+
*
|
|
127
|
+
* ```ts
|
|
128
|
+
* const RECENT = graphql<{ pages: { totalCount: number } }, { n: number }>(`
|
|
129
|
+
* query Recent($n: Int) { pages(first: $n) { totalCount } }
|
|
130
|
+
* `);
|
|
131
|
+
*
|
|
132
|
+
* const { pages } = await foir.raw(RECENT, { n: 5 });
|
|
133
|
+
* // ^? { totalCount: number }
|
|
134
|
+
* ```
|
|
135
|
+
*
|
|
136
|
+
* It does nothing at runtime beyond returning the string it was given. The value
|
|
137
|
+
* is entirely in the type, and in the fact that the type now lives beside the
|
|
138
|
+
* document rather than being retyped at each call.
|
|
139
|
+
*
|
|
140
|
+
* This does NOT make a bespoke selection type-checked against the schema. That
|
|
141
|
+
* is the larger half and wants generated documents rather than a helper. What it
|
|
142
|
+
* removes is the retyping, which is where the two real bugs came from: a result
|
|
143
|
+
* type that drifted from the selection it claimed to describe.
|
|
144
|
+
*/
|
|
145
|
+
export declare function graphql<TResult = unknown, TVariables = Record<string, unknown>>(document: string): TypedFoirDocument<TResult, TVariables>;
|
|
146
|
+
/**
|
|
147
|
+
* Per-call options for {@link FoirClient.raw}.
|
|
148
|
+
*/
|
|
149
|
+
export interface FoirRawOptions {
|
|
150
|
+
/**
|
|
151
|
+
* Treat a partial response as a failure.
|
|
152
|
+
*
|
|
153
|
+
* A GraphQL response can carry `errors` AND `data` together, and the default
|
|
154
|
+
* is to warn and return the data: a field the server could not resolve should
|
|
155
|
+
* not blank a whole page, and forward compatibility rests on it.
|
|
156
|
+
*
|
|
157
|
+
* On a hand-written document that default is the expensive one. There is no
|
|
158
|
+
* generated selection to have caught a wrong field name first, so selecting
|
|
159
|
+
* something that does not exist returns everything else and logs — a typo
|
|
160
|
+
* reads as a missing feature rather than a failure, and the hand-written
|
|
161
|
+
* result type asserts it was fine. `strict: true` turns that into a thrown
|
|
162
|
+
* `FoirRequestError`, which still carries the data on `partialData` so a
|
|
163
|
+
* caller can log what it declined to render.
|
|
164
|
+
*/
|
|
165
|
+
strict?: boolean;
|
|
166
|
+
}
|
|
98
167
|
export interface FoirClientCore<S extends FoirSchemaDefinition> {
|
|
99
168
|
/**
|
|
100
169
|
* Non-model root namespaces (`search`, `customers`, `notifications`).
|
|
@@ -126,7 +195,19 @@ export interface FoirClientCore<S extends FoirSchemaDefinition> {
|
|
|
126
195
|
* );
|
|
127
196
|
* ```
|
|
128
197
|
*/
|
|
129
|
-
raw<
|
|
198
|
+
raw<TResult, TVariables extends Record<string, unknown>>(document: TypedFoirDocument<TResult, TVariables>, variables?: TVariables, options?: FoirRawOptions): Promise<TResult>;
|
|
199
|
+
/**
|
|
200
|
+
* The untyped form, for a document with no brand.
|
|
201
|
+
*
|
|
202
|
+
* The document type excludes a branded one deliberately. Without that, a
|
|
203
|
+
* branded call whose VARIABLES do not match falls through to this overload,
|
|
204
|
+
* whose `Record<string, unknown>` accepts anything, and the mistake compiles.
|
|
205
|
+
* The result would still be typed, so the call site reads as checked while the
|
|
206
|
+
* variables are not, which is the failure mode a brand exists to prevent.
|
|
207
|
+
*/
|
|
208
|
+
raw<T = unknown>(document: string & {
|
|
209
|
+
readonly [foirDocumentResult]?: undefined;
|
|
210
|
+
}, variables?: Record<string, unknown>, options?: FoirRawOptions): Promise<T>;
|
|
130
211
|
/**
|
|
131
212
|
* Close the subscription socket and drop every live subscription.
|
|
132
213
|
*
|
|
@@ -150,8 +231,9 @@ export declare function createFoirClient<S extends FoirSchemaDefinition = FoirSc
|
|
|
150
231
|
* `createFoirClient` from the `PLATFORM_API_URL` / `PLATFORM_API_KEY` /
|
|
151
232
|
* `FOIR_PREVIEW` convention `@foir/sdk/client` already uses, so a storefront
|
|
152
233
|
* that has an env wired for GraphQL can add the data client without a second
|
|
153
|
-
* set of variables. Preview picks
|
|
154
|
-
* draft schema channel,
|
|
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.
|
|
155
237
|
*
|
|
156
238
|
* ```ts
|
|
157
239
|
* import * as generated from './foir.generated';
|
|
@@ -161,6 +243,7 @@ export declare function createFoirClient<S extends FoirSchemaDefinition = FoirSc
|
|
|
161
243
|
* `generated` is spelled out in the overrides type rather than left inside the
|
|
162
244
|
* `Omit<…>` so `S` is inferred from it here exactly as it is on `createFoirClient`.
|
|
163
245
|
*/
|
|
164
|
-
export declare function createFoirClientFromEnv<S extends FoirSchemaDefinition = FoirSchemaDefinition>(env: FoirEnv, overrides?: Omit<
|
|
165
|
-
generated?:
|
|
246
|
+
export declare function createFoirClientFromEnv<S extends FoirSchemaDefinition = FoirSchemaDefinition>(env: FoirEnv, overrides?: Omit<FoirKeyClientConfig, 'apiUrl' | 'apiKey' | 'channel' | 'generated'> & {
|
|
247
|
+
generated?: FoirKeyGeneratedModule<S>;
|
|
166
248
|
}): FoirClient<S>;
|
|
249
|
+
export {};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `FoirDecimal` — the exact numeric type a `decimal` field arrives as.
|
|
3
|
+
*
|
|
4
|
+
* A `decimal` field travels as a STRING on the wire, because JSON has one
|
|
5
|
+
* number type and every JavaScript consumer parses it into an IEEE-754 double
|
|
6
|
+
* whatever the server meant. Supabase demonstrates the failure: Postgres stores
|
|
7
|
+
* `numeric` exactly, PostgREST serialises it as a JSON number, and precision
|
|
8
|
+
* dies in `JSON.parse` before any application code runs. Exact storage with a
|
|
9
|
+
* lossy wire format buys nothing.
|
|
10
|
+
*
|
|
11
|
+
* So the string is the transport. It is deliberately NOT what you are handed:
|
|
12
|
+
* `"19.99" + "2.00"` is `"19.992.00"`, silently. This class is the thing that
|
|
13
|
+
* makes that unrepresentable.
|
|
14
|
+
*
|
|
15
|
+
* ---------------------------------------------------------------------------
|
|
16
|
+
* Representation
|
|
17
|
+
* ---------------------------------------------------------------------------
|
|
18
|
+
*
|
|
19
|
+
* A scaled `bigint`: `value = units / 10 ** scale`. `19.99` is
|
|
20
|
+
* `{ units: 1999n, scale: 2 }`. All arithmetic is integer arithmetic on
|
|
21
|
+
* `units`, so it is exact by construction rather than exact by care.
|
|
22
|
+
*
|
|
23
|
+
* `bigint` is ES2020 and the package targets ES2022, so this has NO dependency.
|
|
24
|
+
* That is deliberate: Prisma ships `Decimal.js` instances and prisma/prisma#16397
|
|
25
|
+
* has run for years because callers cannot get a `Decimal.js` whose version
|
|
26
|
+
* matches Prisma's. A type that crosses a package boundary should not carry a
|
|
27
|
+
* third party's identity with it.
|
|
28
|
+
*
|
|
29
|
+
* ---------------------------------------------------------------------------
|
|
30
|
+
* Scale is preserved, equality is numeric
|
|
31
|
+
* ---------------------------------------------------------------------------
|
|
32
|
+
*
|
|
33
|
+
* `decimal('19.90')` renders as `"19.90"`, not `"19.9"`. Trailing zeros are
|
|
34
|
+
* significant to anyone displaying money, and dropping them would make the
|
|
35
|
+
* round trip lossy in the one way people actually notice.
|
|
36
|
+
*
|
|
37
|
+
* But `equals` compares VALUES: `decimal('1.0').equals(decimal('1.00'))` is
|
|
38
|
+
* `true`. Textual comparison would be a trap of a different shape.
|
|
39
|
+
*
|
|
40
|
+
* ---------------------------------------------------------------------------
|
|
41
|
+
* Why `+` throws instead of concatenating
|
|
42
|
+
* ---------------------------------------------------------------------------
|
|
43
|
+
*
|
|
44
|
+
* `Symbol.toPrimitive` answers the `'string'` hint and throws on every other
|
|
45
|
+
* one. So:
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* `Total: ${price}` // "Total: 19.99" — string hint, fine
|
|
49
|
+
* price + tax // TypeError, naming .add()
|
|
50
|
+
* price > limit // TypeError, naming .cmp()
|
|
51
|
+
* Number(price) // TypeError, naming .toNumber()
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* TypeScript already rejects `price + tax` at compile time. The throw is for
|
|
55
|
+
* JavaScript callers and for anything that reaches this through `any`, where a
|
|
56
|
+
* silent `"19.992.00"` would otherwise reach a customer. Loud beats quiet: the
|
|
57
|
+
* whole reason this type exists is that the float bug was silent.
|
|
58
|
+
*
|
|
59
|
+
* `JSON.stringify` is answered by `toJSON`, so a decimal serialises back to the
|
|
60
|
+
* canonical string it arrived as — `{"price":"19.99"}`, never `{"price":{}}`.
|
|
61
|
+
* Prisma's most common support thread is that this behaviour was left implicit;
|
|
62
|
+
* it is pinned by a test here.
|
|
63
|
+
*
|
|
64
|
+
* ---------------------------------------------------------------------------
|
|
65
|
+
* If you are caching, cloning or comparing these, read this
|
|
66
|
+
* ---------------------------------------------------------------------------
|
|
67
|
+
*
|
|
68
|
+
* **Treat a decimal as an opaque leaf.** The state lives in `#`-private fields,
|
|
69
|
+
* which are invisible to every structural walk:
|
|
70
|
+
*
|
|
71
|
+
* ```ts
|
|
72
|
+
* Object.keys(price) // []
|
|
73
|
+
* Object.keys(structuredClone(price)) // [] <- no throw. silently {}.
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* So a generic deep-clone, structural-share or deep-equal sees an EMPTY OBJECT.
|
|
77
|
+
* `structuredClone` does not throw, it just returns `{}`, and any equality that
|
|
78
|
+
* walks own enumerable keys calls two different decimals equal. This is the one
|
|
79
|
+
* that fails quietly, which on money is the worst place for it.
|
|
80
|
+
*
|
|
81
|
+
* Identity is `instanceof`. Equality is `.equals()`. Cloning is unnecessary —
|
|
82
|
+
* every operation returns a new instance and nothing mutates, so share the
|
|
83
|
+
* reference.
|
|
84
|
+
*
|
|
85
|
+
* **A JSON round trip returns a string, not a decimal.** Lossless in value,
|
|
86
|
+
* lossy in type:
|
|
87
|
+
*
|
|
88
|
+
* ```ts
|
|
89
|
+
* JSON.parse(JSON.stringify(price)) // "19.99", a string
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* Rehydrate at the boundary — `decimal(value)` takes the string, and the
|
|
93
|
+
* generated types say which fields are decimals, so the revive is keyed on the
|
|
94
|
+
* schema rather than sniffed.
|
|
95
|
+
*
|
|
96
|
+
* **Scale is display, not identity — so normalise KEYS, never PAYLOADS.**
|
|
97
|
+
* `decimal('1.0').equals(decimal('1.00'))` is true while their `toString()`
|
|
98
|
+
* differs. Two consequences that pull in opposite directions:
|
|
99
|
+
*
|
|
100
|
+
* - Keying a cache on `toString()` splits one value into two entries. If you
|
|
101
|
+
* need a canonical key, `rescale()` to a fixed scale first.
|
|
102
|
+
* - But `rescale()` on a value you will SERVE rewrites what the author entered:
|
|
103
|
+
* `19.90` becomes `19.9`, and a price changes how it displays. Never
|
|
104
|
+
* normalise a stored payload.
|
|
105
|
+
*
|
|
106
|
+
* The cleanest way to hold both is to store the raw wire string and hydrate at
|
|
107
|
+
* the read boundary. The store then never holds two instances to choose
|
|
108
|
+
* between, and persistence is correct by construction.
|
|
109
|
+
*/
|
|
110
|
+
/**
|
|
111
|
+
* How to resolve a value sitting exactly halfway between two representable
|
|
112
|
+
* results. Only `div`, `toFixed` and `rescale` can produce one — `add`, `sub`
|
|
113
|
+
* and `mul` are always exact.
|
|
114
|
+
*
|
|
115
|
+
* - `half-even` — banker's rounding. Ties go to the even neighbour, so a long
|
|
116
|
+
* run of them does not drift upward. The default for `div`, where the result
|
|
117
|
+
* is usually an intermediate value that will be summed.
|
|
118
|
+
* - `half-up` — ties go away from zero. What a person means by "round to 2dp",
|
|
119
|
+
* and the default for `toFixed`, which is a display operation.
|
|
120
|
+
* - `down` — truncate toward zero. Never rounds up, for the cases where over-
|
|
121
|
+
* stating is the expensive direction.
|
|
122
|
+
*/
|
|
123
|
+
export type FoirDecimalRounding = 'half-even' | 'half-up' | 'down';
|
|
124
|
+
/** Options for {@link FoirDecimal.div}. */
|
|
125
|
+
export interface FoirDecimalDivOptions {
|
|
126
|
+
/**
|
|
127
|
+
* Decimal places in the result. Division is the one operation that cannot be
|
|
128
|
+
* exact — `1/3` has no finite decimal expansion — so a scale has to be named.
|
|
129
|
+
* Defaults to 20, which is wider than any currency and narrow enough to stay
|
|
130
|
+
* cheap.
|
|
131
|
+
*/
|
|
132
|
+
scale?: number;
|
|
133
|
+
/** Defaults to `half-even`. */
|
|
134
|
+
rounding?: FoirDecimalRounding;
|
|
135
|
+
}
|
|
136
|
+
/** Anything that can be widened to a decimal without losing precision. */
|
|
137
|
+
export type FoirDecimalInput = string | bigint | FoirDecimal;
|
|
138
|
+
/**
|
|
139
|
+
* An exact decimal number.
|
|
140
|
+
*
|
|
141
|
+
* Construct from a string (the wire form), a `bigint`, or another decimal.
|
|
142
|
+
* A `number` is refused — see {@link FoirDecimal.fromNumber}.
|
|
143
|
+
*/
|
|
144
|
+
export declare class FoirDecimal {
|
|
145
|
+
#private;
|
|
146
|
+
constructor(value: FoirDecimalInput);
|
|
147
|
+
/**
|
|
148
|
+
* Build a decimal from a JavaScript number, accepting the precision that
|
|
149
|
+
* number may already have lost.
|
|
150
|
+
*
|
|
151
|
+
* Deliberately not the constructor. It is for interop — a value read from a
|
|
152
|
+
* third-party JSON API that emitted a float — and naming it at every call
|
|
153
|
+
* site is the point. `fromNumber(0.1 + 0.2)` gives `0.30000000000000004`,
|
|
154
|
+
* because that is genuinely the value it was handed.
|
|
155
|
+
*/
|
|
156
|
+
static fromNumber(value: number): FoirDecimal;
|
|
157
|
+
/** `true` if `value` parses as a decimal. Does not throw. */
|
|
158
|
+
static isDecimalString(value: unknown): value is string;
|
|
159
|
+
/** Decimal places carried by this value. `19.90` has scale 2. */
|
|
160
|
+
get scale(): number;
|
|
161
|
+
/** Re-express at `scale`, rounding away any digits that no longer fit. */
|
|
162
|
+
rescale(scale: number, rounding?: FoirDecimalRounding): FoirDecimal;
|
|
163
|
+
add(other: FoirDecimalInput): FoirDecimal;
|
|
164
|
+
sub(other: FoirDecimalInput): FoirDecimal;
|
|
165
|
+
/** Exact: scales add, so `1.10 × 1.10` is `1.2100` rather than `1.21`. */
|
|
166
|
+
mul(other: FoirDecimalInput): FoirDecimal;
|
|
167
|
+
/** The only inexact operation — see {@link FoirDecimalDivOptions}. */
|
|
168
|
+
div(other: FoirDecimalInput, options?: FoirDecimalDivOptions): FoirDecimal;
|
|
169
|
+
abs(): FoirDecimal;
|
|
170
|
+
neg(): FoirDecimal;
|
|
171
|
+
isZero(): boolean;
|
|
172
|
+
isNegative(): boolean;
|
|
173
|
+
/** `-1`, `0` or `1`. Use this rather than `<`, which throws. */
|
|
174
|
+
cmp(other: FoirDecimalInput): -1 | 0 | 1;
|
|
175
|
+
/** Numeric equality: `1.0` equals `1.00`. Scale is display, not identity. */
|
|
176
|
+
equals(other: FoirDecimalInput): boolean;
|
|
177
|
+
/**
|
|
178
|
+
* Fixed-place string, rounding `half-up` by default because this is the
|
|
179
|
+
* display operation and that is what a person means by "to 2dp".
|
|
180
|
+
*/
|
|
181
|
+
toFixed(places: number, rounding?: FoirDecimalRounding): string;
|
|
182
|
+
/**
|
|
183
|
+
* The canonical wire form, preserving scale. Never exponent notation, so the
|
|
184
|
+
* output always parses back to an equal value.
|
|
185
|
+
*/
|
|
186
|
+
toString(): string;
|
|
187
|
+
/** `JSON.stringify` emits the canonical string, not an object. Pinned by test. */
|
|
188
|
+
toJSON(): string;
|
|
189
|
+
/**
|
|
190
|
+
* Explicitly lossy — this is the boundary back into float, and naming it is
|
|
191
|
+
* the point. Fine for a chart axis; never for money you will add up.
|
|
192
|
+
*/
|
|
193
|
+
toNumber(): number;
|
|
194
|
+
/**
|
|
195
|
+
* Answers the `'string'` hint so template literals work, and throws on every
|
|
196
|
+
* other hint so `+`, `<` and `Number()` fail loudly instead of silently
|
|
197
|
+
* concatenating or truncating.
|
|
198
|
+
*/
|
|
199
|
+
[Symbol.toPrimitive](hint: string): string;
|
|
200
|
+
get [Symbol.toStringTag](): string;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Shorthand for `new FoirDecimal(value)`.
|
|
204
|
+
*
|
|
205
|
+
* ```ts
|
|
206
|
+
* import { decimal } from '@foir/sdk/data';
|
|
207
|
+
*
|
|
208
|
+
* const total = decimal(product.price).mul('3').add(shipping);
|
|
209
|
+
* console.log(total.toFixed(2)); // "62.97"
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
export declare function decimal(value: FoirDecimalInput): FoirDecimal;
|
package/dist/data/document.d.ts
CHANGED
|
@@ -215,7 +215,7 @@ export interface FoirNamespaceCall {
|
|
|
215
215
|
* stopped in front of. Each level above 1 spends one of those stopping points.
|
|
216
216
|
*
|
|
217
217
|
* It is a BUDGET rather than a setting, though a much larger one since
|
|
218
|
-
*
|
|
218
|
+
* On a real project the same op measures 318 fields at level 1, 468
|
|
219
219
|
* at level 2 and 618 at level 3, against a server ceiling of 10,000. Those
|
|
220
220
|
* were 837 / 3,513 / 14,217 while a union position was charged every member it
|
|
221
221
|
* could hold rather than the one a value can be.
|
|
@@ -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
|
*
|