@foir/sdk 0.8.0 → 0.10.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 +550 -28
- package/README.md +111 -0
- package/dist/auth/auth.d.ts +72 -1
- package/dist/auth/hosted.d.ts +93 -4
- package/dist/auth/index.d.ts +4 -4
- package/dist/auth/operations.d.ts +84 -0
- package/dist/auth/ops.d.ts +10 -0
- package/dist/auth.js +138 -19
- package/dist/client.js +63 -6
- 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/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 +131 -12
- package/dist/data/client.d.ts +82 -1
- package/dist/data/decimal.d.ts +212 -0
- package/dist/data/document.d.ts +1 -1
- package/dist/data/index.d.ts +11 -9
- package/dist/data/manifest.d.ts +33 -6
- package/dist/data/schema.d.ts +2 -2
- package/dist/data/subscriptions.d.ts +36 -10
- package/dist/data/transport.d.ts +120 -3
- package/dist/data.js +377 -16
- package/dist/graphql/cdn-route.d.ts +1 -1
- package/dist/graphql/foir-request.d.ts +1 -1
- package/dist/live/coalesce.d.ts +57 -0
- package/dist/live/drift.d.ts +59 -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 +179 -6
- package/dist/push.js +81 -8
- package/dist/react.js +76 -5
- package/dist/realtime/index.d.ts +4 -5
- package/dist/server/events-client.d.ts +54 -4
- package/dist/server/index.d.ts +3 -2
- package/dist/server/require-scoped-token.d.ts +1 -1
- package/dist/server.js +454 -2
- package/dist/transport/foir-http.d.ts +113 -10
- package/package.json +10 -2
package/dist/data.js
CHANGED
|
@@ -5,11 +5,30 @@ function isFoirPreview(env) {
|
|
|
5
5
|
function resolveFoirApiKey(env) {
|
|
6
6
|
return isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
|
|
7
7
|
}
|
|
8
|
+
var MALFORMED_PRUNE_REASONS = /* @__PURE__ */ new Set([
|
|
9
|
+
"unknown_field",
|
|
10
|
+
"subselections_on_scalar",
|
|
11
|
+
"missing_subselections"
|
|
12
|
+
]);
|
|
8
13
|
var FoirRequestError = class extends Error {
|
|
9
14
|
/** HTTP status code, when the request failed at the HTTP layer. */
|
|
10
15
|
status;
|
|
11
16
|
/** GraphQL errors, when the request failed with a no-data error envelope. */
|
|
12
17
|
graphQLErrors;
|
|
18
|
+
/**
|
|
19
|
+
* The `data` the server did return, when a STRICT read refused a partial
|
|
20
|
+
* response. Present only on that path: without it, opting into strictness
|
|
21
|
+
* would trade a silent wrong answer for a thrown one that discards the answer,
|
|
22
|
+
* and a caller that wants to log what it nearly rendered could not.
|
|
23
|
+
*/
|
|
24
|
+
partialData;
|
|
25
|
+
/**
|
|
26
|
+
* The selections the server stripped, when the refusal was a pruned document
|
|
27
|
+
* rather than a partial error. Same purpose as `partialData`: a caller that
|
|
28
|
+
* wants to log what it nearly rendered, or name the offending path in a
|
|
29
|
+
* developer-facing message, has it without re-parsing anything.
|
|
30
|
+
*/
|
|
31
|
+
prunedSelections;
|
|
13
32
|
cause;
|
|
14
33
|
constructor(message, options) {
|
|
15
34
|
super(message);
|
|
@@ -17,6 +36,8 @@ var FoirRequestError = class extends Error {
|
|
|
17
36
|
this.cause = options?.cause;
|
|
18
37
|
this.status = options?.status;
|
|
19
38
|
this.graphQLErrors = options?.graphQLErrors;
|
|
39
|
+
this.partialData = options?.partialData;
|
|
40
|
+
this.prunedSelections = options?.prunedSelections;
|
|
20
41
|
}
|
|
21
42
|
/**
|
|
22
43
|
* The code of the first error, if the response carried one.
|
|
@@ -66,6 +87,9 @@ function buildFoirHeaders(input) {
|
|
|
66
87
|
if (input.bearerToken) {
|
|
67
88
|
headers["Authorization"] = `Bearer ${input.bearerToken}`;
|
|
68
89
|
}
|
|
90
|
+
if (input.orgId) {
|
|
91
|
+
headers["X-Org-Id"] = input.orgId;
|
|
92
|
+
}
|
|
69
93
|
if (input.extra) {
|
|
70
94
|
for (const [key, value] of Object.entries(input.extra)) {
|
|
71
95
|
headers[key] = value;
|
|
@@ -80,14 +104,45 @@ function foirApiEndpoint(apiUrl, path) {
|
|
|
80
104
|
}
|
|
81
105
|
return base + path;
|
|
82
106
|
}
|
|
83
|
-
|
|
107
|
+
var warnedDowngrade = null;
|
|
108
|
+
function reportAuthDowngrade(envelope) {
|
|
109
|
+
const downgrade = envelope.extensions?.auth;
|
|
110
|
+
if (!downgrade) {
|
|
111
|
+
warnedDowngrade = null;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (warnedDowngrade === downgrade.reason) return;
|
|
115
|
+
warnedDowngrade = downgrade.reason;
|
|
116
|
+
console.warn(
|
|
117
|
+
`[foir] your customer token was ${downgrade.customerToken} (${downgrade.reason}) \u2014 this request was served as ${downgrade.servedAs}, so auth-targeted content resolved to its anonymous variant. ` + (downgrade.reason === "expired" ? "Re-exchange the token and retry." : "Check which credential you are sending.")
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
function refusePrunedDocument(json, options) {
|
|
121
|
+
const selections = json.extensions?.pruned?.selections;
|
|
122
|
+
if (!selections?.length) return;
|
|
123
|
+
const describe2 = (s) => `${s.path}${s.type ? ` on ${s.type}` : ""} (${s.reason})`;
|
|
124
|
+
const malformed = selections.filter((s) => MALFORMED_PRUNE_REASONS.has(s.reason));
|
|
125
|
+
if (malformed.length && options?.strict !== false) {
|
|
126
|
+
throw new FoirRequestError(
|
|
127
|
+
`[foir] the server could not serve part of this query and returned the rest without it: ${malformed.map(describe2).join(", ")}. Those selections do not exist on the schema this project is serving. Pass strict: false to get the partial answer instead of this error.`,
|
|
128
|
+
{ prunedSelections: selections }
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
console.warn(
|
|
132
|
+
`[foir] selections stripped from this query: ${selections.map(describe2).join(", ")}`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
function readFoirEnvelope(json, options) {
|
|
136
|
+
reportAuthDowngrade(json);
|
|
137
|
+
refusePrunedDocument(json, options);
|
|
84
138
|
if (json.errors?.length) {
|
|
85
139
|
const msg = json.errors.map((e) => e.message).join("; ");
|
|
86
|
-
if (json.data) {
|
|
140
|
+
if (json.data && !options?.strict) {
|
|
87
141
|
console.warn(`[foir] GraphQL partial errors (data returned): ${msg}`);
|
|
88
142
|
} else {
|
|
89
143
|
throw new FoirRequestError(`[foir] GraphQL errors: ${msg}`, {
|
|
90
|
-
graphQLErrors: json.errors
|
|
144
|
+
graphQLErrors: json.errors,
|
|
145
|
+
partialData: json.data ?? void 0
|
|
91
146
|
});
|
|
92
147
|
}
|
|
93
148
|
}
|
|
@@ -128,6 +183,14 @@ async function hasFoirErrorCode(res, ...codes) {
|
|
|
128
183
|
}
|
|
129
184
|
|
|
130
185
|
// src/data/transport.ts
|
|
186
|
+
var FoirSchemaDriftError = class extends FoirRequestError {
|
|
187
|
+
drift;
|
|
188
|
+
constructor(message, drift) {
|
|
189
|
+
super(message);
|
|
190
|
+
this.name = "FoirSchemaDriftError";
|
|
191
|
+
this.drift = drift;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
131
194
|
function foirDataEndpoint(apiUrl, path) {
|
|
132
195
|
return foirApiEndpoint(apiUrl, path);
|
|
133
196
|
}
|
|
@@ -139,7 +202,9 @@ async function resolveBearerToken(config) {
|
|
|
139
202
|
function foirRoute(input) {
|
|
140
203
|
if (!input.apiKey.startsWith("pk_")) return { lane: "origin", reason: "key" };
|
|
141
204
|
if (input.bearerToken) return { lane: "origin", reason: "bearer" };
|
|
205
|
+
if (input.orgId) return { lane: "origin", reason: "org" };
|
|
142
206
|
if (input.mutation) return { lane: "origin", reason: "operation" };
|
|
207
|
+
if (input.writeToken) return { lane: "origin", reason: "floor" };
|
|
143
208
|
if (input.channel === "draft") return { lane: "origin", reason: "channel" };
|
|
144
209
|
return { lane: "shared" };
|
|
145
210
|
}
|
|
@@ -152,10 +217,10 @@ async function executeFoirDocument(config, plan) {
|
|
|
152
217
|
}
|
|
153
218
|
return data[plan.rootField];
|
|
154
219
|
}
|
|
155
|
-
async function executeFoirRawDocument(config, document, mutation, variables) {
|
|
156
|
-
return await runFoirDocument(config, document, mutation, variables);
|
|
220
|
+
async function executeFoirRawDocument(config, document, mutation, variables, options) {
|
|
221
|
+
return await runFoirDocument(config, document, mutation, variables, options);
|
|
157
222
|
}
|
|
158
|
-
async function runFoirDocument(config, document, mutation, variables) {
|
|
223
|
+
async function runFoirDocument(config, document, mutation, variables, options) {
|
|
159
224
|
const doFetch = config.fetch ?? globalThis.fetch;
|
|
160
225
|
const endpoint = foirDataEndpoint(config.apiUrl, "/graphql");
|
|
161
226
|
const bearerToken = await resolveBearerToken(config);
|
|
@@ -163,12 +228,14 @@ async function runFoirDocument(config, document, mutation, variables) {
|
|
|
163
228
|
apiKey: config.apiKey,
|
|
164
229
|
draftChannel: config.channel === "draft",
|
|
165
230
|
bearerToken,
|
|
231
|
+
orgId: config.orgId,
|
|
166
232
|
extra: config.headers
|
|
167
233
|
});
|
|
168
234
|
const hash = await documentHash(document);
|
|
169
235
|
const route = foirRoute({
|
|
170
236
|
apiKey: config.apiKey,
|
|
171
237
|
bearerToken,
|
|
238
|
+
orgId: config.orgId,
|
|
172
239
|
mutation,
|
|
173
240
|
channel: config.channel
|
|
174
241
|
});
|
|
@@ -208,7 +275,7 @@ async function runFoirDocument(config, document, mutation, variables) {
|
|
|
208
275
|
});
|
|
209
276
|
}
|
|
210
277
|
const json = await res.json();
|
|
211
|
-
return readFoirEnvelope(json);
|
|
278
|
+
return readFoirEnvelope(json, options);
|
|
212
279
|
}
|
|
213
280
|
async function readErrorBody(res) {
|
|
214
281
|
try {
|
|
@@ -231,6 +298,7 @@ async function fetchFoirManifest(config) {
|
|
|
231
298
|
apiKey: config.apiKey,
|
|
232
299
|
draftChannel: config.channel === "draft",
|
|
233
300
|
bearerToken: await resolveBearerToken(config),
|
|
301
|
+
orgId: config.orgId,
|
|
234
302
|
extra: config.headers
|
|
235
303
|
}),
|
|
236
304
|
...config.requestInit
|
|
@@ -869,6 +937,7 @@ var FoirSocketTransport = class {
|
|
|
869
937
|
apiKey: this.config.apiKey,
|
|
870
938
|
draftChannel: this.config.channel === "draft",
|
|
871
939
|
bearerToken: await resolveBearerToken(this.config),
|
|
940
|
+
orgId: this.config.orgId,
|
|
872
941
|
extra: this.config.headers
|
|
873
942
|
});
|
|
874
943
|
const { "Content-Type": _contentType, ...params } = headers;
|
|
@@ -977,7 +1046,9 @@ var FoirSocketTransport = class {
|
|
|
977
1046
|
return;
|
|
978
1047
|
}
|
|
979
1048
|
case GQL.error: {
|
|
980
|
-
const error = new FoirRequestError(`[foir] subscription refused: ${describe(message.payload)}
|
|
1049
|
+
const error = new FoirRequestError(`[foir] subscription refused: ${describe(message.payload)}`, {
|
|
1050
|
+
graphQLErrors: graphQLErrorItems(message.payload)
|
|
1051
|
+
});
|
|
981
1052
|
if (!message.id) {
|
|
982
1053
|
this.failAll(error);
|
|
983
1054
|
this.teardownSocket(true);
|
|
@@ -1088,6 +1159,21 @@ function defaultWebSocketFactory() {
|
|
|
1088
1159
|
}
|
|
1089
1160
|
return (url, protocols) => new Impl(url, protocols);
|
|
1090
1161
|
}
|
|
1162
|
+
function graphQLErrorItems(payload) {
|
|
1163
|
+
const list = Array.isArray(payload) ? payload : payload == null ? [] : [payload];
|
|
1164
|
+
const items = [];
|
|
1165
|
+
for (const entry of list) {
|
|
1166
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
1167
|
+
const { message, path, extensions } = entry;
|
|
1168
|
+
if (extensions === void 0 && path === void 0) continue;
|
|
1169
|
+
items.push({
|
|
1170
|
+
message: typeof message === "string" ? message : describe(entry),
|
|
1171
|
+
...Array.isArray(path) ? { path } : {},
|
|
1172
|
+
...typeof extensions === "object" && extensions !== null ? { extensions } : {}
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
return items.length > 0 ? items : void 0;
|
|
1176
|
+
}
|
|
1091
1177
|
function describe(payload) {
|
|
1092
1178
|
if (payload == null) return "no reason given";
|
|
1093
1179
|
if (typeof payload === "string") return payload;
|
|
@@ -1133,12 +1219,19 @@ var FoirSubscriptionRouter = class {
|
|
|
1133
1219
|
if (method !== "onChange") {
|
|
1134
1220
|
throw new FoirRequestError(`[foir] ${modelKey}.${method} is not a subscription`);
|
|
1135
1221
|
}
|
|
1222
|
+
const after = input.after;
|
|
1223
|
+
const args = [{ name: "modelKey", type: "String!" }];
|
|
1224
|
+
const variables = { modelKey };
|
|
1225
|
+
if (typeof after === "number") {
|
|
1226
|
+
args.push({ name: "after", type: "EventSeq" });
|
|
1227
|
+
variables.after = after;
|
|
1228
|
+
}
|
|
1136
1229
|
return this.open(
|
|
1137
1230
|
{
|
|
1138
1231
|
field: "recordChanged",
|
|
1139
1232
|
operationName: "FoirRecordChanged",
|
|
1140
|
-
args
|
|
1141
|
-
variables
|
|
1233
|
+
args,
|
|
1234
|
+
variables
|
|
1142
1235
|
},
|
|
1143
1236
|
input
|
|
1144
1237
|
);
|
|
@@ -1177,7 +1270,7 @@ var FoirSubscriptionRouter = class {
|
|
|
1177
1270
|
args: [{ name: "id", type: "String!" }],
|
|
1178
1271
|
variables: { id }
|
|
1179
1272
|
},
|
|
1180
|
-
handlers
|
|
1273
|
+
{ ...handlers, onData: (event) => handlers.onData(executionEventWithEnumStatus(event)) }
|
|
1181
1274
|
);
|
|
1182
1275
|
}
|
|
1183
1276
|
throw new FoirRequestError(`[foir] ns.${namespace}.${method} is not a subscription`);
|
|
@@ -1222,6 +1315,12 @@ var FoirSubscriptionRouter = class {
|
|
|
1222
1315
|
};
|
|
1223
1316
|
}
|
|
1224
1317
|
};
|
|
1318
|
+
function executionEventWithEnumStatus(event) {
|
|
1319
|
+
if (event === null || typeof event !== "object") return event;
|
|
1320
|
+
const status = event.status;
|
|
1321
|
+
if (typeof status !== "string") return event;
|
|
1322
|
+
return { ...event, status: status.toUpperCase() };
|
|
1323
|
+
}
|
|
1225
1324
|
function findSubscription(manifest, field) {
|
|
1226
1325
|
const found = manifest.subscriptions?.find((s) => s.field === field);
|
|
1227
1326
|
if (found) return found;
|
|
@@ -1369,6 +1468,9 @@ function recordsAt(value, path) {
|
|
|
1369
1468
|
}
|
|
1370
1469
|
|
|
1371
1470
|
// src/data/client.ts
|
|
1471
|
+
function graphql(document) {
|
|
1472
|
+
return document;
|
|
1473
|
+
}
|
|
1372
1474
|
var NON_MODEL_PROPS = /* @__PURE__ */ new Set([
|
|
1373
1475
|
"then",
|
|
1374
1476
|
"catch",
|
|
@@ -1500,19 +1602,26 @@ function resolveGeneratedConfig(config) {
|
|
|
1500
1602
|
`[foir] createFoirClient: \`generated.foirSelectionUnions["${name}"]\` has no \`members\`. A tree that names this union would print an empty selection set. Re-run \`foir types\`.`
|
|
1501
1603
|
);
|
|
1502
1604
|
}
|
|
1605
|
+
const subscriptions = generated["foirSubscriptions"];
|
|
1606
|
+
if (subscriptions !== void 0 && !Array.isArray(subscriptions)) {
|
|
1607
|
+
throw new FoirRequestError(
|
|
1608
|
+
"[foir] createFoirClient: `generated.foirSubscriptions` is not an array of subscription descriptors. Re-run `foir types`."
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1503
1611
|
return {
|
|
1504
1612
|
...config,
|
|
1505
1613
|
schemaVersion: config.schemaVersion ?? version,
|
|
1506
1614
|
selections: config.selections ?? selections,
|
|
1507
1615
|
namespaceSelections: config.namespaceSelections ?? namespaceSelections,
|
|
1508
|
-
selectionUnions: config.selectionUnions ?? selectionUnions
|
|
1616
|
+
selectionUnions: config.selectionUnions ?? selectionUnions,
|
|
1617
|
+
subscriptions: config.subscriptions ?? subscriptions
|
|
1509
1618
|
};
|
|
1510
1619
|
}
|
|
1511
1620
|
function announceFallback(config) {
|
|
1512
1621
|
if (config.generated !== void 0 || config.selections !== void 0) return;
|
|
1513
1622
|
if (config.announceManifestFallback === false) return;
|
|
1514
1623
|
console.info(
|
|
1515
|
-
"[foir] no generated module passed, so this client fetches `GET /v1/manifest` once at first use and prints from the trees it publishes. That works \u2014 it is what makes a zero-codegen start possible \u2014 but it puts a round trip in front of the first query on every cold start. Run `foir types`, then `import * as generated from './foir.generated'` and pass `generated`. Silence this with `announceManifestFallback: false
|
|
1624
|
+
"[foir] no generated module passed, so this client fetches `GET /v1/manifest` once at first use and prints from the trees it publishes. That works \u2014 it is what makes a zero-codegen start possible \u2014 but it puts a round trip in front of the first query on every cold start. Run `foir types`, then `import * as generated from './foir.generated'` and pass `generated`. Silence this with `announceManifestFallback: false`.\n[foir] A subscribe-only client can now take that advice too: `foir types` emits `foirSubscriptions` alongside the model trees, so a generated client prints its subscribe document from the module and makes no request at all. Weigh the module against one fetch behind a socket that lives for the whole session \u2014 on a phone the fallback may still be the better trade."
|
|
1516
1625
|
);
|
|
1517
1626
|
}
|
|
1518
1627
|
function createFoirClient(clientConfig) {
|
|
@@ -1522,6 +1631,7 @@ function createFoirClient(clientConfig) {
|
|
|
1522
1631
|
const config = resolveGeneratedConfig(clientConfig);
|
|
1523
1632
|
announceFallback(config);
|
|
1524
1633
|
let driftChecked = false;
|
|
1634
|
+
let detectedDrift;
|
|
1525
1635
|
let manifestOnce;
|
|
1526
1636
|
const manifest = () => {
|
|
1527
1637
|
if (!manifestOnce) {
|
|
@@ -1542,6 +1652,16 @@ function createFoirClient(clientConfig) {
|
|
|
1542
1652
|
}
|
|
1543
1653
|
return manifest();
|
|
1544
1654
|
};
|
|
1655
|
+
const sameSurface = (a, b) => {
|
|
1656
|
+
const stable = (v) => {
|
|
1657
|
+
if (Array.isArray(v)) return v.map(stable);
|
|
1658
|
+
if (v && typeof v === "object") {
|
|
1659
|
+
return Object.keys(v).sort().map((k) => [k, stable(v[k])]);
|
|
1660
|
+
}
|
|
1661
|
+
return v;
|
|
1662
|
+
};
|
|
1663
|
+
return JSON.stringify(stable(a)) === JSON.stringify(stable(b));
|
|
1664
|
+
};
|
|
1545
1665
|
const maybeCheckDrift = () => {
|
|
1546
1666
|
const generated = config.schemaVersion;
|
|
1547
1667
|
if (driftChecked) return;
|
|
@@ -1550,11 +1670,15 @@ function createFoirClient(clientConfig) {
|
|
|
1550
1670
|
driftChecked = true;
|
|
1551
1671
|
void manifest().then((served) => {
|
|
1552
1672
|
if (served.schemaVersion === generated) return;
|
|
1673
|
+
if (sameSurface(config.selections, served.models) && sameSurface(config.namespaceSelections ?? {}, served.namespaces ?? {}) && sameSurface(config.selectionUnions ?? {}, served.selectionUnions ?? {})) {
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1553
1676
|
const drift = {
|
|
1554
1677
|
generated,
|
|
1555
1678
|
server: served.schemaVersion,
|
|
1556
1679
|
channel: served.channel
|
|
1557
1680
|
};
|
|
1681
|
+
detectedDrift = drift;
|
|
1558
1682
|
if (config.onSchemaDrift) {
|
|
1559
1683
|
config.onSchemaDrift(drift);
|
|
1560
1684
|
return;
|
|
@@ -1565,7 +1689,15 @@ function createFoirClient(clientConfig) {
|
|
|
1565
1689
|
}).catch(() => {
|
|
1566
1690
|
});
|
|
1567
1691
|
};
|
|
1692
|
+
const assertNoStaleSelections = (what) => {
|
|
1693
|
+
if (config.strictSchemaDrift === false || !detectedDrift) return;
|
|
1694
|
+
throw new FoirSchemaDriftError(
|
|
1695
|
+
`[foir] refusing ${what}: types were generated from schemaVersion ${detectedDrift.generated}, the server is serving ${detectedDrift.server} (${detectedDrift.channel}). The selection trees this client prints from are the ones that version published, so a field added since is not requested at all. Re-run \`foir types\`. Set \`strictSchemaDrift: false\` to warn instead.`,
|
|
1696
|
+
detectedDrift
|
|
1697
|
+
);
|
|
1698
|
+
};
|
|
1568
1699
|
const runModel = async (model, op, query) => {
|
|
1700
|
+
assertNoStaleSelections(`${model}.${op}`);
|
|
1569
1701
|
const { args, include, omit, pick, depth } = splitQuery(query);
|
|
1570
1702
|
const plan = planFoirModelCall(await selectionSource(), {
|
|
1571
1703
|
model,
|
|
@@ -1582,6 +1714,7 @@ function createFoirClient(clientConfig) {
|
|
|
1582
1714
|
return result;
|
|
1583
1715
|
};
|
|
1584
1716
|
const runNamespace = async (namespace, op, query) => {
|
|
1717
|
+
assertNoStaleSelections(`ns.${namespace}.${op}`);
|
|
1585
1718
|
const { args, include, omit, pick, depth } = splitQuery(query);
|
|
1586
1719
|
const plan = planFoirNamespaceCall(await selectionSource(), {
|
|
1587
1720
|
namespace,
|
|
@@ -1597,11 +1730,18 @@ function createFoirClient(clientConfig) {
|
|
|
1597
1730
|
maybeCheckDrift();
|
|
1598
1731
|
return result;
|
|
1599
1732
|
};
|
|
1733
|
+
const subscriptionManifest = () => {
|
|
1734
|
+
if (!config.subscriptions) return manifest();
|
|
1735
|
+
return Promise.resolve({
|
|
1736
|
+
subscriptions: config.subscriptions,
|
|
1737
|
+
selectionUnions: config.selectionUnions
|
|
1738
|
+
});
|
|
1739
|
+
};
|
|
1600
1740
|
const subscriptions = /* @__PURE__ */ (() => {
|
|
1601
1741
|
const held = { router: null };
|
|
1602
1742
|
return {
|
|
1603
1743
|
get() {
|
|
1604
|
-
held.router ??= new FoirSubscriptionRouter(config,
|
|
1744
|
+
held.router ??= new FoirSubscriptionRouter(config, subscriptionManifest);
|
|
1605
1745
|
return held.router;
|
|
1606
1746
|
},
|
|
1607
1747
|
dispose() {
|
|
@@ -1629,7 +1769,7 @@ function createFoirClient(clientConfig) {
|
|
|
1629
1769
|
return opProxy({ namespace });
|
|
1630
1770
|
}
|
|
1631
1771
|
});
|
|
1632
|
-
const raw = async (document, variables) => {
|
|
1772
|
+
const raw = async (document, variables, options) => {
|
|
1633
1773
|
if (typeof document !== "string" || !document.trim()) {
|
|
1634
1774
|
throw new FoirRequestError("[foir] raw() takes a GraphQL document string");
|
|
1635
1775
|
}
|
|
@@ -1637,7 +1777,8 @@ function createFoirClient(clientConfig) {
|
|
|
1637
1777
|
config,
|
|
1638
1778
|
document,
|
|
1639
1779
|
isFoirMutationDocument(document),
|
|
1640
|
-
variables
|
|
1780
|
+
variables,
|
|
1781
|
+
options
|
|
1641
1782
|
);
|
|
1642
1783
|
maybeCheckDrift();
|
|
1643
1784
|
return result;
|
|
@@ -1666,16 +1807,236 @@ function createFoirClientFromEnv(env, overrides) {
|
|
|
1666
1807
|
channel: isFoirPreview(env) ? "draft" : "published"
|
|
1667
1808
|
});
|
|
1668
1809
|
}
|
|
1810
|
+
|
|
1811
|
+
// src/data/decimal.ts
|
|
1812
|
+
var DECIMAL_PATTERN = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
1813
|
+
var MAX_SCALE = 1e3;
|
|
1814
|
+
function pow10(n) {
|
|
1815
|
+
return 10n ** BigInt(n);
|
|
1816
|
+
}
|
|
1817
|
+
function divRound(numerator, denominator, rounding) {
|
|
1818
|
+
if (denominator === 0n) throw new RangeError("FoirDecimal: division by zero");
|
|
1819
|
+
const negative = numerator < 0n !== denominator < 0n;
|
|
1820
|
+
const n = numerator < 0n ? -numerator : numerator;
|
|
1821
|
+
const d = denominator < 0n ? -denominator : denominator;
|
|
1822
|
+
let quotient = n / d;
|
|
1823
|
+
const remainder = n % d;
|
|
1824
|
+
if (remainder !== 0n && rounding !== "down") {
|
|
1825
|
+
const doubled = remainder * 2n;
|
|
1826
|
+
if (doubled > d) {
|
|
1827
|
+
quotient += 1n;
|
|
1828
|
+
} else if (doubled === d) {
|
|
1829
|
+
if (rounding === "half-up" || quotient % 2n === 1n) quotient += 1n;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
return negative ? -quotient : quotient;
|
|
1833
|
+
}
|
|
1834
|
+
function parse(input) {
|
|
1835
|
+
const text = input.trim();
|
|
1836
|
+
if (!DECIMAL_PATTERN.test(text)) {
|
|
1837
|
+
throw new TypeError(
|
|
1838
|
+
`FoirDecimal: ${JSON.stringify(input)} is not a decimal. Expected a numeric string such as "19.99".`
|
|
1839
|
+
);
|
|
1840
|
+
}
|
|
1841
|
+
const [mantissa, exponentText] = text.split(/[eE]/);
|
|
1842
|
+
const exponent = exponentText ? Number(exponentText) : 0;
|
|
1843
|
+
const negative = mantissa.startsWith("-");
|
|
1844
|
+
const unsigned = negative || mantissa.startsWith("+") ? mantissa.slice(1) : mantissa;
|
|
1845
|
+
const [integerPart = "", fractionPart = ""] = unsigned.split(".");
|
|
1846
|
+
let units = BigInt(`${integerPart || "0"}${fractionPart}`);
|
|
1847
|
+
let scale = fractionPart.length - exponent;
|
|
1848
|
+
if (scale < 0) {
|
|
1849
|
+
units *= pow10(-scale);
|
|
1850
|
+
scale = 0;
|
|
1851
|
+
}
|
|
1852
|
+
if (scale > MAX_SCALE) {
|
|
1853
|
+
throw new RangeError(`FoirDecimal: scale ${scale} exceeds the maximum of ${MAX_SCALE}`);
|
|
1854
|
+
}
|
|
1855
|
+
return { units: negative ? -units : units, scale };
|
|
1856
|
+
}
|
|
1857
|
+
var FoirDecimal = class _FoirDecimal {
|
|
1858
|
+
// Not `readonly` only so `#of` can populate a fresh instance without going
|
|
1859
|
+
// back through the string parser. Never mutated after construction, and
|
|
1860
|
+
// `#`-private so immutability is not part of the public contract to keep.
|
|
1861
|
+
#units;
|
|
1862
|
+
#scale;
|
|
1863
|
+
constructor(value) {
|
|
1864
|
+
if (value instanceof _FoirDecimal) {
|
|
1865
|
+
this.#units = value.#units;
|
|
1866
|
+
this.#scale = value.#scale;
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
if (typeof value === "bigint") {
|
|
1870
|
+
this.#units = value;
|
|
1871
|
+
this.#scale = 0;
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
if (typeof value !== "string") {
|
|
1875
|
+
throw new TypeError(
|
|
1876
|
+
`FoirDecimal: refusing to construct from ${typeof value}. A JavaScript number is a float and may already have lost precision \u2014 pass the string, or use FoirDecimal.fromNumber() to accept that loss explicitly.`
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
const { units, scale } = parse(value);
|
|
1880
|
+
this.#units = units;
|
|
1881
|
+
this.#scale = scale;
|
|
1882
|
+
}
|
|
1883
|
+
/**
|
|
1884
|
+
* Build a decimal from a JavaScript number, accepting the precision that
|
|
1885
|
+
* number may already have lost.
|
|
1886
|
+
*
|
|
1887
|
+
* Deliberately not the constructor. It is for interop — a value read from a
|
|
1888
|
+
* third-party JSON API that emitted a float — and naming it at every call
|
|
1889
|
+
* site is the point. `fromNumber(0.1 + 0.2)` gives `0.30000000000000004`,
|
|
1890
|
+
* because that is genuinely the value it was handed.
|
|
1891
|
+
*/
|
|
1892
|
+
static fromNumber(value) {
|
|
1893
|
+
if (!Number.isFinite(value)) {
|
|
1894
|
+
throw new RangeError(`FoirDecimal: ${value} is not a finite number`);
|
|
1895
|
+
}
|
|
1896
|
+
return new _FoirDecimal(value.toPrecision(17));
|
|
1897
|
+
}
|
|
1898
|
+
/** `true` if `value` parses as a decimal. Does not throw. */
|
|
1899
|
+
static isDecimalString(value) {
|
|
1900
|
+
return typeof value === "string" && DECIMAL_PATTERN.test(value.trim());
|
|
1901
|
+
}
|
|
1902
|
+
/** Decimal places carried by this value. `19.90` has scale 2. */
|
|
1903
|
+
get scale() {
|
|
1904
|
+
return this.#scale;
|
|
1905
|
+
}
|
|
1906
|
+
/** Re-express at `scale`, rounding away any digits that no longer fit. */
|
|
1907
|
+
rescale(scale, rounding = "half-up") {
|
|
1908
|
+
if (!Number.isInteger(scale) || scale < 0 || scale > MAX_SCALE) {
|
|
1909
|
+
throw new RangeError(`FoirDecimal: scale must be an integer in 0..${MAX_SCALE}, got ${scale}`);
|
|
1910
|
+
}
|
|
1911
|
+
if (scale === this.#scale) return this;
|
|
1912
|
+
const units = scale > this.#scale ? this.#units * pow10(scale - this.#scale) : divRound(this.#units, pow10(this.#scale - scale), rounding);
|
|
1913
|
+
return _FoirDecimal.#of(units, scale);
|
|
1914
|
+
}
|
|
1915
|
+
add(other) {
|
|
1916
|
+
const [a, b, scale] = _FoirDecimal.#align(this, other);
|
|
1917
|
+
return _FoirDecimal.#of(a + b, scale);
|
|
1918
|
+
}
|
|
1919
|
+
sub(other) {
|
|
1920
|
+
const [a, b, scale] = _FoirDecimal.#align(this, other);
|
|
1921
|
+
return _FoirDecimal.#of(a - b, scale);
|
|
1922
|
+
}
|
|
1923
|
+
/** Exact: scales add, so `1.10 × 1.10` is `1.2100` rather than `1.21`. */
|
|
1924
|
+
mul(other) {
|
|
1925
|
+
const rhs = new _FoirDecimal(other);
|
|
1926
|
+
return _FoirDecimal.#of(this.#units * rhs.#units, this.#scale + rhs.#scale);
|
|
1927
|
+
}
|
|
1928
|
+
/** The only inexact operation — see {@link FoirDecimalDivOptions}. */
|
|
1929
|
+
div(other, options = {}) {
|
|
1930
|
+
const { scale = 20, rounding = "half-even" } = options;
|
|
1931
|
+
if (!Number.isInteger(scale) || scale < 0 || scale > MAX_SCALE) {
|
|
1932
|
+
throw new RangeError(`FoirDecimal: scale must be an integer in 0..${MAX_SCALE}, got ${scale}`);
|
|
1933
|
+
}
|
|
1934
|
+
const rhs = new _FoirDecimal(other);
|
|
1935
|
+
const numerator = this.#units * pow10(rhs.#scale + scale);
|
|
1936
|
+
const denominator = rhs.#units * pow10(this.#scale);
|
|
1937
|
+
return _FoirDecimal.#of(divRound(numerator, denominator, rounding), scale);
|
|
1938
|
+
}
|
|
1939
|
+
abs() {
|
|
1940
|
+
return this.#units < 0n ? _FoirDecimal.#of(-this.#units, this.#scale) : this;
|
|
1941
|
+
}
|
|
1942
|
+
neg() {
|
|
1943
|
+
return _FoirDecimal.#of(-this.#units, this.#scale);
|
|
1944
|
+
}
|
|
1945
|
+
isZero() {
|
|
1946
|
+
return this.#units === 0n;
|
|
1947
|
+
}
|
|
1948
|
+
isNegative() {
|
|
1949
|
+
return this.#units < 0n;
|
|
1950
|
+
}
|
|
1951
|
+
/** `-1`, `0` or `1`. Use this rather than `<`, which throws. */
|
|
1952
|
+
cmp(other) {
|
|
1953
|
+
const [a, b] = _FoirDecimal.#align(this, other);
|
|
1954
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
1955
|
+
}
|
|
1956
|
+
/** Numeric equality: `1.0` equals `1.00`. Scale is display, not identity. */
|
|
1957
|
+
equals(other) {
|
|
1958
|
+
return this.cmp(other) === 0;
|
|
1959
|
+
}
|
|
1960
|
+
/**
|
|
1961
|
+
* Fixed-place string, rounding `half-up` by default because this is the
|
|
1962
|
+
* display operation and that is what a person means by "to 2dp".
|
|
1963
|
+
*/
|
|
1964
|
+
toFixed(places, rounding = "half-up") {
|
|
1965
|
+
return this.rescale(places, rounding).toString();
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* The canonical wire form, preserving scale. Never exponent notation, so the
|
|
1969
|
+
* output always parses back to an equal value.
|
|
1970
|
+
*/
|
|
1971
|
+
toString() {
|
|
1972
|
+
const negative = this.#units < 0n;
|
|
1973
|
+
const digits = (negative ? -this.#units : this.#units).toString();
|
|
1974
|
+
if (this.#scale === 0) return negative ? `-${digits}` : digits;
|
|
1975
|
+
const padded = digits.padStart(this.#scale + 1, "0");
|
|
1976
|
+
const integerPart = padded.slice(0, padded.length - this.#scale);
|
|
1977
|
+
const fractionPart = padded.slice(padded.length - this.#scale);
|
|
1978
|
+
return `${negative ? "-" : ""}${integerPart}.${fractionPart}`;
|
|
1979
|
+
}
|
|
1980
|
+
/** `JSON.stringify` emits the canonical string, not an object. Pinned by test. */
|
|
1981
|
+
toJSON() {
|
|
1982
|
+
return this.toString();
|
|
1983
|
+
}
|
|
1984
|
+
/**
|
|
1985
|
+
* Explicitly lossy — this is the boundary back into float, and naming it is
|
|
1986
|
+
* the point. Fine for a chart axis; never for money you will add up.
|
|
1987
|
+
*/
|
|
1988
|
+
toNumber() {
|
|
1989
|
+
return Number(this.toString());
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Answers the `'string'` hint so template literals work, and throws on every
|
|
1993
|
+
* other hint so `+`, `<` and `Number()` fail loudly instead of silently
|
|
1994
|
+
* concatenating or truncating.
|
|
1995
|
+
*/
|
|
1996
|
+
[Symbol.toPrimitive](hint) {
|
|
1997
|
+
if (hint === "string") return this.toString();
|
|
1998
|
+
throw new TypeError(
|
|
1999
|
+
hint === "number" ? "FoirDecimal: cannot be coerced to a number implicitly. Use .toNumber() to accept the precision loss, or .cmp() to compare." : "FoirDecimal: cannot be coerced implicitly. Use .add() / .sub() / .mul() / .div() for arithmetic, .cmp() to compare, or .toString() for text."
|
|
2000
|
+
);
|
|
2001
|
+
}
|
|
2002
|
+
get [Symbol.toStringTag]() {
|
|
2003
|
+
return "FoirDecimal";
|
|
2004
|
+
}
|
|
2005
|
+
/** Skip parsing when the units are already known to be well-formed. */
|
|
2006
|
+
static #of(units, scale) {
|
|
2007
|
+
const instance = new _FoirDecimal(0n);
|
|
2008
|
+
instance.#units = units;
|
|
2009
|
+
instance.#scale = scale;
|
|
2010
|
+
return instance;
|
|
2011
|
+
}
|
|
2012
|
+
/** Bring two values to a common scale so their units can be compared. */
|
|
2013
|
+
static #align(left, right) {
|
|
2014
|
+
const rhs = new _FoirDecimal(right);
|
|
2015
|
+
const scale = Math.max(left.#scale, rhs.#scale);
|
|
2016
|
+
return [
|
|
2017
|
+
left.#units * pow10(scale - left.#scale),
|
|
2018
|
+
rhs.#units * pow10(scale - rhs.#scale),
|
|
2019
|
+
scale
|
|
2020
|
+
];
|
|
2021
|
+
}
|
|
2022
|
+
};
|
|
2023
|
+
function decimal(value) {
|
|
2024
|
+
return new FoirDecimal(value);
|
|
2025
|
+
}
|
|
1669
2026
|
export {
|
|
2027
|
+
FoirDecimal,
|
|
1670
2028
|
FoirRequestError,
|
|
2029
|
+
FoirSchemaDriftError,
|
|
1671
2030
|
FoirSocketTransport,
|
|
1672
2031
|
createFoirClient,
|
|
1673
2032
|
createFoirClientFromEnv,
|
|
1674
2033
|
createFoirSubjectRegistry,
|
|
2034
|
+
decimal,
|
|
1675
2035
|
fetchFoirManifest,
|
|
1676
2036
|
foirDataEndpoint,
|
|
1677
2037
|
foirRoute,
|
|
1678
2038
|
foirSocketEndpoint,
|
|
2039
|
+
graphql,
|
|
1679
2040
|
planFoirModelCall,
|
|
1680
2041
|
planFoirNamespaceCall,
|
|
1681
2042
|
printSubscriptionDocument,
|
|
@@ -21,6 +21,6 @@ export { buildCdnUrl, documentHash, hasFoirErrorCode, NOT_SHAREABLE, PERSISTED_Q
|
|
|
21
21
|
* `/graphql` when it does — but every condition checked here is one round trip
|
|
22
22
|
* saved.
|
|
23
23
|
*/
|
|
24
|
-
export declare function isCdnEligible(env: FoirEnv, documentText: string, bearerToken?: string): boolean;
|
|
24
|
+
export declare function isCdnEligible(env: FoirEnv, documentText: string, bearerToken?: string, orgId?: string): boolean;
|
|
25
25
|
/** Print a document once, so eligibility and the URL agree on the same text. */
|
|
26
26
|
export declare function printDocument<TData, TVariables>(document: TypedDocumentNode<TData, TVariables>): string;
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
26
26
|
import { type FoirEnv, type FoirRequestOptions } from '../transport/foir-http';
|
|
27
|
-
export { FoirRequestError, isFoirPreview, isFoirRateLimited, isFoirUnauthenticated, isFoirForbidden, isFoirNotFound, readFoirRateLimit, type FoirEnv, type FoirErrorCode, type FoirErrorExtensions, type FoirGraphQLErrorItem, type FoirRateLimitSnapshot, type FoirRequestOptions, } from '../transport/foir-http';
|
|
27
|
+
export { FoirRequestError, isFoirPreview, isFoirRateLimited, isFoirUnauthenticated, isFoirForbidden, isFoirNotFound, readFoirRateLimit, type FoirEnv, type FoirErrorCode, type FoirErrorExtensions, type FoirGraphQLErrorItem, type FoirRateLimitSnapshot, type FoirRequestOptions, type FoirAuthDowngrade, } from '../transport/foir-http';
|
|
28
28
|
export { withFoirRetry, foirRetryDelayMs, type FoirRetryPolicy, } from '../transport/retry';
|
|
29
29
|
/** Default Foir CDN origin used to absolutise relative media URLs. */
|
|
30
30
|
export declare const FOIR_CDN_BASE = "https://cdn.foir.dev/";
|