@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.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
  }
@@ -28,6 +130,48 @@ var FetchContentfulError = class extends Error {
28
130
  function isFetchContentfulError(value) {
29
131
  return value instanceof FetchContentfulError;
30
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
+ }
31
175
 
32
176
  // src/retry.ts
33
177
  function defaultSleep(ms) {
@@ -94,6 +238,9 @@ function withDirective(field, name) {
94
238
  function responseKeyOf(field) {
95
239
  return field.alias?.value ?? field.name.value;
96
240
  }
241
+ function hasResponseKey(selection, key) {
242
+ return selection.kind === graphql.Kind.FIELD && responseKeyOf(selection) === key;
243
+ }
97
244
  function isCollectionField(field) {
98
245
  if (!field.name.value.endsWith(COLLECTION_SUFFIX) || field.name.value === COLLECTION_SUFFIX || !field.selectionSet) {
99
246
  return false;
@@ -136,16 +283,14 @@ function getOperation(document) {
136
283
  );
137
284
  const operation = operations[0];
138
285
  if (!operation || operations.length > 1) {
139
- throw new FetchContentfulError(
140
- "fetch-contentful expects exactly one operation per document.",
141
- { code: "CONFIG" }
142
- );
286
+ throw new FetchContentfulError("fetch-contentful expects exactly one operation per document.", {
287
+ code: "CONFIG"
288
+ });
143
289
  }
144
290
  if (operation.operation !== "query") {
145
- throw new FetchContentfulError(
146
- "fetch-contentful only supports query operations.",
147
- { code: "CONFIG" }
148
- );
291
+ throw new FetchContentfulError("fetch-contentful only supports query operations.", {
292
+ code: "CONFIG"
293
+ });
149
294
  }
150
295
  return operation;
151
296
  }
@@ -157,41 +302,34 @@ function inlineFragments(document) {
157
302
  }
158
303
  }
159
304
  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
- };
305
+ const selections = selectionSet.selections.map((selection) => {
306
+ if (selection.kind === graphql.Kind.FRAGMENT_SPREAD) {
307
+ const name = selection.name.value;
308
+ const fragment = fragments.get(name);
309
+ if (!fragment) {
310
+ throw new FetchContentfulError(`Unknown fragment "${name}" referenced in query.`, {
311
+ code: "CONFIG"
312
+ });
185
313
  }
186
- if (selection.selectionSet) {
187
- return {
188
- ...selection,
189
- selectionSet: inlineSelectionSet(selection.selectionSet, stack)
190
- };
314
+ if (stack.includes(name)) {
315
+ throw new FetchContentfulError(`Fragment cycle detected involving "${name}".`, {
316
+ code: "CONFIG"
317
+ });
191
318
  }
192
- return selection;
319
+ return {
320
+ kind: graphql.Kind.INLINE_FRAGMENT,
321
+ typeCondition: fragment.typeCondition,
322
+ selectionSet: inlineSelectionSet(fragment.selectionSet, [...stack, name])
323
+ };
193
324
  }
194
- );
325
+ if (selection.selectionSet) {
326
+ return {
327
+ ...selection,
328
+ selectionSet: inlineSelectionSet(selection.selectionSet, stack)
329
+ };
330
+ }
331
+ return selection;
332
+ });
195
333
  return { ...selectionSet, selections };
196
334
  }
197
335
  const definitions = document.definitions.filter((definition) => definition.kind !== graphql.Kind.FRAGMENT_DEFINITION).map(
@@ -226,26 +364,23 @@ function planSplits(document, options) {
226
364
  continue;
227
365
  }
228
366
  if (selection.kind !== graphql.Kind.FIELD) {
229
- throw new FetchContentfulError(
230
- "Fragment spreads must be inlined before split planning.",
231
- { code: "CONFIG" }
232
- );
367
+ throw new FetchContentfulError("Fragment spreads must be inlined before split planning.", {
368
+ code: "CONFIG"
369
+ });
233
370
  }
234
371
  const field = selection;
235
372
  const isExplicitSplit = hasDirective(field, SPLIT_DIRECTIVE);
236
373
  const isAutoSplit = options.autoSplitNestedCollections && isCollectionField(field);
374
+ const isForcedSplit = options.forceSplit?.(field) ?? false;
237
375
  if (isExplicitSplit && depth === 0) {
238
376
  throw new FetchContentfulError(
239
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.`,
240
378
  { code: "CONFIG" }
241
379
  );
242
380
  }
243
- 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);
244
382
  if (shouldSplit) {
245
- const planned = withDirective(
246
- withoutDirective(field, SPLIT_DIRECTIVE),
247
- NO_SPLIT_DIRECTIVE
248
- );
383
+ const planned = withDirective(withoutDirective(field, SPLIT_DIRECTIVE), NO_SPLIT_DIRECTIVE);
249
384
  plans.push({
250
385
  path: [...path],
251
386
  responseKey: responseKeyOf(field),
@@ -274,8 +409,11 @@ function planSplits(document, options) {
274
409
  selections.push(field);
275
410
  }
276
411
  }
277
- if (needsMarkers) {
278
- 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());
279
417
  }
280
418
  return { ...selectionSet, selections };
281
419
  }
@@ -293,6 +431,49 @@ function planSplits(document, options) {
293
431
  plans
294
432
  };
295
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
+ }
296
477
  function stringArgument(name, value) {
297
478
  return {
298
479
  kind: graphql.Kind.ARGUMENT,
@@ -344,10 +525,7 @@ function whereIdInArgument() {
344
525
  }
345
526
  function buildSubquery(plan, typename, batchSize, context) {
346
527
  const rootKey = `${lowerFirst(typename)}${COLLECTION_SUFFIX}`;
347
- const args = [
348
- whereIdInArgument(),
349
- intArgument("limit", batchSize)
350
- ];
528
+ const args = [whereIdInArgument(), intArgument("limit", batchSize)];
351
529
  if (context.preview) {
352
530
  args.push(booleanArgument("preview", true));
353
531
  }
@@ -356,10 +534,7 @@ function buildSubquery(plan, typename, batchSize, context) {
356
534
  }
357
535
  const rootField = {
358
536
  ...namedField(rootKey, void 0, [
359
- namedField("items", void 0, [
360
- namedField("sys", void 0, [namedField("id")]),
361
- plan.field
362
- ])
537
+ namedField("items", void 0, [namedField("sys", void 0, [namedField("id")]), plan.field])
363
538
  ]),
364
539
  arguments: args
365
540
  };
@@ -409,12 +584,68 @@ function stripInternalDirectives(document) {
409
584
  });
410
585
  }
411
586
 
587
+ // src/unresolvable.ts
588
+ var UNRESOLVABLE_LINK_CODE = "UNRESOLVABLE_LINK";
589
+ var COLLECTION_SUFFIX2 = "Collection";
590
+ function isRecord(value) {
591
+ return typeof value === "object" && value !== null && !Array.isArray(value);
592
+ }
593
+ function isCollectionValue(value) {
594
+ return isRecord(value) && Array.isArray(value["items"]);
595
+ }
596
+ function isCollectionKey(key) {
597
+ return key.endsWith(COLLECTION_SUFFIX2) && key !== COLLECTION_SUFFIX2;
598
+ }
599
+ function isUnresolvableLinkError(error) {
600
+ const contentful = error.extensions?.["contentful"];
601
+ return isRecord(contentful) && contentful["code"] === UNRESOLVABLE_LINK_CODE;
602
+ }
603
+ function partitionUnresolvableLinks(errors) {
604
+ const unresolvable = [];
605
+ const fatal = [];
606
+ for (const error of errors) {
607
+ if (isUnresolvableLinkError(error)) {
608
+ unresolvable.push(error);
609
+ } else {
610
+ fatal.push(error);
611
+ }
612
+ }
613
+ return { unresolvable, fatal };
614
+ }
615
+ function walk(value) {
616
+ if (Array.isArray(value)) {
617
+ return value.map(walk);
618
+ }
619
+ if (isRecord(value)) {
620
+ const result = {};
621
+ for (const [key, child] of Object.entries(value)) {
622
+ result[key] = isCollectionKey(key) && isCollectionValue(child) ? stripCollection(child) : walk(child);
623
+ }
624
+ return result;
625
+ }
626
+ return value;
627
+ }
628
+ function stripCollection(collection) {
629
+ const result = {};
630
+ for (const [key, value] of Object.entries(collection)) {
631
+ result[key] = key === "items" ? value.filter((item) => item !== null && item !== void 0).map(walk) : walk(value);
632
+ }
633
+ return result;
634
+ }
635
+ function omitUnresolvedLinks(data) {
636
+ return walk(data);
637
+ }
638
+
412
639
  // src/client.ts
413
640
  function graphqlEndpoint(space, environment) {
414
641
  return `https://graphql.contentful.com/content/v1/spaces/${encodeURIComponent(
415
642
  space
416
643
  )}/environments/${encodeURIComponent(environment)}`;
417
644
  }
645
+ function printOutgoingQuery(document, context) {
646
+ const printed = graphql.print(stripInternalDirectives(document));
647
+ return context.minifyQuery === false ? printed : minifyQuery(printed);
648
+ }
418
649
  function parseRetryAfter(header) {
419
650
  if (!header) return void 0;
420
651
  const seconds = Number(header);
@@ -444,14 +675,47 @@ function pickDeclaredVariables(document, variables) {
444
675
  function isRetryableStatus(status) {
445
676
  return status === 408 || status === 429 || status >= 500;
446
677
  }
678
+ function isRecord2(value) {
679
+ return typeof value === "object" && value !== null && !Array.isArray(value);
680
+ }
681
+ function reportUnresolvableLinks(errors, context) {
682
+ if (!context.onUnresolvableLink) return;
683
+ try {
684
+ context.onUnresolvableLink(errors);
685
+ } catch {
686
+ }
687
+ }
688
+ async function readGraphQLErrors(response) {
689
+ try {
690
+ const payload = await response.json();
691
+ const errors = payload?.errors;
692
+ return Array.isArray(errors) && errors.length > 0 ? errors : void 0;
693
+ } catch {
694
+ return void 0;
695
+ }
696
+ }
697
+ function describeFailure(summary, errors, query, context) {
698
+ if (!errors || errors.length === 0) return summary;
699
+ const message = `${summary} ${errors.map((error) => error.message).join("; ")}`;
700
+ if (context.annotateQueryOnError === false) return message;
701
+ const annotated = annotateQuery(query, errors);
702
+ return annotated ? `${message}
703
+
704
+ ${annotated}
705
+ ` : message;
706
+ }
447
707
  async function rawRequest(document, variables, context) {
448
- const query = graphql.print(stripInternalDirectives(document));
449
- const body = JSON.stringify({
450
- query,
451
- variables: pickDeclaredVariables(document, variables)
452
- });
708
+ const query = printOutgoingQuery(document, context);
709
+ const pickedVariables = pickDeclaredVariables(document, variables);
453
710
  const url = graphqlEndpoint(context.space, context.environment);
454
- 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
+ });
455
719
  let response;
456
720
  try {
457
721
  const init = {
@@ -469,16 +733,24 @@ async function rawRequest(document, variables, context) {
469
733
  } catch (cause) {
470
734
  throw new FetchContentfulError(
471
735
  `Network error while contacting Contentful: ${String(cause)}`,
472
- { code: "NETWORK", retryable: true, cause }
736
+ { code: "NETWORK", query, retryable: true, cause }
473
737
  );
474
738
  }
475
739
  if (!response.ok) {
476
740
  const retryable = isRetryableStatus(response.status);
741
+ const errors = retryable ? void 0 : await readGraphQLErrors(response);
477
742
  throw new FetchContentfulError(
478
- `Contentful responded with HTTP ${response.status}.`,
743
+ describeFailure(
744
+ `Contentful responded with HTTP ${response.status}.`,
745
+ errors,
746
+ query,
747
+ context
748
+ ),
479
749
  {
480
750
  code: "HTTP",
481
751
  status: response.status,
752
+ errors,
753
+ query,
482
754
  retryable,
483
755
  retryAfterMs: retryable ? parseRetryAfter(response.headers.get("Retry-After")) : void 0
484
756
  }
@@ -488,24 +760,46 @@ async function rawRequest(document, variables, context) {
488
760
  try {
489
761
  payload = await response.json();
490
762
  } catch (cause) {
491
- throw new FetchContentfulError(
492
- "Contentful returned an unreadable response body.",
493
- { code: "NETWORK", retryable: true, cause }
494
- );
763
+ throw new FetchContentfulError("Contentful returned an unreadable response body.", {
764
+ code: "NETWORK",
765
+ query,
766
+ retryable: true,
767
+ cause
768
+ });
495
769
  }
496
770
  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
- );
771
+ const { unresolvable, fatal } = partitionUnresolvableLinks(payload.errors);
772
+ const tolerable = context.unresolvableLinks !== "error" && fatal.length === 0 && unresolvable.length > 0 && isRecord2(payload.data);
773
+ if (!tolerable) {
774
+ throw new FetchContentfulError(
775
+ describeFailure("Contentful returned GraphQL errors:", payload.errors, query, context),
776
+ { code: "GRAPHQL", errors: payload.errors, query }
777
+ );
778
+ }
779
+ reportUnresolvableLinks(unresolvable, context);
501
780
  }
502
781
  if (!payload.data || typeof payload.data !== "object") {
503
- throw new FetchContentfulError(
504
- "Contentful returned no data and no errors.",
505
- { code: "GRAPHQL" }
506
- );
782
+ throw new FetchContentfulError("Contentful returned no data and no errors.", {
783
+ code: "GRAPHQL",
784
+ query
785
+ });
507
786
  }
508
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
+ }
509
803
  }, context.retry);
510
804
  }
511
805
 
@@ -519,7 +813,6 @@ function readEnvSettings() {
519
813
  space: void 0,
520
814
  environment: void 0,
521
815
  deliveryToken: void 0,
522
- token: void 0,
523
816
  previewToken: void 0
524
817
  };
525
818
  }
@@ -534,7 +827,6 @@ function readEnvSettings() {
534
827
  process.env.CONTENTFUL_ENVIRONMENT || process.env.NEXT_PUBLIC_CONTENTFUL_ENVIRONMENT
535
828
  ),
536
829
  deliveryToken,
537
- token: deliveryToken,
538
830
  // No `NEXT_PUBLIC_` fallback: see the note at the top of this file.
539
831
  previewToken: orUndefined(process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN)
540
832
  };
@@ -576,20 +868,18 @@ function injectIntoField(field, args) {
576
868
  return { ...field, arguments: [...argumentsOf(field), ...additions] };
577
869
  }
578
870
  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;
871
+ const selections = selectionSet.selections.map((selection) => {
872
+ if (selection.kind === graphql.Kind.FIELD) {
873
+ return injectIntoField(selection, args);
591
874
  }
592
- );
875
+ if (selection.kind === graphql.Kind.INLINE_FRAGMENT) {
876
+ return {
877
+ ...selection,
878
+ selectionSet: injectIntoSelectionSet(selection.selectionSet, args)
879
+ };
880
+ }
881
+ return selection;
882
+ });
593
883
  return { ...selectionSet, selections };
594
884
  }
595
885
  function injectRootArgs(document, args) {
@@ -630,27 +920,24 @@ async function fetchLocales(context) {
630
920
  if (context.signal) init.signal = context.signal;
631
921
  response = await context.fetch(localesEndpoint(context), init);
632
922
  } catch (cause) {
633
- throw new FetchContentfulError(
634
- `Network error while fetching locales: ${String(cause)}`,
635
- { code: "NETWORK", retryable: true, cause }
636
- );
923
+ throw new FetchContentfulError(`Network error while fetching locales: ${String(cause)}`, {
924
+ code: "NETWORK",
925
+ retryable: true,
926
+ cause
927
+ });
637
928
  }
638
929
  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
- );
930
+ throw new FetchContentfulError(`Locale request failed with HTTP ${response.status}.`, {
931
+ code: "HTTP",
932
+ status: response.status,
933
+ retryable: response.status === 429 || response.status >= 500
934
+ });
647
935
  }
648
936
  const payload = await response.json();
649
937
  if (!Array.isArray(payload.items)) {
650
- throw new FetchContentfulError(
651
- "Locale response did not include an items array.",
652
- { code: "NETWORK" }
653
- );
938
+ throw new FetchContentfulError("Locale response did not include an items array.", {
939
+ code: "NETWORK"
940
+ });
654
941
  }
655
942
  return payload.items.map((item) => ({
656
943
  code: item.code,
@@ -678,24 +965,22 @@ function clearLocaleCache() {
678
965
  }
679
966
 
680
967
  // src/shape.ts
681
- var COLLECTION_SUFFIX2 = "Collection";
682
- function isRecord(value) {
968
+ var COLLECTION_SUFFIX3 = "Collection";
969
+ function isRecord3(value) {
683
970
  return typeof value === "object" && value !== null && !Array.isArray(value);
684
971
  }
685
- function isCollectionValue(value) {
686
- return isRecord(value) && Array.isArray(value["items"]);
972
+ function isCollectionValue2(value) {
973
+ return isRecord3(value) && Array.isArray(value["items"]);
687
974
  }
688
975
  function shapeData(data) {
689
976
  if (Array.isArray(data)) {
690
977
  return data.map((item) => shapeData(item));
691
978
  }
692
- if (isRecord(data)) {
979
+ if (isRecord3(data)) {
693
980
  const shaped = {};
694
981
  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
- );
982
+ if (key.endsWith(COLLECTION_SUFFIX3) && key !== COLLECTION_SUFFIX3 && isCollectionValue2(value)) {
983
+ shaped[key.slice(0, -COLLECTION_SUFFIX3.length)] = shapeData(value.items);
699
984
  } else {
700
985
  shaped[key] = shapeData(value);
701
986
  }
@@ -705,7 +990,7 @@ function shapeData(data) {
705
990
  return data;
706
991
  }
707
992
  function unwrapSingleRoot(data) {
708
- if (isRecord(data)) {
993
+ if (isRecord3(data)) {
709
994
  const keys = Object.keys(data);
710
995
  if (keys.length === 1) {
711
996
  return data[keys[0]];
@@ -720,7 +1005,9 @@ var DEFAULT_RETRIES = 5;
720
1005
  var DEFAULT_RETRY_DELAY_MS = 250;
721
1006
  var DEFAULT_MAX_RETRY_DELAY_MS = 8e3;
722
1007
  var DEFAULT_SPLIT_BATCH_SIZE = 50;
723
- function isRecord2(value) {
1008
+ var DEFAULT_MAX_QUERY_SIZE = 7500;
1009
+ var DEFAULT_MAX_QUERY_SIZE_WITH_APQ = 15500;
1010
+ function isRecord4(value) {
724
1011
  return typeof value === "object" && value !== null && !Array.isArray(value);
725
1012
  }
726
1013
  function collectAtPath(data, path) {
@@ -728,7 +1015,7 @@ function collectAtPath(data, path) {
728
1015
  for (const segment of path) {
729
1016
  const next = [];
730
1017
  for (const value of current) {
731
- if (isRecord2(value)) {
1018
+ if (isRecord4(value)) {
732
1019
  const child = value[segment];
733
1020
  if (Array.isArray(child)) {
734
1021
  next.push(...child);
@@ -739,14 +1026,14 @@ function collectAtPath(data, path) {
739
1026
  }
740
1027
  current = next;
741
1028
  }
742
- return current.filter(isRecord2);
1029
+ return current.filter(isRecord4);
743
1030
  }
744
1031
  function resolveContext(options) {
745
1032
  const env = readEnvSettings();
746
1033
  const space = options.space ?? env.space;
747
1034
  const environment = options.environment ?? env.environment ?? "master";
748
1035
  const preview = options.preview ?? false;
749
- const token = preview ? options.previewToken ?? options.token ?? env.previewToken : options.deliveryToken ?? options.token ?? env.deliveryToken;
1036
+ const token = preview ? options.previewToken ?? env.previewToken : options.deliveryToken ?? env.deliveryToken;
750
1037
  if (!space || !token) {
751
1038
  const missing = [];
752
1039
  if (!space) {
@@ -770,6 +1057,7 @@ function resolveContext(options) {
770
1057
  maxDelayMs: options.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS
771
1058
  };
772
1059
  const fetchImpl = options.fetch ?? globalThis.fetch;
1060
+ const automaticPersistedQueries = options.automaticPersistedQueries ?? false;
773
1061
  const context = {
774
1062
  space,
775
1063
  environment,
@@ -780,8 +1068,19 @@ function resolveContext(options) {
780
1068
  fetch: fetchImpl,
781
1069
  retry,
782
1070
  autoSplitNestedCollections: options.autoSplitNestedCollections ?? true,
783
- splitBatchSize: options.splitBatchSize ?? DEFAULT_SPLIT_BATCH_SIZE
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),
1075
+ splitBatchSize: options.splitBatchSize ?? DEFAULT_SPLIT_BATCH_SIZE,
1076
+ annotateQueryOnError: options.annotateQueryOnError ?? true,
1077
+ unresolvableLinks: options.unresolvableLinks ?? "omit",
1078
+ minifyQuery: options.minifyQuery ?? true,
1079
+ automaticPersistedQueries
784
1080
  };
1081
+ if (options.onUnresolvableLink) {
1082
+ context.onUnresolvableLink = options.onUnresolvableLink;
1083
+ }
785
1084
  if (options.signal) context.signal = options.signal;
786
1085
  if (options.next) context.next = options.next;
787
1086
  if (options.cache) context.cache = options.cache;
@@ -799,7 +1098,7 @@ async function resolvePlan(plan, data, variables, context) {
799
1098
  for (const parent of relevant) {
800
1099
  const typename = parent[TYPENAME_ALIAS];
801
1100
  const sys = parent[SYS_ID_ALIAS];
802
- if (!isRecord2(sys) || typeof sys["id"] !== "string") {
1101
+ if (!isRecord4(sys) || typeof sys["id"] !== "string") {
803
1102
  throw new FetchContentfulError(
804
1103
  `Cannot split "${plan.responseKey}": a parent entry is missing its sys id.`,
805
1104
  { code: "STITCH" }
@@ -822,19 +1121,14 @@ async function resolvePlan(plan, data, variables, context) {
822
1121
  ];
823
1122
  await Promise.all(
824
1123
  chunk(ids, context.splitBatchSize).map(async (idBatch) => {
825
- const { document, rootKey } = buildSubquery(
826
- plan,
827
- typename,
828
- idBatch.length,
829
- context
830
- );
1124
+ const { document, rootKey } = buildSubquery(plan, typename, idBatch.length, context);
831
1125
  const subData = await executeDocument(
832
1126
  document,
833
1127
  { ...variables, _splitIds: idBatch },
834
1128
  context
835
1129
  );
836
1130
  const collection = subData[rootKey];
837
- const items = isRecord2(collection) && Array.isArray(collection["items"]) ? collection["items"] : void 0;
1131
+ const items = isRecord4(collection) && Array.isArray(collection["items"]) ? collection["items"] : void 0;
838
1132
  if (!items) {
839
1133
  throw new FetchContentfulError(
840
1134
  `Split subquery for "${typename}" returned no "${rootKey}.items". Check that the content type follows Contentful naming conventions.`,
@@ -842,9 +1136,9 @@ async function resolvePlan(plan, data, variables, context) {
842
1136
  );
843
1137
  }
844
1138
  for (const item of items) {
845
- if (isRecord2(item)) {
1139
+ if (isRecord4(item)) {
846
1140
  const sys = item["sys"];
847
- if (isRecord2(sys) && typeof sys["id"] === "string") {
1141
+ if (isRecord4(sys) && typeof sys["id"] === "string") {
848
1142
  resolved.set(`${typename}:${sys["id"]}`, item[plan.responseKey]);
849
1143
  }
850
1144
  }
@@ -874,12 +1168,18 @@ function cleanupMarkers(data, plans) {
874
1168
  }
875
1169
  }
876
1170
  }
1171
+ function byteLength(text) {
1172
+ return new TextEncoder().encode(text).length;
1173
+ }
877
1174
  async function executeDocument(document, variables, context) {
878
- 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
+ });
879
1181
  const data = await rawRequest(outer, variables, context);
880
- await Promise.all(
881
- plans.map((plan) => resolvePlan(plan, data, variables, context))
882
- );
1182
+ await Promise.all(plans.map((plan) => resolvePlan(plan, data, variables, context)));
883
1183
  cleanupMarkers(data, plans);
884
1184
  return data;
885
1185
  }
@@ -893,10 +1193,10 @@ function toDocument(query) {
893
1193
  try {
894
1194
  return graphql.parse(typeof query === "string" ? query : String(query));
895
1195
  } catch (cause) {
896
- throw new FetchContentfulError(
897
- `Failed to parse GraphQL query: ${String(cause)}`,
898
- { code: "CONFIG", cause }
899
- );
1196
+ throw new FetchContentfulError(`Failed to parse GraphQL query: ${String(cause)}`, {
1197
+ code: "CONFIG",
1198
+ cause
1199
+ });
900
1200
  }
901
1201
  }
902
1202
  function withAutoVariables(document, variables, context) {
@@ -932,13 +1232,10 @@ async function runFetchContentful(query, options) {
932
1232
  );
933
1233
  }
934
1234
  }
935
- const variables = withAutoVariables(
936
- document,
937
- options.variables ?? {},
938
- context
939
- );
1235
+ const variables = withAutoVariables(document, options.variables ?? {}, context);
940
1236
  const data = await executeDocument(document, variables, context);
941
- const result = options.shapeResponseData === false ? data : shapeData(data);
1237
+ const resolved = context.unresolvableLinks === "null" ? data : omitUnresolvedLinks(data);
1238
+ const result = options.shapeResponseData === false ? resolved : shapeData(resolved);
942
1239
  if (options.unwrapRootField === false) {
943
1240
  return result;
944
1241
  }
@@ -959,6 +1256,8 @@ function createFetchContentful(defaults = {}) {
959
1256
  var index_default = fetchContentful;
960
1257
 
961
1258
  exports.FetchContentfulError = FetchContentfulError;
1259
+ exports.UNRESOLVABLE_LINK_CODE = UNRESOLVABLE_LINK_CODE;
1260
+ exports.annotateQuery = annotateQuery;
962
1261
  exports.clearLocaleCache = clearLocaleCache;
963
1262
  exports.collectAtPath = collectAtPath;
964
1263
  exports.createFetchContentful = createFetchContentful;
@@ -968,6 +1267,10 @@ exports.getLocales = getLocales;
968
1267
  exports.injectRootArgs = injectRootArgs;
969
1268
  exports.inlineFragments = inlineFragments;
970
1269
  exports.isFetchContentfulError = isFetchContentfulError;
1270
+ exports.isUnresolvableLinkError = isUnresolvableLinkError;
1271
+ exports.minifyQuery = minifyQuery;
1272
+ exports.omitUnresolvedLinks = omitUnresolvedLinks;
1273
+ exports.partitionUnresolvableLinks = partitionUnresolvableLinks;
971
1274
  exports.readEnvSettings = readEnvSettings;
972
1275
  exports.shapeData = shapeData;
973
1276
  exports.unwrapSingleRoot = unwrapSingleRoot;