@fourtwelvelabs/fetch-contentful 0.4.1 → 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.mjs CHANGED
@@ -1,12 +1,113 @@
1
- import { Kind, parse, print, visit } from 'graphql';
1
+ import { TokenKind, Lexer, Source, Kind, parse, print, visit } from 'graphql';
2
2
 
3
3
  // src/index.ts
4
4
 
5
+ // src/annotate.ts
6
+ var ESC = String.fromCharCode(27);
7
+ var ANSI = {
8
+ reset: `${ESC}[0m`,
9
+ bold: `${ESC}[1m`,
10
+ dim: `${ESC}[2m`,
11
+ red: `${ESC}[31m`
12
+ };
13
+ var DEFAULT_CONTEXT_LINES = 3;
14
+ function supportsColor() {
15
+ if (typeof process === "undefined" || !process.env) {
16
+ return false;
17
+ }
18
+ const forced = process.env.FORCE_COLOR;
19
+ const disabled = process.env.NO_COLOR;
20
+ if (forced) {
21
+ return forced !== "0" && forced !== "false";
22
+ }
23
+ if (disabled) {
24
+ return false;
25
+ }
26
+ return Boolean(process.stderr?.isTTY);
27
+ }
28
+ function makePalette(enabled) {
29
+ const paint = (code) => (text) => enabled ? `${code}${text}${ANSI.reset}` : text;
30
+ return {
31
+ dim: paint(ANSI.dim),
32
+ red: paint(ANSI.red),
33
+ bold: paint(ANSI.bold)
34
+ };
35
+ }
36
+ function collectMarks(errors, lineCount) {
37
+ const marks = [];
38
+ const seen = /* @__PURE__ */ new Set();
39
+ for (const error of errors) {
40
+ for (const location of error.locations ?? []) {
41
+ const line = Math.trunc(location.line);
42
+ if (!(line >= 1 && line <= lineCount)) continue;
43
+ const reported = Math.trunc(location.column);
44
+ const column = reported >= 1 ? reported : 1;
45
+ const key = `${line}:${column}:${error.message}`;
46
+ if (seen.has(key)) continue;
47
+ seen.add(key);
48
+ marks.push({ line, column, message: error.message });
49
+ }
50
+ }
51
+ return marks.sort((a, b) => a.line - b.line || a.column - b.column);
52
+ }
53
+ function excerptsFor(marks, lineCount, contextLines) {
54
+ const excerpts = [];
55
+ for (const mark of marks) {
56
+ const start = Math.max(1, mark.line - contextLines);
57
+ const end = Math.min(lineCount, mark.line + contextLines);
58
+ const previous = excerpts[excerpts.length - 1];
59
+ if (previous && start <= previous.end + 1) {
60
+ previous.end = Math.max(previous.end, end);
61
+ } else {
62
+ excerpts.push({ start, end });
63
+ }
64
+ }
65
+ return excerpts;
66
+ }
67
+ function annotateQuery(query, errors, options = {}) {
68
+ const lines = query.split("\n");
69
+ const marks = collectMarks(errors, lines.length);
70
+ if (marks.length === 0) return "";
71
+ const color = makePalette(options.color ?? supportsColor());
72
+ const excerpts = excerptsFor(marks, lines.length, options.contextLines ?? DEFAULT_CONTEXT_LINES);
73
+ const gutter = String(
74
+ excerpts.reduce((widest, excerpt) => Math.max(widest, excerpt.end), 0)
75
+ ).length;
76
+ const blank = " ".repeat(gutter);
77
+ const out = [];
78
+ for (const [index, excerpt] of excerpts.entries()) {
79
+ if (index > 0) out.push(color.dim(` ${blank} ...`));
80
+ const window = lines.slice(excerpt.start - 1, excerpt.end);
81
+ for (const [offset, source] of window.entries()) {
82
+ const line = excerpt.start + offset;
83
+ const number = String(line).padStart(gutter, " ");
84
+ const here = marks.filter((mark) => mark.line === line);
85
+ if (here.length === 0) {
86
+ out.push(`${color.dim(` ${number} |`)} ${source}`);
87
+ continue;
88
+ }
89
+ out.push(`${color.red(`> ${number} |`)} ${color.bold(source)}`);
90
+ for (const mark of here) {
91
+ const lead = " ".repeat(mark.column - 1);
92
+ out.push(`${color.dim(` ${blank} |`)} ${lead}${color.red(`^ ${mark.message}`)}`);
93
+ }
94
+ }
95
+ }
96
+ return out.join("\n");
97
+ }
98
+
5
99
  // src/errors.ts
