@fourtwelvelabs/fetch-contentful 0.4.1 → 1.0.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
@@ -6,11 +6,112 @@ var graphql = require('graphql');
6
6
 
7
7
  // src/index.ts
8
8
 
9
+ // src/annotate.ts
10
+ var ESC = String.fromCharCode(27);
11
+ var ANSI = {
12
+ reset: `${ESC}[0m`,
13
+ bold: `${ESC}[1m`,
14
+ dim: `${ESC}[2m`,
15
+ red: `${ESC}[31m`
16
+ };
17
+ var DEFAULT_CONTEXT_LINES = 3;
18
+ function supportsColor() {
19
+ if (typeof process === "undefined" || !process.env) {
20
+ return false;
21
+ }
22
+ const forced = process.env.FORCE_COLOR;
23
+ const disabled = process.env.NO_COLOR;
24
+ if (forced) {
25
+ return forced !== "0" && forced !== "false";
26
+ }
27
+ if (disabled) {
28
+ return false;
29
+ }
30
+ return Boolean(process.stderr?.isTTY);
31
+ }
32
+ function makePalette(enabled) {
33
+ const paint = (code) => (text) => enabled ? `${code}${text}${ANSI.reset}` : text;
34
+ return {
35
+ dim: paint(ANSI.dim),
36
+ red: paint(ANSI.red),
37
+ bold: paint(ANSI.bold)
38
+ };
39
+ }
40
+ function collectMarks(errors, lineCount) {
41
+ const marks = [];
42
+ const seen = /* @__PURE__ */ new Set();
43
+ for (const error of errors) {
44
+ for (const location of error.locations ?? []) {
45
+ const line = Math.trunc(location.line);
46
+ if (!(line >= 1 && line <= lineCount)) continue;
47
+ const reported = Math.trunc(location.column);
48
+ const column = reported >= 1 ? reported : 1;
49
+ const key = `${line}:${column}:${error.message}`;
50
+ if (seen.has(key)) continue;
51
+ seen.add(key);
52
+ marks.push({ line, column, message: error.message });
53
+ }
54
+ }
55
+ return marks.sort((a, b) => a.line - b.line || a.column - b.column);
56
+ }
57
+ function excerptsFor(marks, lineCount, contextLines) {
58
+ const excerpts = [];
59
+ for (const mark of marks) {
60
+ const start = Math.max(1, mark.line - contextLines);
61
+ const end = Math.min(lineCount, mark.line + contextLines);
62
+ const previous = excerpts[excerpts.length - 1];
63
+ if (previous && start <= previous.end + 1) {
64
+ previous.end = Math.max(previous.end, end);
65
+ } else {
66
+ excerpts.push({ start, end });
67
+ }
68
+ }
69
+ return excerpts;
70
+ }
71
+ function annotateQuery(query, errors, options = {}) {
72
+ const lines = query.split("\n");
73
+ const marks = collectMarks(errors, lines.length);
74
+ if (marks.length === 0) return "";
75
+ const color = makePalette(options.color ?? supportsColor());
76
+ const excerpts = excerptsFor(marks, lines.length, options.contextLines ?? DEFAULT_CONTEXT_LINES);
77
+ const gutter = String(
78
+ excerpts.reduce((widest, excerpt) => Math.max(widest, excerpt.end), 0)
79
+ ).length;
80
+ const blank = " ".repeat(gutter);
81
+ const out = [];
82
+ for (const [index, excerpt] of excerpts.entries()) {
83
+ if (index > 0) out.push(color.dim(` ${blank} ...`));
84
+ const window = lines.slice(excerpt.start - 1, excerpt.end);
85
+ for (const [offset, source] of window.entries()) {
86
+ const line = excerpt.start + offset;
87
+ const number = String(line).padStart(gutter, " ");
88
+ const here = marks.filter((mark) => mark.line === line);
89
+ if (here.length === 0) {
90
+ out.push(`${color.dim(` ${number} |`)} ${source}`);
91
+ continue;
92
+ }
93
+ out.push(`${color.red(`> ${number} |`)} ${color.bold(source)}`);
94
+ for (const mark of here) {
95
+ const lead = " ".repeat(mark.column - 1);
96
+ out.push(`${color.dim(` ${blank} |`)} ${lead}${color.red(`^ ${mark.message}`)}`);
97
+ }
98
+ }
99
+ }
100
+ return out.join("\n");
101
+ }
102
+
9
103
  // src/errors.ts
