@fourtwelvelabs/fetch-contentful 1.0.0 → 1.1.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/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(), typenameMarker());
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 = graphql.print(stripInternalDirectives(document));
613
- const body = JSON.stringify({
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
- return withRetries(async () => {
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.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 } = planSplits(document, context);
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;