6
100
  var FetchContentfulError = class extends Error {
7
101
  code;
8
102
  status;
9
103
  errors;
104
+ /**
105
+ * The GraphQL query exactly as it was sent to Contentful — fragments
106
+ * inlined, arguments injected, and (for a subquery) generated. This is
107
+ * the text the `line`/`column` in {@link FetchContentfulError.errors}
108
+ * point into, so it is the text to annotate; see `annotateQuery`.
109
+ */
110
+ query;
10
111
  /** Internal: whether a retry may succeed. */
11
112
  retryable;
12
113
  /** Internal: server-requested retry delay (from Retry-After), in ms. */
@@ -17,6 +118,7 @@ var FetchContentfulError = class extends Error {
17
118
  this.code = options.code;
18
119
  this.status = options.status;
19
120
  this.errors = options.errors;
121
+ this.query = options.query;
20
122
  this.retryable = options.retryable ?? false;
21
123
  this.retryAfterMs = options.retryAfterMs;
22
124
  }
@@ -24,6 +126,48 @@ var FetchContentfulError = class extends Error {
24
126
  function isFetchContentfulError(value) {
25
127
  return value instanceof FetchContentfulError;
26
128
  }
129
+ var WORD_LIKE = /* @__PURE__ */ new Set([TokenKind.NAME, TokenKind.INT, TokenKind.FLOAT]);
130
+ function minifyQuery(query) {
131
+ const lexer = new Lexer(new Source(query));
132
+ let previousKind;
133
+ let out = "";
134
+ for (let token = lexer.advance(); token.kind !== TokenKind.EOF; token = lexer.advance()) {
135
+ if (previousKind && WORD_LIKE.has(previousKind) && WORD_LIKE.has(token.kind)) {
136
+ out += " ";
137
+ }
138
+ out += query.slice(token.start, token.end);
139
+ previousKind = token.kind;
140
+ }
141
+ return out;
142
+ }
143
+
144
+ // src/persisted-query.ts
145
+ var PERSISTED_QUERY_VERSION = 1;
146
+ function toHex(bytes) {
147
+ let hex = "";
148
+ for (const byte of bytes) {
149
+ hex += byte.toString(16).padStart(2, "0");
150
+ }
151
+ return hex;
152
+ }
153
+ async function sha256Hex(query) {
154
+ const data = new TextEncoder().encode(query);
155
+ const subtle = globalThis.crypto?.subtle;
156
+ if (subtle) {
157
+ const digest = await subtle.digest("SHA-256", data);
158
+ return toHex(new Uint8Array(digest));
159
+ }
160
+ const { createHash } = await import('crypto');
161
+ return createHash("sha256").update(data).digest("hex");
162
+ }
163
+ function persistedQueryExtensions(sha256Hash) {
164
+ return { persistedQuery: { version: PERSISTED_QUERY_VERSION, sha256Hash } };
165
+ }
166
+ function isPersistedQueryNotFoundError(error) {
167
+ if (error.message === "PersistedQueryNotFound") return true;
168
+ const extensions = error.extensions;
169
+ return extensions?.code === "PERSISTED_QUERY_NOT_FOUND";
170
+ }
27
171
 
28
172
  // src/retry.ts
29
173
  function defaultSleep(ms) {
@@ -90,6 +234,9 @@ function withDirective(field, name) {
90
234
  function responseKeyOf(field) {
91
235
  return field.alias?.value ?? field.name.value;
92
236
  }
237
+ function hasResponseKey(selection, key) {
238
+ return selection.kind === Kind.FIELD && responseKeyOf(selection) === key;
239
+ }
93
240
  function isCollectionField(field) {
94
241
  if (!field.name.value.endsWith(COLLECTION_SUFFIX) || field.name.value === COLLECTION_SUFFIX || !field.selectionSet) {
95
242
  return false;
@@ -132,16 +279,14 @@ function getOperation(document) {
132
279
  );
133
280
  const operation = operations[0];
134
281
  if (!operation || operations.length > 1) {
135
- throw new FetchContentfulError(
136
- "fetch-contentful expects exactly one operation per document.",
137
- { code: "CONFIG" }
138
- );
282
+ throw new FetchContentfulError("fetch-contentful expects exactly one operation per document.", {
283
+ code: "CONFIG"
284
+ });
139
285
  }
140
286
  if (operation.operation !== "query") {
141
- throw new FetchContentfulError(
142
- "fetch-contentful only supports query operations.",
143
- { code: "CONFIG" }
144
- );
287
+ throw new FetchContentfulError("fetch-contentful only supports query operations.", {
288
+ code: "CONFIG"
289
+ });
145
290
  }
146
291
  return operation;
147
292
  }
@@ -153,41 +298,34 @@ function inlineFragments(document) {
153
298
  }
154
299
  }
155
300
  function inlineSelectionSet(selectionSet, stack) {
156
- const selections = selectionSet.selections.map(
157
- (selection) => {
158
- if (selection.kind === Kind.FRAGMENT_SPREAD) {
159
- const name = selection.name.value;
160
- const fragment = fragments.get(name);
161
- if (!fragment) {
162
- throw new FetchContentfulError(
163
- `Unknown fragment "${name}" referenced in query.`,
164
- { code: "CONFIG" }
165
- );
166
- }
167
- if (stack.includes(name)) {
168
- throw new FetchContentfulError(
169
- `Fragment cycle detected involving "${name}".`,
170
- { code: "CONFIG" }
171
- );
172
- }
173
- return {
174
- kind: Kind.INLINE_FRAGMENT,
175
- typeCondition: fragment.typeCondition,
176
- selectionSet: inlineSelectionSet(fragment.selectionSet, [
177
- ...stack,
178
- name
179
- ])
180
- };
301
+ const selections = selectionSet.selections.map((selection) => {
302
+ if (selection.kind === Kind.FRAGMENT_SPREAD) {
303
+ const name = selection.name.value;
304
+ const fragment = fragments.get(name);
305
+ if (!fragment) {
306
+ throw new FetchContentfulError(`Unknown fragment "${name}" referenced in query.`, {
307
+ code: "CONFIG"
308
+ });
181
309
  }
182
- if (selection.selectionSet) {
183
- return {
184
- ...selection,
185
- selectionSet: inlineSelectionSet(selection.selectionSet, stack)
186
- };
310
+ if (stack.includes(name)) {
311
+ throw new FetchContentfulError(`Fragment cycle detected involving "${name}".`, {
312
+ code: "CONFIG"
313
+ });
187
314
  }
188
- return selection;
315
+ return {
316
+ kind: Kind.INLINE_FRAGMENT,
317
+ typeCondition: fragment.typeCondition,
318
+ selectionSet: inlineSelectionSet(fragment.selectionSet, [...stack, name])
319
+ };
189
320
  }
190
- );
321
+ if (selection.selectionSet) {
322
+ return {
323
+ ...selection,
324
+ selectionSet: inlineSelectionSet(selection.selectionSet, stack)
325
+ };
326
+ }
327
+ return selection;
328
+ });
191
329
  return { ...selectionSet, selections };
192
330
  }
193
331
  const definitions = document.definitions.filter((definition) => definition.kind !== Kind.FRAGMENT_DEFINITION).map(
@@ -222,26 +360,23 @@ function planSplits(document, options) {
222
360
  continue;
223
361
  }
224
362
  if (selection.kind !== Kind.FIELD) {
225
- throw new FetchContentfulError(
226
- "Fragment spreads must be inlined before split planning.",
227
- { code: "CONFIG" }
228
- );
363
+ throw new FetchContentfulError("Fragment spreads must be inlined before split planning.", {
364
+ code: "CONFIG"
365
+ });
229
366
  }
230
367
  const field = selection;
231
368
  const isExplicitSplit = hasDirective(field, SPLIT_DIRECTIVE);
232
369
  const isAutoSplit = options.autoSplitNestedCollections && isCollectionField(field);
370
+ const isForcedSplit = options.forceSplit?.(field) ?? false;
233
371
  if (isExplicitSplit && depth === 0) {
234
372
  throw new FetchContentfulError(
235
373
  `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.`,
236
374
  { code: "CONFIG" }
237
375
  );
238
376
  }
239
- const shouldSplit = depth > 0 && field.selectionSet !== void 0 && !hasDirective(field, NO_SPLIT_DIRECTIVE) && (isExplicitSplit || isAutoSplit);
377
+ const shouldSplit = depth > 0 && field.selectionSet !== void 0 && !hasDirective(field, NO_SPLIT_DIRECTIVE) && (isExplicitSplit || isAutoSplit || isForcedSplit);
240
378
  if (shouldSplit) {
241
- const planned = withDirective(
242
- withoutDirective(field, SPLIT_DIRECTIVE),
243
- NO_SPLIT_DIRECTIVE
244
- );
379
+ const planned = withDirective(withoutDirective(field, SPLIT_DIRECTIVE), NO_SPLIT_DIRECTIVE);
245
380
  plans.push({
246
381
  path: [...path],
247
382
  responseKey: responseKeyOf(field),
@@ -270,8 +405,11 @@ function planSplits(document, options) {
270
405
  selections.push(field);
271
406
  }
272
407
  }
273
- if (needsMarkers) {
274
- selections.push(sysIdMarker(), typenameMarker());
408
+ if (needsMarkers && !selections.some((s) => hasResponseKey(s, SYS_ID_ALIAS))) {
409
+ selections.push(sysIdMarker());
410
+ }
411
+ if (needsMarkers && !selections.some((s) => hasResponseKey(s, TYPENAME_ALIAS))) {
412
+ selections.push(typenameMarker());
275
413
  }
276
414
  return { ...selectionSet, selections };
277
415
  }
@@ -289,6 +427,49 @@ function planSplits(document, options) {
289
427
  plans
290
428
  };
291
429
  }
430
+ function findSplitCandidates(document) {
431
+ const operation = getOperation(document);
432
+ const candidates = [];
433
+ function walk2(selectionSet, depth) {
434
+ for (const selection of selectionSet.selections) {
435
+ if (selection.kind === Kind.INLINE_FRAGMENT) {
436
+ walk2(selection.selectionSet, depth);
437
+ continue;
438
+ }
439
+ if (selection.kind !== Kind.FIELD || !selection.selectionSet) continue;
440
+ const isMarker = hasResponseKey(selection, SYS_ID_ALIAS) || hasResponseKey(selection, TYPENAME_ALIAS);
441
+ if (depth > 0 && selection.name.value !== "items" && !isMarker) {
442
+ candidates.push({ field: selection, size: print(selection).length });
443
+ }
444
+ walk2(selection.selectionSet, depth + 1);
445
+ }
446
+ }
447
+ walk2(operation.selectionSet, 0);
448
+ return candidates;
449
+ }
450
+ function planSplitsWithSizeBudget(document, options) {
451
+ const first = planSplits(document, {
452
+ autoSplitNestedCollections: options.autoSplitNestedCollections
453
+ });
454
+ if (!options.autoSplitOnSize) return first;
455
+ let outer = first.document;
456
+ const plans = [...first.plans];
457
+ let remainingIterations = findSplitCandidates(outer).length;
458
+ while (options.measure(outer) > options.maxQuerySize && remainingIterations-- > 0) {
459
+ const candidates = findSplitCandidates(outer);
460
+ if (candidates.length === 0) break;
461
+ const target = candidates.reduce(
462
+ (largest, candidate) => candidate.size > largest.size ? candidate : largest
463
+ );
464
+ const next = planSplits(outer, {
465
+ autoSplitNestedCollections: false,
466
+ forceSplit: (field) => field === target.field
467
+ });
468
+ outer = next.document;
469
+ plans.push(...next.plans);
470
+ }
471
+ return { document: outer, plans };
472
+ }
292
473
  function stringArgument(name, value) {
293
474
  return {
294
475
  kind: Kind.ARGUMENT,
@@ -340,10 +521,7 @@ function whereIdInArgument() {
340
521
  }
341
522
  function buildSubquery(plan, typename, batchSize, context) {
342
523
  const rootKey = `${lowerFirst(typename)}${COLLECTION_SUFFIX}`;
343
- const args = [
344
- whereIdInArgument(),
345
- intArgument("limit", batchSize)
346
- ];
524
+ const args = [whereIdInArgument(), intArgument("limit", batchSize)];
347
525
  if (context.preview) {
348
526
  args.push(booleanArgument("preview", true));
349
527
  }
@@ -352,10 +530,7 @@ function buildSubquery(plan, typename, batchSize, context) {
352
530
  }
353
531
  const rootField = {
354
532
  ...namedField(rootKey, void 0, [
355
- namedField("items", void 0, [
356
- namedField("sys", void 0, [namedField("id")]),
357
- plan.field
358
- ])
533
+ namedField("items", void 0, [namedField("sys", void 0, [namedField("id")]), plan.field])
359
534
  ]),
360
535
  arguments: args
361
536
  };
@@ -405,12 +580,68 @@ function stripInternalDirectives(document) {
405
580
  });
406
581
  }
407
582
 
583
+ // src/unresolvable.ts
584
+ var UNRESOLVABLE_LINK_CODE = "UNRESOLVABLE_LINK";
585
+ var COLLECTION_SUFFIX2 = "Collection";
586
+ function isRecord(value) {
587
+ return typeof value === "object" && value !== null && !Array.isArray(value);
588
+ }
589
+ function isCollectionValue(value) {
590
+ return isRecord(value) && Array.isArray(value["items"]);
591
+ }
592
+ function isCollectionKey(key) {
593
+ return key.endsWith(COLLECTION_SUFFIX2) && key !== COLLECTION_SUFFIX2;
594
+ }
595
+ function isUnresolvableLinkError(error) {
596
+ const contentful = error.extensions?.["contentful"];
597
+ return isRecord(contentful) && contentful["code"] === UNRESOLVABLE_LINK_CODE;
598
+ }
599
+ function partitionUnresolvableLinks(errors) {
600
+ const unresolvable = [];
601
+ const fatal = [];
602
+ for (const error of errors) {
603
+ if (isUnresolvableLinkError(error)) {
604
+ unresolvable.push(error);
605
+ } else {
606
+ fatal.push(error);
607
+ }
608
+ }
609
+ return { unresolvable, fatal };
610
+ }
611
+ function walk(value) {
612
+ if (Array.isArray(value)) {
613
+ return value.map(walk);
614
+ }
615
+ if (isRecord(value)) {
616
+ const result = {};
617
+ for (const [key, child] of Object.entries(value)) {
618
+ result[key] = isCollectionKey(key) && isCollectionValue(child) ? stripCollection(child) : walk(child);
619
+ }
620
+ return result;
621
+ }
622
+ return value;
623
+ }
624
+ function stripCollection(collection) {
625
+ const result = {};
626
+ for (const [key, value] of Object.entries(collection)) {
627
+ result[key] = key === "items" ? value.filter((item) => item !== null && item !== void 0).map(walk) : walk(value);
628
+ }
629
+ return result;
630
+ }
631
+ function omitUnresolvedLinks(data) {
632
+ return walk(data);
633
+ }
634
+
408
635
  // src/client.ts
409
636
  function graphqlEndpoint(space, environment) {
410
637
  return `https://graphql.contentful.com/content/v1/spaces/${encodeURIComponent(
411
638
  space
412
639
  )}/environments/${encodeURIComponent(environment)}`;
413
640
  }
641
+ function printOutgoingQuery(document, context) {
642
+ const printed = print(stripInternalDirectives(document));
643
+ return context.minifyQuery === false ? printed : minifyQuery(printed);
644
+ }
414
645
  function parseRetryAfter(header) {
415
646
  if (!header) return void 0;
416
647
  const seconds = Number(header);
@@ -440,14 +671,47 @@ function pickDeclaredVariables(document, variables) {
440
671
  function isRetryableStatus(status) {
441
672
  return status === 408 || status === 429 || status >= 500;
442
673
  }
674
+ function isRecord2(value) {
675
+ return typeof value === "object" && value !== null && !Array.isArray(value);
676
+ }
677
+ function reportUnresolvableLinks(errors, context) {
678
+ if (!context.onUnresolvableLink) return;
679
+ try {
680
+ context.onUnresolvableLink(errors);
681
+ } catch {
682
+ }
683
+ }
684
+ async function readGraphQLErrors(response) {
685
+ try {
686
+ const payload = await response.json();
687
+ const errors = payload?.errors;
688
+ return Array.isArray(errors) && errors.length > 0 ? errors : void 0;
689
+ } catch {
690
+ return void 0;
691
+ }
692
+ }
693
+ function describeFailure(summary, errors, query, context) {
694
+ if (!errors || errors.length === 0) return summary;
695
+ const message = `${summary} ${errors.map((error) => error.message).join("; ")}`;
696
+ if (context.annotateQueryOnError === false) return message;
697
+ const annotated = annotateQuery(query, errors);
698
+ return annotated ? `${message}
699
+
700
+ ${annotated}
701
+ ` : message;
702
+ }
443
703
  async function rawRequest(document, variables, context) {
444
- const query = print(stripInternalDirectives(document));
445
- const body = JSON.stringify({
446
- query,
447
- variables: pickDeclaredVariables(document, variables)
448
- });
704
+ const query = printOutgoingQuery(document, context);
705
+ const pickedVariables = pickDeclaredVariables(document, variables);
449
706
  const url = graphqlEndpoint(context.space, context.environment);
450
- return withRetries(async () => {
707
+ const usePersistedQuery = context.automaticPersistedQueries === true;
708
+ const sha256Hash = usePersistedQuery ? await sha256Hex(query) : void 0;
709
+ async function send(includeQuery) {
710
+ const body = JSON.stringify({
711
+ ...includeQuery ? { query } : {},
712
+ variables: pickedVariables,
713
+ ...sha256Hash ? { extensions: persistedQueryExtensions(sha256Hash) } : {}
714
+ });
451
715
  let response;
452
716
  try {
453
717
  const init = {
@@ -465,16 +729,24 @@ async function rawRequest(document, variables, context) {
465
729
  } catch (cause) {
466
730
  throw new FetchContentfulError(
467
731
  `Network error while contacting Contentful: ${String(cause)}`,
468
- { code: "NETWORK", retryable: true, cause }
732
+ { code: "NETWORK", query, retryable: true, cause }
469
733
  );
470
734
  }
471
735
  if (!response.ok) {
472
736
  const retryable = isRetryableStatus(response.status);
737
+ const errors = retryable ? void 0 : await readGraphQLErrors(response);
473
738
  throw new FetchContentfulError(
474
- `Contentful responded with HTTP ${response.status}.`,
739
+ describeFailure(
740
+ `Contentful responded with HTTP ${response.status}.`,
741
+ errors,
742
+ query,
743
+ context
744
+ ),
475
745
  {
476
746
  code: "HTTP",
477
747
  status: response.status,
748
+ errors,
749
+ query,
478
750
  retryable,
479
751
  retryAfterMs: retryable ? parseRetryAfter(response.headers.get("Retry-After")) : void 0
480
752
  }
@@ -484,24 +756,46 @@ async function rawRequest(document, variables, context) {
484
756
  try {
485
757
  payload = await response.json();
486
758
  } catch (cause) {
487
- throw new FetchContentfulError(
488
- "Contentful returned an unreadable response body.",
489
- { code: "NETWORK", retryable: true, cause }
490
- );
759
+ throw new FetchContentfulError("Contentful returned an unreadable response body.", {
760
+ code: "NETWORK",
761
+ query,
762
+ retryable: true,
763
+ cause
764
+ });
491
765
  }
492
766
  if (payload.errors && payload.errors.length > 0) {
493
- throw new FetchContentfulError(
494
- `Contentful returned GraphQL errors: ${payload.errors.map((error) => error.message).join("; ")}`,
495
- { code: "GRAPHQL", errors: payload.errors }
496
- );
767
+ const { unresolvable, fatal } = partitionUnresolvableLinks(payload.errors);
768
+ const tolerable = context.unresolvableLinks !== "error" && fatal.length === 0 && unresolvable.length > 0 && isRecord2(payload.data);
769
+ if (!tolerable) {
770
+ throw new FetchContentfulError(
771
+ describeFailure("Contentful returned GraphQL errors:", payload.errors, query, context),
772
+ { code: "GRAPHQL", errors: payload.errors, query }
773
+ );
774
+ }
775
+ reportUnresolvableLinks(unresolvable, context);
497
776
  }
498
777
  if (!payload.data || typeof payload.data !== "object") {
499
- throw new FetchContentfulError(
500
- "Contentful returned no data and no errors.",
501
- { code: "GRAPHQL" }
502
- );
778
+ throw new FetchContentfulError("Contentful returned no data and no errors.", {
779
+ code: "GRAPHQL",
780
+ query
781
+ });
503
782
  }
504
783
  return payload.data;
784
+ }
785
+ let mustIncludeQuery = !usePersistedQuery;
786
+ return withRetries(async () => {
787
+ if (mustIncludeQuery) {
788
+ return send(true);
789
+ }
790
+ try {
791
+ return await send(false);
792
+ } catch (error) {
793
+ if (isFetchContentfulError(error) && error.code === "GRAPHQL" && error.errors?.some(isPersistedQueryNotFoundError)) {
794
+ mustIncludeQuery = true;
795
+ return await send(true);
796
+ }
797
+ throw error;
798
+ }
505
799
  }, context.retry);
506
800
  }
507
801
 
@@ -515,7 +809,6 @@ function readEnvSettings() {
515
809
  space: void 0,
516
810
  environment: void 0,
517
811
  deliveryToken: void 0,
518
- token: void 0,
519
812
  previewToken: void 0
520
813
  };
521
814
  }
@@ -530,7 +823,6 @@ function readEnvSettings() {
530
823
  process.env.CONTENTFUL_ENVIRONMENT || process.env.NEXT_PUBLIC_CONTENTFUL_ENVIRONMENT
531
824
  ),
532
825
  deliveryToken,
533
- token: deliveryToken,
534
826
  // No `NEXT_PUBLIC_` fallback: see the note at the top of this file.
535
827
  previewToken: orUndefined(process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN)
536
828
  };
@@ -572,20 +864,18 @@ function injectIntoField(field, args) {
572
864
  return { ...field, arguments: [...argumentsOf(field), ...additions] };
573
865
  }
574
866
  function injectIntoSelectionSet(selectionSet, args) {
575
- const selections = selectionSet.selections.map(
576
- (selection) => {
577
- if (selection.kind === Kind.FIELD) {
578
- return injectIntoField(selection, args);
579
- }
580
- if (selection.kind === Kind.INLINE_FRAGMENT) {
581
- return {
582
- ...selection,
583
- selectionSet: injectIntoSelectionSet(selection.selectionSet, args)
584
- };
585
- }
586
- return selection;
867
+ const selections = selectionSet.selections.map((selection) => {
868
+ if (selection.kind === Kind.FIELD) {
869
+ return injectIntoField(selection, args);
587
870
  }
588
- );
871
+ if (selection.kind === Kind.INLINE_FRAGMENT) {
872
+ return {
873
+ ...selection,
874
+ selectionSet: injectIntoSelectionSet(selection.selectionSet, args)
875
+ };
876
+ }
877
+ return selection;
878
+ });
589
879
  return { ...selectionSet, selections };
590
880
  }
591
881
  function injectRootArgs(document, args) {
@@ -626,27 +916,24 @@ async function fetchLocales(context) {
626
916
  if (context.signal) init.signal = context.signal;
627
917
  response = await context.fetch(localesEndpoint(context), init);
628
918
  } catch (cause) {
629
- throw new FetchContentfulError(
630
- `Network error while fetching locales: ${String(cause)}`,
631
- { code: "NETWORK", retryable: true, cause }
632
- );
919
+ throw new FetchContentfulError(`Network error while fetching locales: ${String(cause)}`, {
920
+ code: "NETWORK",
921
+ retryable: true,
922
+ cause
923
+ });
633
924
  }
634
925
  if (!response.ok) {
635
- throw new FetchContentfulError(
636
- `Locale request failed with HTTP ${response.status}.`,
637
- {
638
- code: "HTTP",
639
- status: response.status,
640
- retryable: response.status === 429 || response.status >= 500
641
- }
642
- );
926
+ throw new FetchContentfulError(`Locale request failed with HTTP ${response.status}.`, {
927
+ code: "HTTP",
928
+ status: response.status,
929
+ retryable: response.status === 429 || response.status >= 500
930
+ });
643
931
  }
644
932
  const payload = await response.json();
645
933
  if (!Array.isArray(payload.items)) {
646
- throw new FetchContentfulError(
647
- "Locale response did not include an items array.",
648
- { code: "NETWORK" }
649
- );
934
+ throw new FetchContentfulError("Locale response did not include an items array.", {
935
+ code: "NETWORK"
936
+ });
650
937
  }
651
938
  return payload.items.map((item) => ({
652
939
  code: item.code,
@@ -674,24 +961,22 @@ function clearLocaleCache() {
674
961
  }
675
962
 
676
963
  // src/shape.ts
677
- var COLLECTION_SUFFIX2 = "Collection";
678
- function isRecord(value) {
964
+ var COLLECTION_SUFFIX3 = "Collection";
965
+ function isRecord3(value) {
679
966
  return typeof value === "object" && value !== null && !Array.isArray(value);
680
967
  }
681
- function isCollectionValue(value) {
682
- return isRecord(value) && Array.isArray(value["items"]);
968
+ function isCollectionValue2(value) {
969
+ return isRecord3(value) && Array.isArray(value["items"]);
683
970
  }
684
971
  function shapeData(data) {
685
972
  if (Array.isArray(data)) {
686
973
  return data.map((item) => shapeData(item));
687
974
  }
688
- if (isRecord(data)) {
975
+ if (isRecord3(data)) {
689
976
  const shaped = {};
690
977
  for (const [key, value] of Object.entries(data)) {
691
- if (key.endsWith(COLLECTION_SUFFIX2) && key !== COLLECTION_SUFFIX2 && isCollectionValue(value)) {
692
- shaped[key.slice(0, -COLLECTION_SUFFIX2.length)] = shapeData(
693
- value.items
694
- );
978
+ if (key.endsWith(COLLECTION_SUFFIX3) && key !== COLLECTION_SUFFIX3 && isCollectionValue2(value)) {
979
+ shaped[key.slice(0, -COLLECTION_SUFFIX3.length)] = shapeData(value.items);
695
980
  } else {
696
981
  shaped[key] = shapeData(value);
697
982
  }
@@ -701,7 +986,7 @@ function shapeData(data) {
701
986
  return data;
702
987
  }
703
988
  function unwrapSingleRoot(data) {
704
- if (isRecord(data)) {
989
+ if (isRecord3(data)) {
705
990
  const keys = Object.keys(data);
706
991
  if (keys.length === 1) {
707
992
  return data[keys[0]];
@@ -716,7 +1001,9 @@ var DEFAULT_RETRIES = 5;
716
1001
  var DEFAULT_RETRY_DELAY_MS = 250;
717
1002
  var DEFAULT_MAX_RETRY_DELAY_MS = 8e3;
718
1003
  var DEFAULT_SPLIT_BATCH_SIZE = 50;
719
- function isRecord2(value) {
1004
+ var DEFAULT_MAX_QUERY_SIZE = 7500;
1005
+ var DEFAULT_MAX_QUERY_SIZE_WITH_APQ = 15500;
1006
+ function isRecord4(value) {
720
1007
  return typeof value === "object" && value !== null && !Array.isArray(value);
721
1008
  }
722
1009
  function collectAtPath(data, path) {
@@ -724,7 +1011,7 @@ function collectAtPath(data, path) {
724
1011
  for (const segment of path) {
725
1012
  const next = [];
726
1013
  for (const value of current) {
727
- if (isRecord2(value)) {
1014
+ if (isRecord4(value)) {
728
1015
  const child = value[segment];
729
1016
  if (Array.isArray(child)) {
730
1017
  next.push(...child);
@@ -735,14 +1022,14 @@ function collectAtPath(data, path) {
735
1022
  }
736
1023
  current = next;
737
1024
  }
738
- return current.filter(isRecord2);
1025
+ return current.filter(isRecord4);
739
1026
  }
740
1027
  function resolveContext(options) {
741
1028
  const env = readEnvSettings();
742
1029
  const space = options.space ?? env.space;
743
1030
  const environment = options.environment ?? env.environment ?? "master";
744
1031
  const preview = options.preview ?? false;
745
- const token = preview ? options.previewToken ?? options.token ?? env.previewToken : options.deliveryToken ?? options.token ?? env.deliveryToken;
1032
+ const token = preview ? options.previewToken ?? env.previewToken : options.deliveryToken ?? env.deliveryToken;
746
1033
  if (!space || !token) {
747
1034
  const missing = [];
748
1035
  if (!space) {
@@ -766,6 +1053,7 @@ function resolveContext(options) {
766
1053
  maxDelayMs: options.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS
767
1054
  };
768
1055
  const fetchImpl = options.fetch ?? globalThis.fetch;
1056
+ const automaticPersistedQueries = options.automaticPersistedQueries ?? false;
769
1057
  const context = {
770
1058
  space,
771
1059
  environment,
@@ -776,8 +1064,19 @@ function resolveContext(options) {
776
1064
  fetch: fetchImpl,
777
1065
  retry,
778
1066
  autoSplitNestedCollections: options.autoSplitNestedCollections ?? true,
779
- splitBatchSize: options.splitBatchSize ?? DEFAULT_SPLIT_BATCH_SIZE
1067
+ autoSplitOnSize: options.autoSplitOnSize ?? true,
1068
+ // APQ lifts Contentful's size ceiling from 8 KB to 16 KB, so the budget
1069
+ // that triggers a size-driven split follows suit unless overridden.
1070
+ maxQuerySize: options.maxQuerySize ?? (automaticPersistedQueries ? DEFAULT_MAX_QUERY_SIZE_WITH_APQ : DEFAULT_MAX_QUERY_SIZE),
1071
+ splitBatchSize: options.splitBatchSize ?? DEFAULT_SPLIT_BATCH_SIZE,
1072
+ annotateQueryOnError: options.annotateQueryOnError ?? true,
1073
+ unresolvableLinks: options.unresolvableLinks ?? "omit",
1074
+ minifyQuery: options.minifyQuery ?? true,
1075
+ automaticPersistedQueries
780
1076
  };
1077
+ if (options.onUnresolvableLink) {
1078
+ context.onUnresolvableLink = options.onUnresolvableLink;
1079
+ }
781
1080
  if (options.signal) context.signal = options.signal;
782
1081
  if (options.next) context.next = options.next;
783
1082
  if (options.cache) context.cache = options.cache;
@@ -795,7 +1094,7 @@ async function resolvePlan(plan, data, variables, context) {
795
1094
  for (const parent of relevant) {
796
1095
  const typename = parent[TYPENAME_ALIAS];
797
1096
  const sys = parent[SYS_ID_ALIAS];
798
- if (!isRecord2(sys) || typeof sys["id"] !== "string") {
1097
+ if (!isRecord4(sys) || typeof sys["id"] !== "string") {
799
1098
  throw new FetchContentfulError(
800
1099
  `Cannot split "${plan.responseKey}": a parent entry is missing its sys id.`,
801
1100
  { code: "STITCH" }
@@ -818,19 +1117,14 @@ async function resolvePlan(plan, data, variables, context) {
818
1117
  ];
819
1118
  await Promise.all(
820
1119
  chunk(ids, context.splitBatchSize).map(async (idBatch) => {
821
- const { document, rootKey } = buildSubquery(
822
- plan,
823
- typename,
824
- idBatch.length,
825
- context
826
- );
1120
+ const { document, rootKey } = buildSubquery(plan, typename, idBatch.length, context);
827
1121
  const subData = await executeDocument(
828
1122
  document,
829
1123
  { ...variables, _splitIds: idBatch },
830
1124
  context
831
1125
  );
832
1126
  const collection = subData[rootKey];
833
- const items = isRecord2(collection) && Array.isArray(collection["items"]) ? collection["items"] : void 0;
1127
+ const items = isRecord4(collection) && Array.isArray(collection["items"]) ? collection["items"] : void 0;
834
1128
  if (!items) {
835
1129
  throw new FetchContentfulError(
836
1130
  `Split subquery for "${typename}" returned no "${rootKey}.items". Check that the content type follows Contentful naming conventions.`,
@@ -838,9 +1132,9 @@ async function resolvePlan(plan, data, variables, context) {
838
1132
  );
839
1133
  }
840
1134
  for (const item of items) {
841
- if (isRecord2(item)) {
1135
+ if (isRecord4(item)) {
842
1136
  const sys = item["sys"];
843
- if (isRecord2(sys) && typeof sys["id"] === "string") {
1137
+ if (isRecord4(sys) && typeof sys["id"] === "string") {
844
1138
  resolved.set(`${typename}:${sys["id"]}`, item[plan.responseKey]);
845
1139
  }
846
1140
  }
@@ -870,12 +1164,18 @@ function cleanupMarkers(data, plans) {
870
1164
  }
871
1165
  }
872
1166
  }
1167
+ function byteLength(text) {
1168
+ return new TextEncoder().encode(text).length;
1169
+ }
873
1170
  async function executeDocument(document, variables, context) {
874
- const { document: outer, plans } = planSplits(document, context);
1171
+ const { document: outer, plans } = planSplitsWithSizeBudget(document, {
1172
+ autoSplitNestedCollections: context.autoSplitNestedCollections,
1173
+ autoSplitOnSize: context.autoSplitOnSize,
1174
+ maxQuerySize: context.maxQuerySize,
1175
+ measure: (doc) => byteLength(printOutgoingQuery(doc, context))
1176
+ });
875
1177
  const data = await rawRequest(outer, variables, context);
876
- await Promise.all(
877
- plans.map((plan) => resolvePlan(plan, data, variables, context))
878
- );
1178
+ await Promise.all(plans.map((plan) => resolvePlan(plan, data, variables, context)));
879
1179
  cleanupMarkers(data, plans);
880
1180
  return data;
881
1181
  }
@@ -889,10 +1189,10 @@ function toDocument(query) {
889
1189
  try {
890
1190
  return parse(typeof query === "string" ? query : String(query));
891
1191
  } catch (cause) {
892
- throw new FetchContentfulError(
893
- `Failed to parse GraphQL query: ${String(cause)}`,
894
- { code: "CONFIG", cause }
895
- );
1192
+ throw new FetchContentfulError(`Failed to parse GraphQL query: ${String(cause)}`, {
1193
+ code: "CONFIG",
1194
+ cause
1195
+ });
896
1196
  }
897
1197
  }
898
1198
  function withAutoVariables(document, variables, context) {
@@ -928,13 +1228,10 @@ async function runFetchContentful(query, options) {
928
1228
  );
929
1229
  }
930
1230
  }
931
- const variables = withAutoVariables(
932
- document,
933
- options.variables ?? {},
934
- context
935
- );
1231
+ const variables = withAutoVariables(document, options.variables ?? {}, context);
936
1232
  const data = await executeDocument(document, variables, context);
937
- const result = options.shapeResponseData === false ? data : shapeData(data);
1233
+ const resolved = context.unresolvableLinks === "null" ? data : omitUnresolvedLinks(data);
1234
+ const result = options.shapeResponseData === false ? resolved : shapeData(resolved);
938
1235
  if (options.unwrapRootField === false) {
939
1236
  return result;
940
1237
  }
@@ -954,6 +1251,6 @@ function createFetchContentful(defaults = {}) {
954
1251
  }
955
1252
  var index_default = fetchContentful;
956
1253
 
957
- export { FetchContentfulError, clearLocaleCache, collectAtPath, createFetchContentful, index_default as default, fetchContentful, getLocales, injectRootArgs, inlineFragments, isFetchContentfulError, readEnvSettings, shapeData, unwrapSingleRoot };
1254
+ export { FetchContentfulError, UNRESOLVABLE_LINK_CODE, annotateQuery, clearLocaleCache, collectAtPath, createFetchContentful, index_default as default, fetchContentful, getLocales, injectRootArgs, inlineFragments, isFetchContentfulError, isUnresolvableLinkError, minifyQuery, omitUnresolvedLinks, partitionUnresolvableLinks, readEnvSettings, shapeData, unwrapSingleRoot };
958
1255
  //# sourceMappingURL=index.mjs.map
959
1256
  //# sourceMappingURL=index.mjs.map