@fourtwelvelabs/fetch-contentful 0.4.0 → 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.mjs CHANGED
@@ -2,11 +2,112 @@ import { 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
  }
@@ -132,16 +234,14 @@ function getOperation(document) {
132
234
  );
133
235
  const operation = operations[0];
134
236
  if (!operation || operations.length > 1) {
135
- throw new FetchContentfulError(
136
- "fetch-contentful expects exactly one operation per document.",
137
- { code: "CONFIG" }
138
- );
237
+ throw new FetchContentfulError("fetch-contentful expects exactly one operation per document.", {
238
+ code: "CONFIG"
239
+ });
139
240
  }
140
241
  if (operation.operation !== "query") {
141
- throw new FetchContentfulError(
142
- "fetch-contentful only supports query operations.",
143
- { code: "CONFIG" }
144
- );
242
+ throw new FetchContentfulError("fetch-contentful only supports query operations.", {
243
+ code: "CONFIG"
244
+ });
145
245
  }
146
246
  return operation;
147
247
  }
@@ -153,41 +253,34 @@ function inlineFragments(document) {
153
253
  }
154
254
  }
155
255
  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
- };
256
+ const selections = selectionSet.selections.map((selection) => {
257
+ if (selection.kind === Kind.FRAGMENT_SPREAD) {
258
+ const name = selection.name.value;
259
+ const fragment = fragments.get(name);
260
+ if (!fragment) {
261
+ throw new FetchContentfulError(`Unknown fragment "${name}" referenced in query.`, {
262
+ code: "CONFIG"
263
+ });
181
264
  }
182
- if (selection.selectionSet) {
183
- return {
184
- ...selection,
185
- selectionSet: inlineSelectionSet(selection.selectionSet, stack)
186
- };
265
+ if (stack.includes(name)) {
266
+ throw new FetchContentfulError(`Fragment cycle detected involving "${name}".`, {
267
+ code: "CONFIG"
268
+ });
187
269
  }
188
- return selection;
270
+ return {
271
+ kind: Kind.INLINE_FRAGMENT,
272
+ typeCondition: fragment.typeCondition,
273
+ selectionSet: inlineSelectionSet(fragment.selectionSet, [...stack, name])
274
+ };
189
275
  }
190
- );
276
+ if (selection.selectionSet) {
277
+ return {
278
+ ...selection,
279
+ selectionSet: inlineSelectionSet(selection.selectionSet, stack)
280
+ };
281
+ }
282
+ return selection;
283
+ });
191
284
  return { ...selectionSet, selections };
192
285
  }
