@foir/sdk 0.10.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 +222 -0
- package/README.md +10 -2
- package/dist/auth/ops.d.ts +8 -0
- package/dist/auth.js +91 -37
- package/dist/client.js +44 -13
- package/dist/codegen/preset.d.ts +3 -2
- package/dist/data/builtins.d.ts +17 -1
- package/dist/data/client.d.ts +10 -8
- package/dist/data/document.d.ts +13 -0
- package/dist/data/index.d.ts +13 -3
- package/dist/data/manifest.d.ts +31 -3
- package/dist/data/schema.d.ts +16 -9
- package/dist/data/selection.d.ts +15 -0
- package/dist/data/transport.d.ts +244 -22
- package/dist/data/ws-transport.d.ts +8 -2
- package/dist/data.js +236 -70
- package/dist/index.js +0 -9
- package/dist/live/drift.d.ts +9 -10
- package/dist/live.js +1 -1
- package/dist/next.js +88 -35
- package/dist/push.js +91 -37
- package/dist/react.js +88 -35
- package/dist/server/index.d.ts +1 -0
- 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 +19 -0
- package/dist/server.js +162 -22
- package/dist/transport/foir-http.d.ts +55 -5
- package/package.json +1 -1
package/dist/data.js
CHANGED
|
@@ -76,19 +76,46 @@ var FoirRequestError = class extends Error {
|
|
|
76
76
|
return typeof seconds === "number" && seconds > 0 ? seconds * 1e3 : void 0;
|
|
77
77
|
}
|
|
78
78
|
};
|
|
79
|
+
var DISPATCH_RESERVED_HEADERS = /* @__PURE__ */ new Set([
|
|
80
|
+
"x-api-key",
|
|
81
|
+
"authorization",
|
|
82
|
+
"x-foir-token",
|
|
83
|
+
"x-foir-context"
|
|
84
|
+
]);
|
|
85
|
+
function refuseCredentialHeaders(extra) {
|
|
86
|
+
if (!extra) return;
|
|
87
|
+
for (const name of Object.keys(extra)) {
|
|
88
|
+
if (DISPATCH_RESERVED_HEADERS.has(name.toLowerCase())) {
|
|
89
|
+
throw new FoirRequestError(
|
|
90
|
+
`[foir] a client built from a dispatch token cannot send "${name}" in \`headers\`. The dispatch token is the credential, and an api key or a bearer beside it would make the request two actors at once. Build a separate client for the other credential.`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
79
95
|
function buildFoirHeaders(input) {
|
|
80
96
|
const headers = {
|
|
81
|
-
"Content-Type": "application/json"
|
|
82
|
-
"x-api-key": input.apiKey
|
|
97
|
+
"Content-Type": "application/json"
|
|
83
98
|
};
|
|
84
|
-
if (input.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
headers["
|
|
99
|
+
if (input.dispatchToken !== void 0) {
|
|
100
|
+
refuseCredentialHeaders(input.extra);
|
|
101
|
+
headers["X-Foir-Token"] = input.dispatchToken;
|
|
102
|
+
if (input.dispatchContext) {
|
|
103
|
+
headers["X-Foir-Context"] = input.dispatchContext;
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
headers["x-api-key"] = input.apiKey;
|
|
107
|
+
if (input.draftChannel) {
|
|
108
|
+
headers["X-Foir-Schema-Channel"] = "draft";
|
|
109
|
+
}
|
|
110
|
+
if (input.bearerToken) {
|
|
111
|
+
headers["Authorization"] = `Bearer ${input.bearerToken}`;
|
|
112
|
+
}
|
|
113
|
+
if (input.orgId) {
|
|
114
|
+
headers["X-Org-Id"] = input.orgId;
|
|
115
|
+
}
|
|
116
|
+
if (input.orgUrl) {
|
|
117
|
+
headers["X-Org-Url"] = input.orgUrl;
|
|
118
|
+
}
|
|
92
119
|
}
|
|
93
120
|
if (input.extra) {
|
|
94
121
|
for (const [key, value] of Object.entries(input.extra)) {
|
|
@@ -132,12 +159,15 @@ function refusePrunedDocument(json, options) {
|
|
|
132
159
|
`[foir] selections stripped from this query: ${selections.map(describe2).join(", ")}`
|
|
133
160
|
);
|
|
134
161
|
}
|
|
162
|
+
function answeredAnything(data) {
|
|
163
|
+
return typeof data === "object" && data !== null && Object.values(data).some((value) => value !== null && value !== void 0);
|
|
164
|
+
}
|
|
135
165
|
function readFoirEnvelope(json, options) {
|
|
136
166
|
reportAuthDowngrade(json);
|
|
137
167
|
refusePrunedDocument(json, options);
|
|
138
168
|
if (json.errors?.length) {
|
|
139
169
|
const msg = json.errors.map((e) => e.message).join("; ");
|
|
140
|
-
if (json.data && !options?.strict) {
|
|
170
|
+
if (answeredAnything(json.data) && !options?.strict) {
|
|
141
171
|
console.warn(`[foir] GraphQL partial errors (data returned): ${msg}`);
|
|
142
172
|
} else {
|
|
143
173
|
throw new FoirRequestError(`[foir] GraphQL errors: ${msg}`, {
|
|
@@ -199,10 +229,107 @@ async function resolveBearerToken(config) {
|
|
|
199
229
|
if (typeof source === "function") return await source() ?? void 0;
|
|
200
230
|
return source ?? void 0;
|
|
201
231
|
}
|
|
232
|
+
async function resolveDispatchToken(dispatch) {
|
|
233
|
+
const token = (await dispatch.token())?.trim();
|
|
234
|
+
if (!token) {
|
|
235
|
+
throw new FoirRequestError(
|
|
236
|
+
"[foir] the dispatch token provider returned no token. A client built from a dispatch calls Foir as that operation and holds no other credential. Build it from the token the endpoint was handed: `(await requireScopedToken(request)).dispatch`."
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return token;
|
|
240
|
+
}
|
|
241
|
+
async function resolveFoirAuth(config) {
|
|
242
|
+
if (config.dispatch !== void 0) {
|
|
243
|
+
return {
|
|
244
|
+
headers: buildFoirHeaders({
|
|
245
|
+
dispatchToken: await resolveDispatchToken(config.dispatch),
|
|
246
|
+
dispatchContext: config.dispatch.context ?? void 0,
|
|
247
|
+
extra: config.headers
|
|
248
|
+
})
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const bearerToken = await resolveBearerToken(config);
|
|
252
|
+
return {
|
|
253
|
+
headers: buildFoirHeaders({
|
|
254
|
+
apiKey: config.apiKey,
|
|
255
|
+
draftChannel: config.channel === "draft",
|
|
256
|
+
bearerToken,
|
|
257
|
+
orgId: config.orgId,
|
|
258
|
+
orgUrl: config.orgUrl,
|
|
259
|
+
extra: config.headers
|
|
260
|
+
}),
|
|
261
|
+
key: { apiKey: config.apiKey, bearerToken }
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function assertFoirCredential(config) {
|
|
265
|
+
const loose = config;
|
|
266
|
+
const operation = generatedOperation(config.generated);
|
|
267
|
+
if (loose.dispatch === void 0) {
|
|
268
|
+
if (typeof loose.apiKey !== "string" || loose.apiKey === "") {
|
|
269
|
+
throw new FoirRequestError(
|
|
270
|
+
"[foir] createFoirClient needs both apiUrl and apiKey (or, inside an operation endpoint, apiUrl and dispatch)"
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (operation !== void 0) {
|
|
274
|
+
throw new FoirRequestError(
|
|
275
|
+
`[foir] createFoirClient: the module passed as \`generated\` was generated for operation "${operation}" (\`foir types --operation ${operation}\`), so its types are what that operation's dispatch token is served, and this client holds an api key, which is served its own surface. Generate the key's types with \`foir types\`, or build this client from the dispatch token inside the operation's endpoint.`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const keySide = [
|
|
281
|
+
"apiKey",
|
|
282
|
+
"bearerToken",
|
|
283
|
+
"orgId",
|
|
284
|
+
"orgUrl",
|
|
285
|
+
"channel",
|
|
286
|
+
"wsUrl",
|
|
287
|
+
"webSocketImpl",
|
|
288
|
+
"reconnect",
|
|
289
|
+
"connectionParams"
|
|
290
|
+
].filter((name) => loose[name] !== void 0);
|
|
291
|
+
if (keySide.length > 0) {
|
|
292
|
+
throw new FoirRequestError(
|
|
293
|
+
`[foir] createFoirClient was given \`dispatch\` and ${keySide.map((n) => `\`${n}\``).join(", ")}. A dispatch token and an api key are different actors, and the dispatch token carries its own customer, organization and channel. Pass one credential: \`dispatch\` inside an operation endpoint, \`apiKey\` everywhere else.`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
const dispatch = loose.dispatch;
|
|
297
|
+
if (!dispatch || typeof dispatch.token !== "function") {
|
|
298
|
+
throw new FoirRequestError(
|
|
299
|
+
"[foir] `dispatch.token` must be a function that returns the token, e.g. `() => token`, so a refreshed token is read on each request rather than one captured at construction. `requireScopedToken` returns one ready to pass."
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
refuseCredentialHeaders(config.headers);
|
|
303
|
+
if (operation === void 0) {
|
|
304
|
+
throw new FoirRequestError(
|
|
305
|
+
"[foir] createFoirClient: a client built from a dispatch token takes the module `foir types --operation <operation key>` generated for that operation, as `generated`. Its types are what the operation's token is served; a module generated for a key types calls the token is refused, and without one nothing says which operation this is."
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
const dispatched = contextOperation(dispatch.context);
|
|
309
|
+
if (dispatched !== void 0 && dispatched !== operation) {
|
|
310
|
+
throw new FoirRequestError(
|
|
311
|
+
`[foir] createFoirClient: the module passed as \`generated\` was generated for operation "${operation}", and this endpoint was dispatched for "${dispatched}". Generate this endpoint's types with \`foir types --operation ${dispatched}\`.`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function generatedOperation(generated) {
|
|
316
|
+
if (typeof generated !== "object" || generated === null) return void 0;
|
|
317
|
+
const credential = generated.foirCredential;
|
|
318
|
+
if (typeof credential !== "object" || credential === null) return void 0;
|
|
319
|
+
const operation = credential.operation;
|
|
320
|
+
return typeof operation === "string" && operation !== "" ? operation : void 0;
|
|
321
|
+
}
|
|
322
|
+
function contextOperation(context) {
|
|
323
|
+
for (const part of (context ?? "").split(";")) {
|
|
324
|
+
const [key, value] = part.split("=", 2);
|
|
325
|
+
if (key?.trim() === "operation" && value) return value.trim();
|
|
326
|
+
}
|
|
327
|
+
return void 0;
|
|
328
|
+
}
|
|
202
329
|
function foirRoute(input) {
|
|
203
330
|
if (!input.apiKey.startsWith("pk_")) return { lane: "origin", reason: "key" };
|
|
204
331
|
if (input.bearerToken) return { lane: "origin", reason: "bearer" };
|
|
205
|
-
if (input.orgId) return { lane: "origin", reason: "org" };
|
|
332
|
+
if (input.orgId || input.orgUrl) return { lane: "origin", reason: "org" };
|
|
206
333
|
if (input.mutation) return { lane: "origin", reason: "operation" };
|
|
207
334
|
if (input.writeToken) return { lane: "origin", reason: "floor" };
|
|
208
335
|
if (input.channel === "draft") return { lane: "origin", reason: "channel" };
|
|
@@ -223,22 +350,16 @@ async function executeFoirRawDocument(config, document, mutation, variables, opt
|
|
|
223
350
|
async function runFoirDocument(config, document, mutation, variables, options) {
|
|
224
351
|
const doFetch = config.fetch ?? globalThis.fetch;
|
|
225
352
|
const endpoint = foirDataEndpoint(config.apiUrl, "/graphql");
|
|
226
|
-
const
|
|
227
|
-
const headers = buildFoirHeaders({
|
|
228
|
-
apiKey: config.apiKey,
|
|
229
|
-
draftChannel: config.channel === "draft",
|
|
230
|
-
bearerToken,
|
|
231
|
-
orgId: config.orgId,
|
|
232
|
-
extra: config.headers
|
|
233
|
-
});
|
|
353
|
+
const { headers, key } = await resolveFoirAuth(config);
|
|
234
354
|
const hash = await documentHash(document);
|
|
235
|
-
const
|
|
236
|
-
apiKey:
|
|
237
|
-
bearerToken,
|
|
355
|
+
const sharedKey = key !== void 0 && foirRoute({
|
|
356
|
+
apiKey: key.apiKey,
|
|
357
|
+
bearerToken: key.bearerToken,
|
|
238
358
|
orgId: config.orgId,
|
|
359
|
+
orgUrl: config.orgUrl,
|
|
239
360
|
mutation,
|
|
240
361
|
channel: config.channel
|
|
241
|
-
});
|
|
362
|
+
}).lane === "shared" ? key.apiKey : void 0;
|
|
242
363
|
const post = (body) => doFetch(endpoint, {
|
|
243
364
|
method: "POST",
|
|
244
365
|
headers,
|
|
@@ -248,8 +369,8 @@ async function runFoirDocument(config, document, mutation, variables, options) {
|
|
|
248
369
|
const extensions = persistedQueryExtension(hash);
|
|
249
370
|
let res;
|
|
250
371
|
try {
|
|
251
|
-
if (
|
|
252
|
-
res = await doFetch(buildCdnUrl(endpoint,
|
|
372
|
+
if (sharedKey !== void 0) {
|
|
373
|
+
res = await doFetch(buildCdnUrl(endpoint, sharedKey, hash, variables), {
|
|
253
374
|
method: "GET",
|
|
254
375
|
headers,
|
|
255
376
|
...config.requestInit
|
|
@@ -287,20 +408,16 @@ async function readErrorBody(res) {
|
|
|
287
408
|
}
|
|
288
409
|
|
|
289
410
|
// src/data/manifest.ts
|
|
290
|
-
async function fetchFoirManifest(config) {
|
|
291
|
-
const
|
|
411
|
+
async function fetchFoirManifest(config, options = {}) {
|
|
412
|
+
const base = foirDataEndpoint(config.apiUrl, "/v1/manifest");
|
|
413
|
+
const url = options.operation === void 0 ? base : `${base}?operation=${encodeURIComponent(options.operation)}`;
|
|
292
414
|
const doFetch = config.fetch ?? globalThis.fetch;
|
|
415
|
+
const { headers } = await resolveFoirAuth(config);
|
|
293
416
|
let res;
|
|
294
417
|
try {
|
|
295
418
|
res = await doFetch(url, {
|
|
296
419
|
method: "GET",
|
|
297
|
-
headers
|
|
298
|
-
apiKey: config.apiKey,
|
|
299
|
-
draftChannel: config.channel === "draft",
|
|
300
|
-
bearerToken: await resolveBearerToken(config),
|
|
301
|
-
orgId: config.orgId,
|
|
302
|
-
extra: config.headers
|
|
303
|
-
}),
|
|
420
|
+
headers,
|
|
304
421
|
...config.requestInit
|
|
305
422
|
});
|
|
306
423
|
} catch (err) {
|
|
@@ -705,6 +822,13 @@ function planFoirModelCall(source, call) {
|
|
|
705
822
|
}
|
|
706
823
|
return planCall(source, opDef, `${call.model}.${call.op}`, operationName(model.typeName, call.op), call);
|
|
707
824
|
}
|
|
825
|
+
var CREATING_OPS = /* @__PURE__ */ new Set(["create", "submit"]);
|
|
826
|
+
function withCreateIdempotencyKey(source, model, op, args) {
|
|
827
|
+
if (!CREATING_OPS.has(op) || args.idempotencyKey !== void 0) return args;
|
|
828
|
+
const argTypes = source.models[model]?.ops?.[op]?.argTypes;
|
|
829
|
+
if (!argTypes || !("idempotencyKey" in argTypes)) return args;
|
|
830
|
+
return { ...args, idempotencyKey: globalThis.crypto.randomUUID() };
|
|
831
|
+
}
|
|
708
832
|
function planFoirNamespaceCall(source, call) {
|
|
709
833
|
const namespace = source.namespaces?.[call.namespace];
|
|
710
834
|
if (!namespace) {
|
|
@@ -862,6 +986,11 @@ var FoirSocketTransport = class {
|
|
|
862
986
|
keepAlive = null;
|
|
863
987
|
retryTimer = null;
|
|
864
988
|
constructor(config) {
|
|
989
|
+
if (config.dispatch !== void 0) {
|
|
990
|
+
throw new FoirRequestError(
|
|
991
|
+
"[foir] a client built from a dispatch token cannot subscribe. The subscription socket authenticates an api key, an admin token or a customer bearer, and has no path for a dispatch token. Queries and mutations made as the operation go to /graphql and work as they are."
|
|
992
|
+
);
|
|
993
|
+
}
|
|
865
994
|
this.config = config;
|
|
866
995
|
this.policy = { ...DEFAULT_RECONNECT, ...config.reconnect ?? {} };
|
|
867
996
|
}
|
|
@@ -933,13 +1062,7 @@ var FoirSocketTransport = class {
|
|
|
933
1062
|
async sendConnectionInit(socket) {
|
|
934
1063
|
this.connecting = false;
|
|
935
1064
|
try {
|
|
936
|
-
const headers =
|
|
937
|
-
apiKey: this.config.apiKey,
|
|
938
|
-
draftChannel: this.config.channel === "draft",
|
|
939
|
-
bearerToken: await resolveBearerToken(this.config),
|
|
940
|
-
orgId: this.config.orgId,
|
|
941
|
-
extra: this.config.headers
|
|
942
|
-
});
|
|
1065
|
+
const { headers } = await resolveFoirAuth(this.config);
|
|
943
1066
|
const { "Content-Type": _contentType, ...params } = headers;
|
|
944
1067
|
const extra = await this.config.connectionParams?.() ?? {};
|
|
945
1068
|
this.send(socket, {
|
|
@@ -1546,7 +1669,8 @@ function splitQuery(query) {
|
|
|
1546
1669
|
return { args, include, omit, pick, depth };
|
|
1547
1670
|
}
|
|
1548
1671
|
var GENERATED_EXPORTS = [
|
|
1549
|
-
"
|
|
1672
|
+
"foirSurfaceVersion",
|
|
1673
|
+
"foirSurfaceHashes",
|
|
1550
1674
|
"foirSelections",
|
|
1551
1675
|
"foirNamespaceSelections",
|
|
1552
1676
|
"foirSelectionUnions"
|
|
@@ -1566,10 +1690,16 @@ function resolveGeneratedConfig(config) {
|
|
|
1566
1690
|
`[foir] createFoirClient: the module passed as \`generated\` is missing ${missing.join(", ")}. \`foir types\` emits ${GENERATED_EXPORTS.join(", ")} together, so a module missing one is stale or hand-assembled \u2014 re-run \`foir types\`. Reading the rest and fetching \`GET /v1/manifest\` for the remainder would hide that; omit \`generated\` entirely to ask for the manifest deliberately.`
|
|
1567
1691
|
);
|
|
1568
1692
|
}
|
|
1569
|
-
const version = generated["
|
|
1570
|
-
if (typeof version !== "
|
|
1693
|
+
const version = generated["foirSurfaceVersion"];
|
|
1694
|
+
if (typeof version !== "string" || version === "") {
|
|
1695
|
+
throw new FoirRequestError(
|
|
1696
|
+
"[foir] createFoirClient: `generated.foirSurfaceVersion` is not a surface version. It is what the drift check compares against the server. Re-run `foir types`."
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
const hashes = generated["foirSurfaceHashes"];
|
|
1700
|
+
if (!isRecord(hashes) || Object.values(hashes).some((h) => typeof h !== "string")) {
|
|
1571
1701
|
throw new FoirRequestError(
|
|
1572
|
-
"[foir] createFoirClient: `generated.
|
|
1702
|
+
"[foir] createFoirClient: `generated.foirSurfaceHashes` is not an object of component -> hash. It is how a drift names what moved. Re-run `foir types`."
|
|
1573
1703
|
);
|
|
1574
1704
|
}
|
|
1575
1705
|
const selections = generated["foirSelections"];
|
|
@@ -1610,13 +1740,57 @@ function resolveGeneratedConfig(config) {
|
|
|
1610
1740
|
}
|
|
1611
1741
|
return {
|
|
1612
1742
|
...config,
|
|
1613
|
-
|
|
1743
|
+
surfaceVersion: config.surfaceVersion ?? version,
|
|
1744
|
+
surfaceHashes: config.surfaceHashes ?? hashes,
|
|
1614
1745
|
selections: config.selections ?? selections,
|
|
1615
1746
|
namespaceSelections: config.namespaceSelections ?? namespaceSelections,
|
|
1616
1747
|
selectionUnions: config.selectionUnions ?? selectionUnions,
|
|
1617
1748
|
subscriptions: config.subscriptions ?? subscriptions
|
|
1618
1749
|
};
|
|
1619
1750
|
}
|
|
1751
|
+
function changedSurfaceParts(generated, served) {
|
|
1752
|
+
if (!generated || !served) return [];
|
|
1753
|
+
const out = /* @__PURE__ */ new Set();
|
|
1754
|
+
for (const [key, hash] of Object.entries(served)) {
|
|
1755
|
+
if (generated[key] !== hash) out.add(key);
|
|
1756
|
+
}
|
|
1757
|
+
for (const key of Object.keys(generated)) {
|
|
1758
|
+
if (served[key] === void 0) out.add(key);
|
|
1759
|
+
}
|
|
1760
|
+
return [...out].sort();
|
|
1761
|
+
}
|
|
1762
|
+
var SURFACE_SECTIONS = {
|
|
1763
|
+
models: ["model", "models"],
|
|
1764
|
+
namespaces: ["namespace", "namespaces"],
|
|
1765
|
+
selectionUnions: ["union", "unions"]
|
|
1766
|
+
};
|
|
1767
|
+
var SURFACE_WHOLES = {
|
|
1768
|
+
subscriptions: "the subscriptions",
|
|
1769
|
+
composedFor: "the organization it is composed for"
|
|
1770
|
+
};
|
|
1771
|
+
function describeChanged(changed) {
|
|
1772
|
+
const named = /* @__PURE__ */ new Map();
|
|
1773
|
+
const wholes = [];
|
|
1774
|
+
for (const part of changed) {
|
|
1775
|
+
const dot = part.indexOf(".");
|
|
1776
|
+
const section = dot < 0 ? part : part.slice(0, dot);
|
|
1777
|
+
if (dot < 0 || !SURFACE_SECTIONS[section]) {
|
|
1778
|
+
wholes.push(SURFACE_WHOLES[part] ?? part);
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
named.set(section, [...named.get(section) ?? [], `\`${part.slice(dot + 1)}\``]);
|
|
1782
|
+
}
|
|
1783
|
+
const phrases = [];
|
|
1784
|
+
for (const [section, [one, many]] of Object.entries(SURFACE_SECTIONS)) {
|
|
1785
|
+
const names = named.get(section);
|
|
1786
|
+
if (names?.length) phrases.push(`${names.length === 1 ? one : many} ${names.join(", ")}`);
|
|
1787
|
+
}
|
|
1788
|
+
return [...phrases, ...wholes].join("; ");
|
|
1789
|
+
}
|
|
1790
|
+
function describeDrift(drift) {
|
|
1791
|
+
const moved = describeChanged(drift.changed);
|
|
1792
|
+
return `the types were generated from surface ${drift.generated}, and the server is serving ${drift.server} (${drift.channel}).` + (moved ? ` Changed since: ${moved}.` : "");
|
|
1793
|
+
}
|
|
1620
1794
|
function announceFallback(config) {
|
|
1621
1795
|
if (config.generated !== void 0 || config.selections !== void 0) return;
|
|
1622
1796
|
if (config.announceManifestFallback === false) return;
|
|
@@ -1625,9 +1799,12 @@ function announceFallback(config) {
|
|
|
1625
1799
|
);
|
|
1626
1800
|
}
|
|
1627
1801
|
function createFoirClient(clientConfig) {
|
|
1628
|
-
if (!clientConfig?.apiUrl
|
|
1629
|
-
throw new Error(
|
|
1802
|
+
if (!clientConfig?.apiUrl) {
|
|
1803
|
+
throw new Error(
|
|
1804
|
+
"[foir] createFoirClient needs both apiUrl and apiKey (or, inside an operation endpoint, apiUrl and dispatch)"
|
|
1805
|
+
);
|
|
1630
1806
|
}
|
|
1807
|
+
assertFoirCredential(clientConfig);
|
|
1631
1808
|
const config = resolveGeneratedConfig(clientConfig);
|
|
1632
1809
|
announceFallback(config);
|
|
1633
1810
|
let driftChecked = false;
|
|
@@ -1652,31 +1829,19 @@ function createFoirClient(clientConfig) {
|
|
|
1652
1829
|
}
|
|
1653
1830
|
return manifest();
|
|
1654
1831
|
};
|
|
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
|
-
};
|
|
1665
1832
|
const maybeCheckDrift = () => {
|
|
1666
|
-
const generated = config.
|
|
1833
|
+
const generated = config.surfaceVersion;
|
|
1667
1834
|
if (driftChecked) return;
|
|
1668
1835
|
if (generated === void 0) return;
|
|
1669
1836
|
if (config.checkSchemaDrift === false) return;
|
|
1670
1837
|
driftChecked = true;
|
|
1671
1838
|
void manifest().then((served) => {
|
|
1672
|
-
if (served.
|
|
1673
|
-
if (sameSurface(config.selections, served.models) && sameSurface(config.namespaceSelections ?? {}, served.namespaces ?? {}) && sameSurface(config.selectionUnions ?? {}, served.selectionUnions ?? {})) {
|
|
1674
|
-
return;
|
|
1675
|
-
}
|
|
1839
|
+
if (!served.surfaceVersion || served.surfaceVersion === generated) return;
|
|
1676
1840
|
const drift = {
|
|
1677
1841
|
generated,
|
|
1678
|
-
server: served.
|
|
1679
|
-
channel: served.channel
|
|
1842
|
+
server: served.surfaceVersion,
|
|
1843
|
+
channel: served.channel,
|
|
1844
|
+
changed: changedSurfaceParts(config.surfaceHashes, served.surfaceHashes)
|
|
1680
1845
|
};
|
|
1681
1846
|
detectedDrift = drift;
|
|
1682
1847
|
if (config.onSchemaDrift) {
|
|
@@ -1684,7 +1849,7 @@ function createFoirClient(clientConfig) {
|
|
|
1684
1849
|
return;
|
|
1685
1850
|
}
|
|
1686
1851
|
console.warn(
|
|
1687
|
-
`[foir] schema drift:
|
|
1852
|
+
`[foir] schema drift: ${describeDrift(drift)} Re-run \`foir types\`: the selection trees this client prints from are the ones that surface published, so a new field is not merely untyped, it is not requested.`
|
|
1688
1853
|
);
|
|
1689
1854
|
}).catch(() => {
|
|
1690
1855
|
});
|
|
@@ -1692,17 +1857,18 @@ function createFoirClient(clientConfig) {
|
|
|
1692
1857
|
const assertNoStaleSelections = (what) => {
|
|
1693
1858
|
if (config.strictSchemaDrift === false || !detectedDrift) return;
|
|
1694
1859
|
throw new FoirSchemaDriftError(
|
|
1695
|
-
`[foir] refusing ${what}:
|
|
1860
|
+
`[foir] refusing ${what}: ${describeDrift(detectedDrift)} The selection trees this client prints from are the ones that surface published, so a field added since is not requested at all. Re-run \`foir types\`.`,
|
|
1696
1861
|
detectedDrift
|
|
1697
1862
|
);
|
|
1698
1863
|
};
|
|
1699
1864
|
const runModel = async (model, op, query) => {
|
|
1700
1865
|
assertNoStaleSelections(`${model}.${op}`);
|
|
1701
1866
|
const { args, include, omit, pick, depth } = splitQuery(query);
|
|
1702
|
-
const
|
|
1867
|
+
const source = await selectionSource();
|
|
1868
|
+
const plan = planFoirModelCall(source, {
|
|
1703
1869
|
model,
|
|
1704
1870
|
op,
|
|
1705
|
-
args,
|
|
1871
|
+
args: withCreateIdempotencyKey(source, model, op, args),
|
|
1706
1872
|
include,
|
|
1707
1873
|
omit,
|
|
1708
1874
|
pick,
|
package/dist/index.js
CHANGED
|
@@ -256,15 +256,6 @@ function EditorProvider({
|
|
|
256
256
|
type: "request-operation",
|
|
257
257
|
payload: { operationKey, input, requestId, mode: options?.mode ?? "sync" }
|
|
258
258
|
});
|
|
259
|
-
setTimeout(() => {
|
|
260
|
-
if (pendingOperationsRef.current.has(requestId)) {
|
|
261
|
-
pendingOperationsRef.current.delete(requestId);
|
|
262
|
-
resolve({
|
|
263
|
-
success: false,
|
|
264
|
-
error: { code: "TIMEOUT", message: "Operation timed out" }
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
}, 6e4);
|
|
268
259
|
});
|
|
269
260
|
},
|
|
270
261
|
[postMessage]
|
package/dist/live/drift.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { FoirSchemaDrift } from '../data/transport';
|
|
|
15
15
|
* longer exists — until the tab is reloaded, which on a long-lived admin
|
|
16
16
|
* session can be days.
|
|
17
17
|
*
|
|
18
|
-
* The SDK already detects this: the data client compares the
|
|
18
|
+
* The SDK already detects this: the data client compares the surface version
|
|
19
19
|
* its generated types came from against the one the server is serving, and
|
|
20
20
|
* reports `FoirSchemaDrift`. What was missing is anyone acting on it.
|
|
21
21
|
*
|
|
@@ -36,16 +36,15 @@ export interface FoirDriftHandlerOptions {
|
|
|
36
36
|
/**
|
|
37
37
|
* Whether a reported drift is worth clearing the cache for.
|
|
38
38
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* Any surface other than the one the types were generated from is. The versions
|
|
40
|
+
* are content hashes, so there is no ahead or behind: a deploy that added a
|
|
41
|
+
* field and a rollback that removed one are both a different surface, and in
|
|
42
|
+
* both the cache holds responses resolved against a shape the code no longer
|
|
43
|
+
* reads, which is the torn state this exists to remove.
|
|
42
44
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* torn state this exists to remove. What is NOT actionable is equality — a
|
|
47
|
-
* client reporting no drift at all, which the detector should not produce but
|
|
48
|
-
* which costs nothing to refuse here rather than to clear the world for.
|
|
45
|
+
* What is NOT actionable is equality, or a version missing on either side: a
|
|
46
|
+
* report the detector should not produce, and one that costs nothing to refuse
|
|
47
|
+
* here rather than to clear the world for.
|
|
49
48
|
*/
|
|
50
49
|
export declare function foirDriftIsActionable(drift: FoirSchemaDrift): boolean;
|
|
51
50
|
/**
|
package/dist/live.js
CHANGED
|
@@ -800,7 +800,7 @@ function createFoirCoalescer(options) {
|
|
|
800
800
|
|
|
801
801
|
// src/live/drift.ts
|
|
802
802
|
function foirDriftIsActionable(drift) {
|
|
803
|
-
return
|
|
803
|
+
return Boolean(drift.generated) && Boolean(drift.server) && drift.generated !== drift.server;
|
|
804
804
|
}
|
|
805
805
|
function createFoirDriftHandler(options) {
|
|
806
806
|
return (drift) => {
|