10
104
  var FetchContentfulError = class extends Error {
11
105
  code;
12
106
  status;
13
107
  errors;
108
+ /**
109
+ * The GraphQL query exactly as it was sent to Contentful — fragments
110
+ * inlined, arguments injected, and (for a subquery) generated. This is
111
+ * the text the `line`/`column` in {@link FetchContentfulError.errors}
112
+ * point into, so it is the text to annotate; see `annotateQuery`.
113
+ */
114
+ query;
14
115
  /** Internal: whether a retry may succeed. */
15
116
  retryable;
16
117
  /** Internal: server-requested retry delay (from Retry-After), in ms. */
@@ -21,6 +122,7 @@ var FetchContentfulError = class extends Error {
21
122
  this.code = options.code;
22
123
  this.status = options.status;
23
124
  this.errors = options.errors;
125
+ this.query = options.query;
24
126
  this.retryable = options.retryable ?? false;
25
127
  this.retryAfterMs = options.retryAfterMs;
26
128
  }
@@ -136,16 +238,14 @@ function getOperation(document) {
136
238
  );
137
239
  const operation = operations[0];
138
240
  if (!operation || operations.length > 1) {
139
- throw new FetchContentfulError(
140
- "fetch-contentful expects exactly one operation per document.",
141
- { code: "CONFIG" }
142
- );
241
+ throw new FetchContentfulError("fetch-contentful expects exactly one operation per document.", {
242
+ code: "CONFIG"
243
+ });
143
244
  }
144
245
  if (operation.operation !== "query") {
145
- throw new FetchContentfulError(
146
- "fetch-contentful only supports query operations.",
147
- { code: "CONFIG" }
148
- );
246
+ throw new FetchContentfulError("fetch-contentful only supports query operations.", {
247
+ code: "CONFIG"
248
+ });
149
249
  }
150
250
  return operation;
151
251
  }
@@ -157,41 +257,34 @@ function inlineFragments(document) {
157
257
  }
158
258
  }
