@fourtwelvelabs/fetch-contentful 1.0.0 → 1.2.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 +32 -0
- package/README.md +37 -1
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +144 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -1
- package/dist/index.d.ts +54 -1
- package/dist/index.mjs +145 -13
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -130,6 +130,48 @@ var FetchContentfulError = class extends Error {
|
|
|
130
130
|
function isFetchContentfulError(value) {
|
|
131
131
|
return value instanceof FetchContentfulError;
|
|
132
132
|
}
|
|
133
|
+
var WORD_LIKE = /* @__PURE__ */ new Set([graphql.TokenKind.NAME, graphql.TokenKind.INT, graphql.TokenKind.FLOAT]);
|
|
134
|
+
function minifyQuery(query) {
|
|
135
|
+
const lexer = new graphql.Lexer(new graphql.Source(query));
|
|
136
|
+
let previousKind;
|
|
137
|
+
let out = "";
|
|
138
|
+
for (let token = lexer.advance(); token.kind !== graphql.TokenKind.EOF; token = lexer.advance()) {
|
|
139
|
+
if (previousKind && WORD_LIKE.has(previousKind) && WORD_LIKE.has(token.kind)) {
|
|
140
|
+
out += " ";
|
|
141
|
+
}
|
|
142
|
+
out += query.slice(token.start, token.end);
|
|
143
|
+
previousKind = token.kind;
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/persisted-query.ts
|
|
149
|
+
var PERSISTED_QUERY_VERSION = 1;
|
|
150
|
+
function toHex(bytes) {
|
|
151
|
+
let hex = "";
|
|
152
|
+
for (const byte of bytes) {
|
|
153
|
+
hex += byte.toString(16).padStart(2, "0");
|
|
154
|
+
}
|
|
155
|
+
return hex;
|
|
156
|
+
}
|
|
157
|
+
async function sha256Hex(query) {
|
|
158
|
+
const data = new TextEncoder().encode(query);
|
|
159
|
+
const subtle = globalThis.crypto?.subtle;
|
|
160
|
+
if (subtle) {
|
|
161
|
+
const digest = await subtle.digest("SHA-256", data);
|
|
162
|
+
return toHex(new Uint8Array(digest));
|
|
163
|
+
}
|
|
164
|
+
const { createHash } = await import('crypto');
|
|
165
|
+
return createHash("sha256").update(data).digest("hex");
|
|
166
|
+
}
|
|
167
|
+
function persistedQueryExtensions(sha256Hash) {
|
|
168
|
+
return { persistedQuery: { version: PERSISTED_QUERY_VERSION, sha256Hash } };
|
|
169
|
+
}
|
|
170
|
+
function isPersistedQueryNotFoundError(error) {
|
|
171
|
+
if (error.message === "PersistedQueryNotFound") return true;
|
|
172
|
+
const extensions = error.extensions;
|
|
173
|
+
return extensions?.code === "PERSISTED_QUERY_NOT_FOUND";
|
|
174
|
+
}
|
|
133
175
|
|
|
134
176
|
// src/retry.ts
|
|
135
177
|
function defaultSleep(ms) {
|
|
@@ -196,6 +238,9 @@ function withDirective(field, name) {
|
|
|
196
238
|
function responseKeyOf(field) {
|
|
197
239
|
return field.alias?.value ?? field.name.value;
|
|
198
240
|
}
|
|
241
|
+
function hasResponseKey(selection, key) {
|
|
242
|
+
return selection.kind === graphql.Kind.FIELD && responseKeyOf(selection) === key;
|
|
243
|
+
}
|
|
199
244
|
function isCollectionField(field) {
|
|
200
245
|
if (!field.name.value.endsWith(COLLECTION_SUFFIX) || field.name.value === COLLECTION_SUFFIX || !field.selectionSet) {
|
|
201
246
|
return false;
|
|
@@ -326,13 +371,14 @@ function planSplits(document, options) {
|
|
|
326
371
|
const field = selection;
|
|
327
372
|
const isExplicitSplit = hasDirective(field, SPLIT_DIRECTIVE);
|
|
328
373
|
const isAutoSplit = options.autoSplitNestedCollections && isCollectionField(field);
|
|
374
|
+
const isForcedSplit = options.forceSplit?.(field) ?? false;
|
|
329
375
|
if (isExplicitSplit && depth === 0) {
|
|
330
376
|
throw new FetchContentfulError(
|
|
331
377
|
`Cannot split root field "${responseKeyOf(field)}": @split needs a parent entry to stitch the result back onto, and root fields have none. Move the directive to a nested field, or page through this field with its own \`limit\` and \`skip\` arguments.`,
|
|
332
378
|
{ code: "CONFIG" }
|
|
333
379
|
);
|
|
334
380
|
}
|
|
335
|
-
const shouldSplit = depth > 0 && field.selectionSet !== void 0 && !hasDirective(field, NO_SPLIT_DIRECTIVE) && (isExplicitSplit || isAutoSplit);
|
|
381
|
+
const shouldSplit = depth > 0 && field.selectionSet !== void 0 && !hasDirective(field, NO_SPLIT_DIRECTIVE) && (isExplicitSplit || isAutoSplit || isForcedSplit);
|
|
336
382
|
if (shouldSplit) {
|
|
337
383
|
const planned = withDirective(withoutDirective(field, SPLIT_DIRECTIVE), NO_SPLIT_DIRECTIVE);
|
|
338
384
|
plans.push({
|
|
@@ -363,8 +409,11 @@ function planSplits(document, options) {
|
|
|
363
409
|
selections.push(field);
|
|
364
410
|
}
|
|
365
411
|
}
|
|
366
|
-
if (needsMarkers) {
|
|
367
|
-
selections.push(sysIdMarker()
|
|
412
|
+
if (needsMarkers && !selections.some((s) => hasResponseKey(s, SYS_ID_ALIAS))) {
|
|
413
|
+
selections.push(sysIdMarker());
|
|
414
|
+
}
|
|
415
|
+
if (needsMarkers && !selections.some((s) => hasResponseKey(s, TYPENAME_ALIAS))) {
|
|
416
|
+
selections.push(typenameMarker());
|
|
368
417
|
}
|
|
369
418
|
return { ...selectionSet, selections };
|
|
370
419
|
}
|
|
@@ -382,6 +431,49 @@ function planSplits(document, options) {
|
|
|
382
431
|
plans
|
|
383
432
|
};
|
|
384
433
|
}
|
|
434
|
+
function findSplitCandidates(document) {
|
|
435
|
+
const operation = getOperation(document);
|
|
436
|
+
const candidates = [];
|
|
437
|
+
function walk2(selectionSet, depth) {
|
|
438
|
+
for (const selection of selectionSet.selections) {
|
|
439
|
+
if (selection.kind === graphql.Kind.INLINE_FRAGMENT) {
|
|
440
|
+
walk2(selection.selectionSet, depth);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (selection.kind !== graphql.Kind.FIELD || !selection.selectionSet) continue;
|
|
444
|
+
const isMarker = hasResponseKey(selection, SYS_ID_ALIAS) || hasResponseKey(selection, TYPENAME_ALIAS);
|
|
445
|
+
if (depth > 0 && selection.name.value !== "items" && !isMarker) {
|
|
446
|
+
candidates.push({ field: selection, size: graphql.print(selection).length });
|
|
447
|
+
}
|
|
448
|
+
walk2(selection.selectionSet, depth + 1);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
walk2(operation.selectionSet, 0);
|
|
452
|
+
return candidates;
|
|
453
|
+
}
|
|
454
|
+
function planSplitsWithSizeBudget(document, options) {
|
|
455
|
+
const first = planSplits(document, {
|
|
456
|
+
autoSplitNestedCollections: options.autoSplitNestedCollections
|
|
457
|
+
});
|
|
458
|
+
if (!options.autoSplitOnSize) return first;
|
|
459
|
+
let outer = first.document;
|
|
460
|
+
const plans = [...first.plans];
|
|
461
|
+
let remainingIterations = findSplitCandidates(outer).length;
|
|
462
|
+
while (options.measure(outer) > options.maxQuerySize && remainingIterations-- > 0) {
|
|
463
|
+
const candidates = findSplitCandidates(outer);
|
|
464
|
+
if (candidates.length === 0) break;
|
|
465
|
+
const target = candidates.reduce(
|
|
466
|
+
(largest, candidate) => candidate.size > largest.size ? candidate : largest
|
|
467
|
+
);
|
|
468
|
+
const next = planSplits(outer, {
|
|
469
|
+
autoSplitNestedCollections: false,
|
|
470
|
+
forceSplit: (field) => field === target.field
|
|
471
|
+
});
|
|
472
|
+
outer = next.document;
|
|
473
|
+
plans.push(...next.plans);
|
|
474
|
+
}
|
|
475
|
+
return { document: outer, plans };
|
|
476
|
+
}
|
|
385
477
|
function stringArgument(name, value) {
|
|
386
478
|
return {
|
|
387
479
|
kind: graphql.Kind.ARGUMENT,
|
|
@@ -550,6 +642,10 @@ function graphqlEndpoint(space, environment) {
|
|
|
550
642
|
space
|
|
551
643
|
)}/environments/${encodeURIComponent(environment)}`;
|
|
552
644
|
}
|
|
645
|
+
function printOutgoingQuery(document, context) {
|
|
646
|
+
const printed = graphql.print(stripInternalDirectives(document));
|
|
647
|
+
return context.minifyQuery === false ? printed : minifyQuery(printed);
|
|
648
|
+
}
|
|
553
649
|
function parseRetryAfter(header) {
|
|
554
650
|
if (!header) return void 0;
|
|
555
651
|
const seconds = Number(header);
|
|
@@ -609,13 +705,17 @@ ${annotated}
|
|
|
609
705
|
` : message;
|
|
610
706
|
}
|
|
611
707
|
async function rawRequest(document, variables, context) {
|
|
612
|
-
const query =
|
|
613
|
-
const
|
|
614
|
-
query,
|
|
615
|
-
variables: pickDeclaredVariables(document, variables)
|
|
616
|
-
});
|
|
708
|
+
const query = printOutgoingQuery(document, context);
|
|
709
|
+
const pickedVariables = pickDeclaredVariables(document, variables);
|
|
617
710
|
const url = graphqlEndpoint(context.space, context.environment);
|
|
618
|
-
|
|
711
|
+
const usePersistedQuery = context.automaticPersistedQueries === true;
|
|
712
|
+
const sha256Hash = usePersistedQuery ? await sha256Hex(query) : void 0;
|
|
713
|
+
async function send(includeQuery) {
|
|
714
|
+
const body = JSON.stringify({
|
|
715
|
+
...includeQuery ? { query } : {},
|
|
716
|
+
variables: pickedVariables,
|
|
717
|
+
...sha256Hash ? { extensions: persistedQueryExtensions(sha256Hash) } : {}
|
|
718
|
+
});
|
|
619
719
|
let response;
|
|
620
720
|
try {
|
|
621
721
|
const init = {
|
|
@@ -685,6 +785,21 @@ async function rawRequest(document, variables, context) {
|
|
|
685
785
|
});
|
|
686
786
|
}
|
|
687
787
|
return payload.data;
|
|
788
|
+
}
|
|
789
|
+
let mustIncludeQuery = !usePersistedQuery;
|
|
790
|
+
return withRetries(async () => {
|
|
791
|
+
if (mustIncludeQuery) {
|
|
792
|
+
return send(true);
|
|
793
|
+
}
|
|
794
|
+
try {
|
|
795
|
+
return await send(false);
|
|
796
|
+
} catch (error) {
|
|
797
|
+
if (isFetchContentfulError(error) && (error.code === "GRAPHQL" || error.code === "HTTP") && error.errors?.some(isPersistedQueryNotFoundError)) {
|
|
798
|
+
mustIncludeQuery = true;
|
|
799
|
+
return await send(true);
|
|
800
|
+
}
|
|
801
|
+
throw error;
|
|
802
|
+
}
|
|
688
803
|
}, context.retry);
|
|
689
804
|
}
|
|
690
805
|
|
|
@@ -890,6 +1005,8 @@ var DEFAULT_RETRIES = 5;
|
|
|
890
1005
|
var DEFAULT_RETRY_DELAY_MS = 250;
|
|
891
1006
|
var DEFAULT_MAX_RETRY_DELAY_MS = 8e3;
|
|
892
1007
|
var DEFAULT_SPLIT_BATCH_SIZE = 50;
|
|
1008
|
+
var DEFAULT_MAX_QUERY_SIZE = 7500;
|
|
1009
|
+
var DEFAULT_MAX_QUERY_SIZE_WITH_APQ = 15500;
|
|
893
1010
|
function isRecord4(value) {
|
|
894
1011
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
895
1012
|
}
|
|
@@ -940,6 +1057,7 @@ function resolveContext(options) {
|
|
|
940
1057
|
maxDelayMs: options.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS
|
|
941
1058
|
};
|
|
942
1059
|
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1060
|
+
const automaticPersistedQueries = options.automaticPersistedQueries ?? false;
|
|
943
1061
|
const context = {
|
|
944
1062
|
space,
|
|
945
1063
|
environment,
|
|
@@ -950,9 +1068,15 @@ function resolveContext(options) {
|
|
|
950
1068
|
fetch: fetchImpl,
|
|
951
1069
|
retry,
|
|
952
1070
|
autoSplitNestedCollections: options.autoSplitNestedCollections ?? true,
|
|
1071
|
+
autoSplitOnSize: options.autoSplitOnSize ?? true,
|
|
1072
|
+
// APQ lifts Contentful's size ceiling from 8 KB to 16 KB, so the budget
|
|
1073
|
+
// that triggers a size-driven split follows suit unless overridden.
|
|
1074
|
+
maxQuerySize: options.maxQuerySize ?? (automaticPersistedQueries ? DEFAULT_MAX_QUERY_SIZE_WITH_APQ : DEFAULT_MAX_QUERY_SIZE),
|
|
953
1075
|
splitBatchSize: options.splitBatchSize ?? DEFAULT_SPLIT_BATCH_SIZE,
|
|
954
1076
|
annotateQueryOnError: options.annotateQueryOnError ?? true,
|
|
955
|
-
unresolvableLinks: options.unresolvableLinks ?? "omit"
|
|
1077
|
+
unresolvableLinks: options.unresolvableLinks ?? "omit",
|
|
1078
|
+
minifyQuery: options.minifyQuery ?? true,
|
|
1079
|
+
automaticPersistedQueries
|
|
956
1080
|
};
|
|
957
1081
|
if (options.onUnresolvableLink) {
|
|
958
1082
|
context.onUnresolvableLink = options.onUnresolvableLink;
|
|
@@ -1044,8 +1168,16 @@ function cleanupMarkers(data, plans) {
|
|
|
1044
1168
|
}
|
|
1045
1169
|
}
|
|
1046
1170
|
}
|
|
1171
|
+
function byteLength(text) {
|
|
1172
|
+
return new TextEncoder().encode(text).length;
|
|
1173
|
+
}
|
|
1047
1174
|
async function executeDocument(document, variables, context) {
|
|
1048
|
-
const { document: outer, plans } =
|
|
1175
|
+
const { document: outer, plans } = planSplitsWithSizeBudget(document, {
|
|
1176
|
+
autoSplitNestedCollections: context.autoSplitNestedCollections,
|
|
1177
|
+
autoSplitOnSize: context.autoSplitOnSize,
|
|
1178
|
+
maxQuerySize: context.maxQuerySize,
|
|
1179
|
+
measure: (doc) => byteLength(printOutgoingQuery(doc, context))
|
|
1180
|
+
});
|
|
1049
1181
|
const data = await rawRequest(outer, variables, context);
|
|
1050
1182
|
await Promise.all(plans.map((plan) => resolvePlan(plan, data, variables, context)));
|
|
1051
1183
|
cleanupMarkers(data, plans);
|
|
@@ -1136,6 +1268,7 @@ exports.injectRootArgs = injectRootArgs;
|
|
|
1136
1268
|
exports.inlineFragments = inlineFragments;
|
|
1137
1269
|
exports.isFetchContentfulError = isFetchContentfulError;
|
|
1138
1270
|
exports.isUnresolvableLinkError = isUnresolvableLinkError;
|
|
1271
|
+
exports.minifyQuery = minifyQuery;
|
|
1139
1272
|
exports.omitUnresolvedLinks = omitUnresolvedLinks;
|
|
1140
1273
|
exports.partitionUnresolvableLinks = partitionUnresolvableLinks;
|
|
1141
1274
|
exports.readEnvSettings = readEnvSettings;
|