193
286
  const definitions = document.definitions.filter((definition) => definition.kind !== Kind.FRAGMENT_DEFINITION).map(
@@ -222,10 +315,9 @@ function planSplits(document, options) {
222
315
  continue;
223
316
  }
224
317
  if (selection.kind !== Kind.FIELD) {
225
- throw new FetchContentfulError(
226
- "Fragment spreads must be inlined before split planning.",
227
- { code: "CONFIG" }
228
- );
318
+ throw new FetchContentfulError("Fragment spreads must be inlined before split planning.", {
319
+ code: "CONFIG"
320
+ });
229
321
  }
230
322
  const field = selection;
231
323
  const isExplicitSplit = hasDirective(field, SPLIT_DIRECTIVE);
@@ -238,10 +330,7 @@ function planSplits(document, options) {
238
330
  }
239
331
  const shouldSplit = depth > 0 && field.selectionSet !== void 0 && !hasDirective(field, NO_SPLIT_DIRECTIVE) && (isExplicitSplit || isAutoSplit);
240
332
  if (shouldSplit) {
241
- const planned = withDirective(
242
- withoutDirective(field, SPLIT_DIRECTIVE),
243
- NO_SPLIT_DIRECTIVE
244
- );
333
+ const planned = withDirective(withoutDirective(field, SPLIT_DIRECTIVE), NO_SPLIT_DIRECTIVE);
245
334
  plans.push({
246
335
  path: [...path],
247
336
  responseKey: responseKeyOf(field),
@@ -340,10 +429,7 @@ function whereIdInArgument() {
340
429
  }
341
430
  function buildSubquery(plan, typename, batchSize, context) {
342
431
  const rootKey = `${lowerFirst(typename)}${COLLECTION_SUFFIX}`;
343
- const args = [
344
- whereIdInArgument(),
345
- intArgument("limit", batchSize)
346
- ];
432
+ const args = [whereIdInArgument(), intArgument("limit", batchSize)];
347
433
  if (context.preview) {
348
434
  args.push(booleanArgument("preview", true));
349
435
  }
@@ -352,10 +438,7 @@ function buildSubquery(plan, typename, batchSize, context) {
352
438
  }
353
439
  const rootField = {
354
440
  ...namedField(rootKey, void 0, [
355
- namedField("items", void 0, [
356
- namedField("sys", void 0, [namedField("id")]),
357
- plan.field
358
- ])
441
+ namedField("items", void 0, [namedField("sys", void 0, [namedField("id")]), plan.field])
359
442
  ]),
360
443
  arguments: args
361
444
  };
@@ -405,6 +488,58 @@ function stripInternalDirectives(document) {
405
488
  });
406
489
  }
407
490
 
491
+ // src/unresolvable.ts
492
+ var UNRESOLVABLE_LINK_CODE = "UNRESOLVABLE_LINK";
493
+ var COLLECTION_SUFFIX2 = "Collection";
494
+ function isRecord(value) {
495
+ return typeof value === "object" && value !== null && !Array.isArray(value);
496
+ }
497
+ function isCollectionValue(value) {
498
+ return isRecord(value) && Array.isArray(value["items"]);
499
+ }
500
+ function isCollectionKey(key) {
501
+ return key.endsWith(COLLECTION_SUFFIX2) && key !== COLLECTION_SUFFIX2;
502
+ }
503
+ function isUnresolvableLinkError(error) {
504
+ const contentful = error.extensions?.["contentful"];
505
+ return isRecord(contentful) && contentful["code"] === UNRESOLVABLE_LINK_CODE;
506
+ }
507
+ function partitionUnresolvableLinks(errors) {
508
+ const unresolvable = [];
509
+ const fatal = [];
510
+ for (const error of errors) {
511
+ if (isUnresolvableLinkError(error)) {
512
+ unresolvable.push(error);
513
+ } else {
514
+ fatal.push(error);
515
+ }
516
+ }
517
+ return { unresolvable, fatal };
518
+ }
519
+ function walk(value) {
520
+ if (Array.isArray(value)) {
521
+ return value.map(walk);
522
+ }
523
+ if (isRecord(value)) {
524
+ const result = {};
525
+ for (const [key, child] of Object.entries(value)) {
526
+ result[key] = isCollectionKey(key) && isCollectionValue(child) ? stripCollection(child) : walk(child);
527
+ }
528
+ return result;
529
+ }
530
+ return value;
531
+ }
532
+ function stripCollection(collection) {
533
+ const result = {};
534
+ for (const [key, value] of Object.entries(collection)) {
535
+ result[key] = key === "items" ? value.filter((item) => item !== null && item !== void 0).map(walk) : walk(value);
536
+ }
537
+ return result;
538
+ }
539
+ function omitUnresolvedLinks(data) {
540
+ return walk(data);
541
+ }
542
+
408
543
  // src/client.ts
409
544
  function graphqlEndpoint(space, environment) {
410
545
  return `https://graphql.contentful.com/content/v1/spaces/${encodeURIComponent(
@@ -440,6 +575,35 @@ function pickDeclaredVariables(document, variables) {
440
575
  function isRetryableStatus(status) {
441
576
  return status === 408 || status === 429 || status >= 500;
442
577
  }
578
+ function isRecord2(value) {
579
+ return typeof value === "object" && value !== null && !Array.isArray(value);
580
+ }
581
+ function reportUnresolvableLinks(errors, context) {
582
+ if (!context.onUnresolvableLink) return;
583
+ try {
584
+ context.onUnresolvableLink(errors);
585
+ } catch {
586
+ }
587
+ }
588
+ async function readGraphQLErrors(response) {
589
+ try {
590
+ const payload = await response.json();
591
+ const errors = payload?.errors;
592
+ return Array.isArray(errors) && errors.length > 0 ? errors : void 0;
593
+ } catch {
594
+ return void 0;
595
+ }
596
+ }
597
+ function describeFailure(summary, errors, query, context) {
598
+ if (!errors || errors.length === 0) return summary;
599
+ const message = `${summary} ${errors.map((error) => error.message).join("; ")}`;
600
+ if (context.annotateQueryOnError === false) return message;
601
+ const annotated = annotateQuery(query, errors);
602
+ return annotated ? `${message}
603
+
604
+ ${annotated}
605
+ ` : message;
606
+ }
443
607
  async function rawRequest(document, variables, context) {
444
608
  const query = print(stripInternalDirectives(document));
445
609
  const body = JSON.stringify({
@@ -465,16 +629,24 @@ async function rawRequest(document, variables, context) {
465
629
  } catch (cause) {
466
630
  throw new FetchContentfulError(
467
631
  `Network error while contacting Contentful: ${String(cause)}`,
468
- { code: "NETWORK", retryable: true, cause }
632
+ { code: "NETWORK", query, retryable: true, cause }
469
633
  );
470
634
  }
471
635
  if (!response.ok) {
472
636
  const retryable = isRetryableStatus(response.status);
637
+ const errors = retryable ? void 0 : await readGraphQLErrors(response);
473
638
  throw new FetchContentfulError(
474
- `Contentful responded with HTTP ${response.status}.`,
639
+ describeFailure(
640
+ `Contentful responded with HTTP ${response.status}.`,
641
+ errors,
642
+ query,
643
+ context
644
+ ),
475
645
  {
476
646
  code: "HTTP",
477
647
  status: response.status,
648
+ errors,
649
+ query,
478
650
  retryable,
479
651
  retryAfterMs: retryable ? parseRetryAfter(response.headers.get("Retry-After")) : void 0
480
652
  }
@@ -484,22 +656,29 @@ async function rawRequest(document, variables, context) {
484
656
  try {
485
657
  payload = await response.json();
486
658
  } catch (cause) {
487
- throw new FetchContentfulError(
488
- "Contentful returned an unreadable response body.",
489
- { code: "NETWORK", retryable: true, cause }
490
- );
659
+ throw new FetchContentfulError("Contentful returned an unreadable response body.", {
660
+ code: "NETWORK",
661
+ query,
662
+ retryable: true,
663
+ cause
664
+ });
491
665
  }
492
666
  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
- );
667
+ const { unresolvable, fatal } = partitionUnresolvableLinks(payload.errors);
668
+ const tolerable = context.unresolvableLinks !== "error" && fatal.length === 0 && unresolvable.length > 0 && isRecord2(payload.data);
669
+ if (!tolerable) {
670
+ throw new FetchContentfulError(
671
+ describeFailure("Contentful returned GraphQL errors:", payload.errors, query, context),
672
+ { code: "GRAPHQL", errors: payload.errors, query }
673
+ );
674
+ }
675
+ reportUnresolvableLinks(unresolvable, context);
497
676
  }
498
677
  if (!payload.data || typeof payload.data !== "object") {
499
- throw new FetchContentfulError(
500
- "Contentful returned no data and no errors.",
501
- { code: "GRAPHQL" }
502
- );
678
+ throw new FetchContentfulError("Contentful returned no data and no errors.", {
679
+ code: "GRAPHQL",
680
+ query
681
+ });
503
682
  }
504
683
  return payload.data;
505
684
  }, context.retry);
@@ -515,7 +694,6 @@ function readEnvSettings() {
515
694
  space: void 0,
516
695
  environment: void 0,
517
696
  deliveryToken: void 0,
518
- token: void 0,
519
697
  previewToken: void 0
520
698
  };
521
699
  }
@@ -530,7 +708,6 @@ function readEnvSettings() {
530
708
  process.env.CONTENTFUL_ENVIRONMENT || process.env.NEXT_PUBLIC_CONTENTFUL_ENVIRONMENT
531
709
  ),
532
710
  deliveryToken,
533
- token: deliveryToken,
534
711
  // No `NEXT_PUBLIC_` fallback: see the note at the top of this file.
535
712
  previewToken: orUndefined(process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN)
536
713
  };
@@ -572,20 +749,18 @@ function injectIntoField(field, args) {
572
749
  return { ...field, arguments: [...argumentsOf(field), ...additions] };
573
750
  }
574
751
  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;
752
+ const selections = selectionSet.selections.map((selection) => {
753
+ if (selection.kind === Kind.FIELD) {
754
+ return injectIntoField(selection, args);
587
755
  }
588
- );
756
+ if (selection.kind === Kind.INLINE_FRAGMENT) {
757
+ return {
758
+ ...selection,
759
+ selectionSet: injectIntoSelectionSet(selection.selectionSet, args)
760
+ };
761
+ }
762
+ return selection;
763
+ });
589
764
  return { ...selectionSet, selections };
590
765
  }
591
766
  function injectRootArgs(document, args) {
@@ -626,27 +801,24 @@ async function fetchLocales(context) {
626
801
  if (context.signal) init.signal = context.signal;
627
802
  response = await context.fetch(localesEndpoint(context), init);
628
803
  } catch (cause) {
629
- throw new FetchContentfulError(
630
- `Network error while fetching locales: ${String(cause)}`,
631
- { code: "NETWORK", retryable: true, cause }
632
- );
804
+ throw new FetchContentfulError(`Network error while fetching locales: ${String(cause)}`, {
805
+ code: "NETWORK",
806
+ retryable: true,
807
+ cause
808
+ });
633
809
  }
634
810
  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
- );
811
+ throw new FetchContentfulError(`Locale request failed with HTTP ${response.status}.`, {
812
+ code: "HTTP",
813
+ status: response.status,
814
+ retryable: response.status === 429 || response.status >= 500
815
+ });
643
816
  }
644
817
  const payload = await response.json();
645
818
  if (!Array.isArray(payload.items)) {
646
- throw new FetchContentfulError(
647
- "Locale response did not include an items array.",
648
- { code: "NETWORK" }
649
- );
819
+ throw new FetchContentfulError("Locale response did not include an items array.", {
820
+ code: "NETWORK"
821
+ });
650
822
  }
651
823
  return payload.items.map((item) => ({
652
824
  code: item.code,
@@ -674,24 +846,22 @@ function clearLocaleCache() {
674
846
  }
675
847
 
676
848
  // src/shape.ts
677
- var COLLECTION_SUFFIX2 = "Collection";
678
- function isRecord(value) {
849
+ var COLLECTION_SUFFIX3 = "Collection";
850
+ function isRecord3(value) {
679
851
  return typeof value === "object" && value !== null && !Array.isArray(value);
680
852
  }
681
- function isCollectionValue(value) {
682
- return isRecord(value) && Array.isArray(value["items"]);
853
+ function isCollectionValue2(value) {
854
+ return isRecord3(value) && Array.isArray(value["items"]);
683
855
  }
684
856
  function shapeData(data) {
685
857
  if (Array.isArray(data)) {
686
858
  return data.map((item) => shapeData(item));
687
859
  }
688
- if (isRecord(data)) {
860
+ if (isRecord3(data)) {
689
861
  const shaped = {};
690
862
  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
- );
863
+ if (key.endsWith(COLLECTION_SUFFIX3) && key !== COLLECTION_SUFFIX3 && isCollectionValue2(value)) {
864
+ shaped[key.slice(0, -COLLECTION_SUFFIX3.length)] = shapeData(value.items);
695
865
  } else {
696
866
  shaped[key] = shapeData(value);
697
867
  }
@@ -701,7 +871,7 @@ function shapeData(data) {
701
871
  return data;
702
872
  }
703
873
  function unwrapSingleRoot(data) {
704
- if (isRecord(data)) {
874
+ if (isRecord3(data)) {
705
875
  const keys = Object.keys(data);
706
876
  if (keys.length === 1) {
707
877
  return data[keys[0]];
@@ -716,7 +886,7 @@ var DEFAULT_RETRIES = 5;
716
886
  var DEFAULT_RETRY_DELAY_MS = 250;
717
887
  var DEFAULT_MAX_RETRY_DELAY_MS = 8e3;
718
888
  var DEFAULT_SPLIT_BATCH_SIZE = 50;
719
- function isRecord2(value) {
889
+ function isRecord4(value) {
720
890
  return typeof value === "object" && value !== null && !Array.isArray(value);
721
891
  }
722
892
  function collectAtPath(data, path) {
@@ -724,7 +894,7 @@ function collectAtPath(data, path) {
724
894
  for (const segment of path) {
725
895
  const next = [];
726
896
  for (const value of current) {
727
- if (isRecord2(value)) {
897
+ if (isRecord4(value)) {
728
898
  const child = value[segment];
729
899
  if (Array.isArray(child)) {
730
900
  next.push(...child);
@@ -735,14 +905,14 @@ function collectAtPath(data, path) {
735
905
  }
736
906
  current = next;
737
907
  }
738
- return current.filter(isRecord2);
908
+ return current.filter(isRecord4);
739
909
  }
740
910
  function resolveContext(options) {
741
911
  const env = readEnvSettings();
742
912
  const space = options.space ?? env.space;
743
913
  const environment = options.environment ?? env.environment ?? "master";
744
914
  const preview = options.preview ?? false;
745
- const token = preview ? options.previewToken ?? options.token ?? env.previewToken : options.deliveryToken ?? options.token ?? env.deliveryToken;
915
+ const token = preview ? options.previewToken ?? env.previewToken : options.deliveryToken ?? env.deliveryToken;
746
916
  if (!space || !token) {
747
917
  const missing = [];
748
918
  if (!space) {
@@ -776,8 +946,13 @@ function resolveContext(options) {
776
946
  fetch: fetchImpl,
777
947
  retry,
778
948
  autoSplitNestedCollections: options.autoSplitNestedCollections ?? true,
779
- splitBatchSize: options.splitBatchSize ?? DEFAULT_SPLIT_BATCH_SIZE
949
+ splitBatchSize: options.splitBatchSize ?? DEFAULT_SPLIT_BATCH_SIZE,
950
+ annotateQueryOnError: options.annotateQueryOnError ?? true,
951
+ unresolvableLinks: options.unresolvableLinks ?? "omit"
780
952
  };
953
+ if (options.onUnresolvableLink) {
954
+ context.onUnresolvableLink = options.onUnresolvableLink;
955
+ }
781
956
  if (options.signal) context.signal = options.signal;
782
957
  if (options.next) context.next = options.next;
783
958
  if (options.cache) context.cache = options.cache;
@@ -795,7 +970,7 @@ async function resolvePlan(plan, data, variables, context) {
795
970
  for (const parent of relevant) {
796
971
  const typename = parent[TYPENAME_ALIAS];
797
972
  const sys = parent[SYS_ID_ALIAS];
798
- if (!isRecord2(sys) || typeof sys["id"] !== "string") {
973
+ if (!isRecord4(sys) || typeof sys["id"] !== "string") {
799
974
  throw new FetchContentfulError(
800
975
  `Cannot split "${plan.responseKey}": a parent entry is missing its sys id.`,
801
976
  { code: "STITCH" }
@@ -818,19 +993,14 @@ async function resolvePlan(plan, data, variables, context) {
818
993
  ];
819
994
  await Promise.all(
820
995
  chunk(ids, context.splitBatchSize).map(async (idBatch) => {
821
- const { document, rootKey } = buildSubquery(
822
- plan,
823
- typename,
824
- idBatch.length,
825
- context
826
- );
996
+ const { document, rootKey } = buildSubquery(plan, typename, idBatch.length, context);
827
997
  const subData = await executeDocument(
828
998
  document,
829
999
  { ...variables, _splitIds: idBatch },
830
1000
  context
831
1001
  );
832
1002
  const collection = subData[rootKey];
833
- const items = isRecord2(collection) && Array.isArray(collection["items"]) ? collection["items"] : void 0;
1003
+ const items = isRecord4(collection) && Array.isArray(collection["items"]) ? collection["items"] : void 0;
834
1004
  if (!items) {
835
1005
  throw new FetchContentfulError(
836
1006
  `Split subquery for "${typename}" returned no "${rootKey}.items". Check that the content type follows Contentful naming conventions.`,
@@ -838,9 +1008,9 @@ async function resolvePlan(plan, data, variables, context) {
838
1008
  );
839
1009
  }
840
1010
  for (const item of items) {
841
- if (isRecord2(item)) {
1011
+ if (isRecord4(item)) {
842
1012
  const sys = item["sys"];
843
- if (isRecord2(sys) && typeof sys["id"] === "string") {
1013
+ if (isRecord4(sys) && typeof sys["id"] === "string") {
844
1014
  resolved.set(`${typename}:${sys["id"]}`, item[plan.responseKey]);
845
1015
  }
846
1016
  }
@@ -873,9 +1043,7 @@ function cleanupMarkers(data, plans) {
873
1043
  async function executeDocument(document, variables, context) {
874
1044
  const { document: outer, plans } = planSplits(document, context);
875
1045
  const data = await rawRequest(outer, variables, context);
876
- await Promise.all(
877
- plans.map((plan) => resolvePlan(plan, data, variables, context))
878
- );
1046
+ await Promise.all(plans.map((plan) => resolvePlan(plan, data, variables, context)));
879
1047
  cleanupMarkers(data, plans);
880
1048
  return data;
881
1049
  }
@@ -889,10 +1057,10 @@ function toDocument(query) {
889
1057
  try {
890
1058
  return parse(typeof query === "string" ? query : String(query));
891
1059
  } catch (cause) {
892
- throw new FetchContentfulError(
893
- `Failed to parse GraphQL query: ${String(cause)}`,
894
- { code: "CONFIG", cause }
895
- );
1060
+ throw new FetchContentfulError(`Failed to parse GraphQL query: ${String(cause)}`, {
1061
+ code: "CONFIG",
1062
+ cause
1063
+ });
896
1064
  }
897
1065
  }
898
1066
  function withAutoVariables(document, variables, context) {
@@ -928,13 +1096,10 @@ async function runFetchContentful(query, options) {
928
1096
  );
929
1097
  }
930
1098
  }
931
- const variables = withAutoVariables(
932
- document,
933
- options.variables ?? {},
934
- context
935
- );
1099
+ const variables = withAutoVariables(document, options.variables ?? {}, context);
936
1100
  const data = await executeDocument(document, variables, context);
937
- const result = options.shapeResponseData === false ? data : shapeData(data);
1101
+ const resolved = context.unresolvableLinks === "null" ? data : omitUnresolvedLinks(data);
1102
+ const result = options.shapeResponseData === false ? resolved : shapeData(resolved);
938
1103
  if (options.unwrapRootField === false) {
939
1104
  return result;
940
1105
  }
@@ -954,6 +1119,6 @@ function createFetchContentful(defaults = {}) {
954
1119
  }
955
1120
  var index_default = fetchContentful;
956
1121
 
957
- export { FetchContentfulError, clearLocaleCache, collectAtPath, createFetchContentful, index_default as default, fetchContentful, getLocales, injectRootArgs, inlineFragments, isFetchContentfulError, readEnvSettings, shapeData, unwrapSingleRoot };
1122
+ export { FetchContentfulError, UNRESOLVABLE_LINK_CODE, annotateQuery, clearLocaleCache, collectAtPath, createFetchContentful, index_default as default, fetchContentful, getLocales, injectRootArgs, inlineFragments, isFetchContentfulError, isUnresolvableLinkError, omitUnresolvedLinks, partitionUnresolvableLinks, readEnvSettings, shapeData, unwrapSingleRoot };
958
1123
  //# sourceMappingURL=index.mjs.map
959
1124
  //# sourceMappingURL=index.mjs.map