159
259
  function inlineSelectionSet(selectionSet, stack) {
160
- const selections = selectionSet.selections.map(
161
- (selection) => {
162
- if (selection.kind === graphql.Kind.FRAGMENT_SPREAD) {
163
- const name = selection.name.value;
164
- const fragment = fragments.get(name);
165
- if (!fragment) {
166
- throw new FetchContentfulError(
167
- `Unknown fragment "${name}" referenced in query.`,
168
- { code: "CONFIG" }
169
- );
170
- }
171
- if (stack.includes(name)) {
172
- throw new FetchContentfulError(
173
- `Fragment cycle detected involving "${name}".`,
174
- { code: "CONFIG" }
175
- );
176
- }
177
- return {
178
- kind: graphql.Kind.INLINE_FRAGMENT,
179
- typeCondition: fragment.typeCondition,
180
- selectionSet: inlineSelectionSet(fragment.selectionSet, [
181
- ...stack,
182
- name
183
- ])
184
- };
260
+ const selections = selectionSet.selections.map((selection) => {
261
+ if (selection.kind === graphql.Kind.FRAGMENT_SPREAD) {
262
+ const name = selection.name.value;
263
+ const fragment = fragments.get(name);
264
+ if (!fragment) {
265
+ throw new FetchContentfulError(`Unknown fragment "${name}" referenced in query.`, {
266
+ code: "CONFIG"
267
+ });
185
268
  }
186
- if (selection.selectionSet) {
187
- return {
188
- ...selection,
189
- selectionSet: inlineSelectionSet(selection.selectionSet, stack)
190
- };
269
+ if (stack.includes(name)) {
270
+ throw new FetchContentfulError(`Fragment cycle detected involving "${name}".`, {
271
+ code: "CONFIG"
272
+ });
191
273
  }
192
- return selection;
274
+ return {
275
+ kind: graphql.Kind.INLINE_FRAGMENT,
276
+ typeCondition: fragment.typeCondition,
277
+ selectionSet: inlineSelectionSet(fragment.selectionSet, [...stack, name])
278
+ };
193
279
  }
194
- );
280
+ if (selection.selectionSet) {
281
+ return {
282
+ ...selection,
283
+ selectionSet: inlineSelectionSet(selection.selectionSet, stack)
284
+ };
285
+ }
286
+ return selection;
287
+ });
195
288
  return { ...selectionSet, selections };
196
289
  }
197
290
  const definitions = document.definitions.filter((definition) => definition.kind !== graphql.Kind.FRAGMENT_DEFINITION).map(
@@ -226,10 +319,9 @@ function planSplits(document, options) {
226
319
  continue;
227
320
  }
228
321
  if (selection.kind !== graphql.Kind.FIELD) {
229
- throw new FetchContentfulError(
230
- "Fragment spreads must be inlined before split planning.",
231
- { code: "CONFIG" }
232
- );
322
+ throw new FetchContentfulError("Fragment spreads must be inlined before split planning.", {
323
+ code: "CONFIG"
324
+ });
233
325
  }
234
326
  const field = selection;
235
327
  const isExplicitSplit = hasDirective(field, SPLIT_DIRECTIVE);
@@ -242,10 +334,7 @@ function planSplits(document, options) {
242
334
  }
243
335
  const shouldSplit = depth > 0 && field.selectionSet !== void 0 && !hasDirective(field, NO_SPLIT_DIRECTIVE) && (isExplicitSplit || isAutoSplit);
244
336
  if (shouldSplit) {
245
- const planned = withDirective(
246
- withoutDirective(field, SPLIT_DIRECTIVE),
247
- NO_SPLIT_DIRECTIVE
248
- );
337
+ const planned = withDirective(withoutDirective(field, SPLIT_DIRECTIVE), NO_SPLIT_DIRECTIVE);
249
338
  plans.push({
250
339
  path: [...path],
251
340
  responseKey: responseKeyOf(field),
@@ -344,10 +433,7 @@ function whereIdInArgument() {
344
433
  }
345
434
  function buildSubquery(plan, typename, batchSize, context) {
346
435
  const rootKey = `${lowerFirst(typename)}${COLLECTION_SUFFIX}`;
347
- const args = [
348
- whereIdInArgument(),
349
- intArgument("limit", batchSize)
350
- ];
436
+ const args = [whereIdInArgument(), intArgument("limit", batchSize)];
351
437
  if (context.preview) {
352
438
  args.push(booleanArgument("preview", true));
353
439
  }
@@ -356,10 +442,7 @@ function buildSubquery(plan, typename, batchSize, context) {
356
442
  }
357
443
  const rootField = {
358
444
  ...namedField(rootKey, void 0, [
359
- namedField("items", void 0, [
360
- namedField("sys", void 0, [namedField("id")]),
361
- plan.field
362
- ])
445
+ namedField("items", void 0, [namedField("sys", void 0, [namedField("id")]), plan.field])
363
446
  ]),
364
447
  arguments: args
365
448
  };
@@ -409,6 +492,58 @@ function stripInternalDirectives(document) {
409
492
  });
410
493
  }
411
494
 
495
+ // src/unresolvable.ts
496
+ var UNRESOLVABLE_LINK_CODE = "UNRESOLVABLE_LINK";
497
+ var COLLECTION_SUFFIX2 = "Collection";
498
+ function isRecord(value) {
499
+ return typeof value === "object" && value !== null && !Array.isArray(value);
500
+ }
501
+ function isCollectionValue(value) {
502
+ return isRecord(value) && Array.isArray(value["items"]);
503
+ }
504
+ function isCollectionKey(key) {
505
+ return key.endsWith(COLLECTION_SUFFIX2) && key !== COLLECTION_SUFFIX2;
506
+ }
507
+ function isUnresolvableLinkError(error) {
508
+ const contentful = error.extensions?.["contentful"];
509
+ return isRecord(contentful) && contentful["code"] === UNRESOLVABLE_LINK_CODE;
510
+ }
511
+ function partitionUnresolvableLinks(errors) {
512
+ const unresolvable = [];
513
+ const fatal = [];
514
+ for (const error of errors) {
515
+ if (isUnresolvableLinkError(error)) {
516
+ unresolvable.push(error);
517
+ } else {
518
+ fatal.push(error);
519
+ }
520
+ }
521
+ return { unresolvable, fatal };
522
+ }
523
+ function walk(value) {
524
+ if (Array.isArray(value)) {
525
+ return value.map(walk);
526
+ }
527
+ if (isRecord(value)) {
528
+ const result = {};
529
+ for (const [key, child] of Object.entries(value)) {
530
+ result[key] = isCollectionKey(key) && isCollectionValue(child) ? stripCollection(child) : walk(child);
531
+ }
532
+ return result;
533
+ }
534
+ return value;
535
+ }
536
+ function stripCollection(collection) {
537
+ const result = {};
538
+ for (const [key, value] of Object.entries(collection)) {
539
+ result[key] = key === "items" ? value.filter((item) => item !== null && item !== void 0).map(walk) : walk(value);
540
+ }
541
+ return result;
542
+ }
543
+ function omitUnresolvedLinks(data) {
544
+ return walk(data);
545
+ }
546
+
412
547
  // src/client.ts
413
548
  function graphqlEndpoint(space, environment) {
414
549
  return `https://graphql.contentful.com/content/v1/spaces/${encodeURIComponent(
@@ -444,6 +579,35 @@ function pickDeclaredVariables(document, variables) {
444
579
  function isRetryableStatus(status) {
445
580
  return status === 408 || status === 429 || status >= 500;
446
581
  }
582
+ function isRecord2(value) {
583
+ return typeof value === "object" && value !== null && !Array.isArray(value);
584
+ }
585
+ function reportUnresolvableLinks(errors, context) {
586
+ if (!context.onUnresolvableLink) return;
587
+ try {
588
+ context.onUnresolvableLink(errors);
589
+ } catch {
590
+ }
591
+ }
592
+ async function readGraphQLErrors(response) {
593
+ try {
594
+ const payload = await response.json();
595
+ const errors = payload?.errors;
596
+ return Array.isArray(errors) && errors.length > 0 ? errors : void 0;
597
+ } catch {
598
+ return void 0;
599
+ }
600
+ }
601
+ function describeFailure(summary, errors, query, context) {
602
+ if (!errors || errors.length === 0) return summary;
603
+ const message = `${summary} ${errors.map((error) => error.message).join("; ")}`;
604
+ if (context.annotateQueryOnError === false) return message;
605
+ const annotated = annotateQuery(query, errors);
606
+ return annotated ? `${message}
607
+
608
+ ${annotated}
609
+ ` : message;
610
+ }
447
611
  async function rawRequest(document, variables, context) {
448
612
  const query = graphql.print(stripInternalDirectives(document));
449
613
  const body = JSON.stringify({
@@ -469,16 +633,24 @@ async function rawRequest(document, variables, context) {
469
633
  } catch (cause) {
470
634
  throw new FetchContentfulError(
471
635
  `Network error while contacting Contentful: ${String(cause)}`,
472
- { code: "NETWORK", retryable: true, cause }
636
+ { code: "NETWORK", query, retryable: true, cause }
473
637
  );
474
638
  }
475
639
  if (!response.ok) {
476
640
  const retryable = isRetryableStatus(response.status);
641
+ const errors = retryable ? void 0 : await readGraphQLErrors(response);
477
642
  throw new FetchContentfulError(
478
- `Contentful responded with HTTP ${response.status}.`,
643
+ describeFailure(
644
+ `Contentful responded with HTTP ${response.status}.`,
645
+ errors,
646
+ query,
647
+ context
648
+ ),
479
649
  {
480
650
  code: "HTTP",
481
651
  status: response.status,
652
+ errors,
653
+ query,
482
654
  retryable,
483
655
  retryAfterMs: retryable ? parseRetryAfter(response.headers.get("Retry-After")) : void 0
484
656
  }
@@ -488,22 +660,29 @@ async function rawRequest(document, variables, context) {
488
660
  try {
489
661
  payload = await response.json();
490
662
  } catch (cause) {
491
- throw new FetchContentfulError(
492
- "Contentful returned an unreadable response body.",
493
- { code: "NETWORK", retryable: true, cause }
494
- );
663
+ throw new FetchContentfulError("Contentful returned an unreadable response body.", {
664
+ code: "NETWORK",
665
+ query,
666
+ retryable: true,
667
+ cause
668
+ });
495
669
  }
496
670
  if (payload.errors && payload.errors.length > 0) {
497
- throw new FetchContentfulError(
498
- `Contentful returned GraphQL errors: ${payload.errors.map((error) => error.message).join("; ")}`,
499
- { code: "GRAPHQL", errors: payload.errors }
500
- );
671
+ const { unresolvable, fatal } = partitionUnresolvableLinks(payload.errors);
672
+ const tolerable = context.unresolvableLinks !== "error" && fatal.length === 0 && unresolvable.length > 0 && isRecord2(payload.data);
673
+ if (!tolerable) {
674
+ throw new FetchContentfulError(
675
+ describeFailure("Contentful returned GraphQL errors:", payload.errors, query, context),
676
+ { code: "GRAPHQL", errors: payload.errors, query }
677
+ );
678
+ }
679
+ reportUnresolvableLinks(unresolvable, context);
501
680
  }
502
681
  if (!payload.data || typeof payload.data !== "object") {
503
- throw new FetchContentfulError(
504
- "Contentful returned no data and no errors.",
505
- { code: "GRAPHQL" }
506
- );
682
+ throw new FetchContentfulError("Contentful returned no data and no errors.", {
683
+ code: "GRAPHQL",
684
+ query
685
+ });
507
686
  }
508
687
  return payload.data;
509
688
  }, context.retry);
@@ -519,7 +698,6 @@ function readEnvSettings() {
519
698
  space: void 0,
520
699
  environment: void 0,
521
700
  deliveryToken: void 0,
522
- token: void 0,
523
701
  previewToken: void 0
524
702
  };
525
703
  }
@@ -534,7 +712,6 @@ function readEnvSettings() {
534
712
  process.env.CONTENTFUL_ENVIRONMENT || process.env.NEXT_PUBLIC_CONTENTFUL_ENVIRONMENT
535
713
  ),
536
714
  deliveryToken,
537
- token: deliveryToken,
538
715
  // No `NEXT_PUBLIC_` fallback: see the note at the top of this file.
539
716
  previewToken: orUndefined(process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN)
540
717
  };
@@ -576,20 +753,18 @@ function injectIntoField(field, args) {
576
753
  return { ...field, arguments: [...argumentsOf(field), ...additions] };
577
754
  }
578
755
  function injectIntoSelectionSet(selectionSet, args) {
579
- const selections = selectionSet.selections.map(
580
- (selection) => {
581
- if (selection.kind === graphql.Kind.FIELD) {
582
- return injectIntoField(selection, args);
583
- }
584
- if (selection.kind === graphql.Kind.INLINE_FRAGMENT) {
585
- return {
586
- ...selection,
587
- selectionSet: injectIntoSelectionSet(selection.selectionSet, args)
588
- };
589
- }
590
- return selection;
756
+ const selections = selectionSet.selections.map((selection) => {
757
+ if (selection.kind === graphql.Kind.FIELD) {
758
+ return injectIntoField(selection, args);
591
759
  }
592
- );
760
+ if (selection.kind === graphql.Kind.INLINE_FRAGMENT) {
761
+ return {
762
+ ...selection,
763
+ selectionSet: injectIntoSelectionSet(selection.selectionSet, args)
764
+ };
765
+ }
766
+ return selection;
767
+ });
593
768
  return { ...selectionSet, selections };
594
769
  }
595
770
  function injectRootArgs(document, args) {
@@ -630,27 +805,24 @@ async function fetchLocales(context) {
630
805
  if (context.signal) init.signal = context.signal;
631
806
  response = await context.fetch(localesEndpoint(context), init);
632
807
  } catch (cause) {
633
- throw new FetchContentfulError(
634
- `Network error while fetching locales: ${String(cause)}`,
635
- { code: "NETWORK", retryable: true, cause }
636
- );
808
+ throw new FetchContentfulError(`Network error while fetching locales: ${String(cause)}`, {
809
+ code: "NETWORK",
810
+ retryable: true,
811
+ cause
812
+ });
637
813
  }
638
814
  if (!response.ok) {
639
- throw new FetchContentfulError(
640
- `Locale request failed with HTTP ${response.status}.`,
641
- {
642
- code: "HTTP",
643
- status: response.status,
644
- retryable: response.status === 429 || response.status >= 500
645
- }
646
- );
815
+ throw new FetchContentfulError(`Locale request failed with HTTP ${response.status}.`, {
816
+ code: "HTTP",
817
+ status: response.status,
818
+ retryable: response.status === 429 || response.status >= 500
819
+ });
647
820
  }
648
821
  const payload = await response.json();
649
822
  if (!Array.isArray(payload.items)) {
650
- throw new FetchContentfulError(
651
- "Locale response did not include an items array.",
652
- { code: "NETWORK" }
653
- );
823
+ throw new FetchContentfulError("Locale response did not include an items array.", {
824
+ code: "NETWORK"
825
+ });
654
826
  }
655
827
  return payload.items.map((item) => ({
656
828
  code: item.code,
@@ -678,24 +850,22 @@ function clearLocaleCache() {
678
850
  }
679
851
 
680
852
  // src/shape.ts
681
- var COLLECTION_SUFFIX2 = "Collection";
682
- function isRecord(value) {
853
+ var COLLECTION_SUFFIX3 = "Collection";
854
+ function isRecord3(value) {
683
855
  return typeof value === "object" && value !== null && !Array.isArray(value);
684
856
  }
685
- function isCollectionValue(value) {
686
- return isRecord(value) && Array.isArray(value["items"]);
857
+ function isCollectionValue2(value) {
858
+ return isRecord3(value) && Array.isArray(value["items"]);
687
859
  }
688
860
  function shapeData(data) {
689
861
  if (Array.isArray(data)) {
690
862
  return data.map((item) => shapeData(item));
691
863
  }
692
- if (isRecord(data)) {
864
+ if (isRecord3(data)) {
693
865
  const shaped = {};
694
866
  for (const [key, value] of Object.entries(data)) {
695
- if (key.endsWith(COLLECTION_SUFFIX2) && key !== COLLECTION_SUFFIX2 && isCollectionValue(value)) {
696
- shaped[key.slice(0, -COLLECTION_SUFFIX2.length)] = shapeData(
697
- value.items
698
- );
867
+ if (key.endsWith(COLLECTION_SUFFIX3) && key !== COLLECTION_SUFFIX3 && isCollectionValue2(value)) {
868
+ shaped[key.slice(0, -COLLECTION_SUFFIX3.length)] = shapeData(value.items);
699
869
  } else {
700
870
  shaped[key] = shapeData(value);
701
871
  }
@@ -705,7 +875,7 @@ function shapeData(data) {
705
875
  return data;
706
876
  }
707
877
  function unwrapSingleRoot(data) {
708
- if (isRecord(data)) {
878
+ if (isRecord3(data)) {
709
879
  const keys = Object.keys(data);
710
880
  if (keys.length === 1) {
711
881
  return data[keys[0]];
@@ -720,7 +890,7 @@ var DEFAULT_RETRIES = 5;
720
890
  var DEFAULT_RETRY_DELAY_MS = 250;
721
891
  var DEFAULT_MAX_RETRY_DELAY_MS = 8e3;
722
892
  var DEFAULT_SPLIT_BATCH_SIZE = 50;
723
- function isRecord2(value) {
893
+ function isRecord4(value) {
724
894
  return typeof value === "object" && value !== null && !Array.isArray(value);
725
895
  }
726
896
  function collectAtPath(data, path) {
@@ -728,7 +898,7 @@ function collectAtPath(data, path) {
728
898
  for (const segment of path) {
729
899
  const next = [];
730
900
  for (const value of current) {
731
- if (isRecord2(value)) {
901
+ if (isRecord4(value)) {
732
902
  const child = value[segment];
733
903
  if (Array.isArray(child)) {
734
904
  next.push(...child);
@@ -739,14 +909,14 @@ function collectAtPath(data, path) {
739
909
  }
740
910
  current = next;
741
911
  }
742
- return current.filter(isRecord2);
912
+ return current.filter(isRecord4);
743
913
  }
744
914
  function resolveContext(options) {
745
915
  const env = readEnvSettings();
746
916
  const space = options.space ?? env.space;
747
917
  const environment = options.environment ?? env.environment ?? "master";
748
918
  const preview = options.preview ?? false;
749
- const token = preview ? options.previewToken ?? options.token ?? env.previewToken : options.deliveryToken ?? options.token ?? env.deliveryToken;
919
+ const token = preview ? options.previewToken ?? env.previewToken : options.deliveryToken ?? env.deliveryToken;
750
920
  if (!space || !token) {
751
921
  const missing = [];
752
922
  if (!space) {
@@ -780,8 +950,13 @@ function resolveContext(options) {
780
950
  fetch: fetchImpl,
781
951
  retry,
782
952
  autoSplitNestedCollections: options.autoSplitNestedCollections ?? true,
783
- splitBatchSize: options.splitBatchSize ?? DEFAULT_SPLIT_BATCH_SIZE
953
+ splitBatchSize: options.splitBatchSize ?? DEFAULT_SPLIT_BATCH_SIZE,
954
+ annotateQueryOnError: options.annotateQueryOnError ?? true,
955
+ unresolvableLinks: options.unresolvableLinks ?? "omit"
784
956
  };
957
+ if (options.onUnresolvableLink) {
958
+ context.onUnresolvableLink = options.onUnresolvableLink;
959
+ }
785
960
  if (options.signal) context.signal = options.signal;
786
961
  if (options.next) context.next = options.next;
787
962
  if (options.cache) context.cache = options.cache;
@@ -799,7 +974,7 @@ async function resolvePlan(plan, data, variables, context) {
799
974
  for (const parent of relevant) {
800
975
  const typename = parent[TYPENAME_ALIAS];
801
976
  const sys = parent[SYS_ID_ALIAS];
802
- if (!isRecord2(sys) || typeof sys["id"] !== "string") {
977
+ if (!isRecord4(sys) || typeof sys["id"] !== "string") {
803
978
  throw new FetchContentfulError(
804
979
  `Cannot split "${plan.responseKey}": a parent entry is missing its sys id.`,
805
980
  { code: "STITCH" }
@@ -822,19 +997,14 @@ async function resolvePlan(plan, data, variables, context) {
822
997
  ];
823
998
  await Promise.all(
824
999
  chunk(ids, context.splitBatchSize).map(async (idBatch) => {
825
- const { document, rootKey } = buildSubquery(
826
- plan,
827
- typename,
828
- idBatch.length,
829
- context
830
- );
1000
+ const { document, rootKey } = buildSubquery(plan, typename, idBatch.length, context);
831
1001
  const subData = await executeDocument(
832
1002
  document,
833
1003
  { ...variables, _splitIds: idBatch },
834
1004
  context
835
1005
  );
836
1006
  const collection = subData[rootKey];
837
- const items = isRecord2(collection) && Array.isArray(collection["items"]) ? collection["items"] : void 0;
1007
+ const items = isRecord4(collection) && Array.isArray(collection["items"]) ? collection["items"] : void 0;
838
1008
  if (!items) {
839
1009
  throw new FetchContentfulError(
840
1010
  `Split subquery for "${typename}" returned no "${rootKey}.items". Check that the content type follows Contentful naming conventions.`,
@@ -842,9 +1012,9 @@ async function resolvePlan(plan, data, variables, context) {
842
1012
  );
843
1013
  }
844
1014
  for (const item of items) {
845
- if (isRecord2(item)) {
1015
+ if (isRecord4(item)) {
846
1016
  const sys = item["sys"];
847
- if (isRecord2(sys) && typeof sys["id"] === "string") {
1017
+ if (isRecord4(sys) && typeof sys["id"] === "string") {
848
1018
  resolved.set(`${typename}:${sys["id"]}`, item[plan.responseKey]);
849
1019
  }
850
1020
  }
@@ -877,9 +1047,7 @@ function cleanupMarkers(data, plans) {
877
1047
  async function executeDocument(document, variables, context) {
878
1048
  const { document: outer, plans } = planSplits(document, context);
879
1049
  const data = await rawRequest(outer, variables, context);
880
- await Promise.all(
881
- plans.map((plan) => resolvePlan(plan, data, variables, context))
882
- );
1050
+ await Promise.all(plans.map((plan) => resolvePlan(plan, data, variables, context)));
883
1051
  cleanupMarkers(data, plans);
884
1052
  return data;
885
1053
  }
@@ -893,10 +1061,10 @@ function toDocument(query) {
893
1061
  try {
894
1062
  return graphql.parse(typeof query === "string" ? query : String(query));
895
1063
  } catch (cause) {
896
- throw new FetchContentfulError(
897
- `Failed to parse GraphQL query: ${String(cause)}`,
898
- { code: "CONFIG", cause }
899
- );
1064
+ throw new FetchContentfulError(`Failed to parse GraphQL query: ${String(cause)}`, {
1065
+ code: "CONFIG",
1066
+ cause
1067
+ });
900
1068
  }
901
1069
  }
902
1070
  function withAutoVariables(document, variables, context) {
@@ -932,13 +1100,10 @@ async function runFetchContentful(query, options) {
932
1100
  );
933
1101
  }
934
1102
  }
935
- const variables = withAutoVariables(
936
- document,
937
- options.variables ?? {},
938
- context
939
- );
1103
+ const variables = withAutoVariables(document, options.variables ?? {}, context);
940
1104
  const data = await executeDocument(document, variables, context);
941
- const result = options.shapeResponseData === false ? data : shapeData(data);
1105
+ const resolved = context.unresolvableLinks === "null" ? data : omitUnresolvedLinks(data);
1106
+ const result = options.shapeResponseData === false ? resolved : shapeData(resolved);
942
1107
  if (options.unwrapRootField === false) {
943
1108
  return result;
944
1109
  }
@@ -959,6 +1124,8 @@ function createFetchContentful(defaults = {}) {
959
1124
  var index_default = fetchContentful;
960
1125
 
961
1126
  exports.FetchContentfulError = FetchContentfulError;
1127
+ exports.UNRESOLVABLE_LINK_CODE = UNRESOLVABLE_LINK_CODE;
1128
+ exports.annotateQuery = annotateQuery;
962
1129
  exports.clearLocaleCache = clearLocaleCache;
963
1130
  exports.collectAtPath = collectAtPath;
964
1131
  exports.createFetchContentful = createFetchContentful;
@@ -968,6 +1135,9 @@ exports.getLocales = getLocales;
968
1135
  exports.injectRootArgs = injectRootArgs;
969
1136
  exports.inlineFragments = inlineFragments;
970
1137
  exports.isFetchContentfulError = isFetchContentfulError;
1138
+ exports.isUnresolvableLinkError = isUnresolvableLinkError;
1139
+ exports.omitUnresolvedLinks = omitUnresolvedLinks;
1140
+ exports.partitionUnresolvableLinks = partitionUnresolvableLinks;
971
1141
  exports.readEnvSettings = readEnvSettings;
972
1142
  exports.shapeData = shapeData;
973
1143
  exports.unwrapSingleRoot = unwrapSingleRoot;