@fourtwelvelabs/fetch-contentful 0.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 ADDED
@@ -0,0 +1,977 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var graphql = require('graphql');
6
+
7
+ // src/index.ts
8
+
9
+ // src/errors.ts
10
+ var FetchContentfulError = class extends Error {
11
+ code;
12
+ status;
13
+ errors;
14
+ /** Internal: whether a retry may succeed. */
15
+ retryable;
16
+ /** Internal: server-requested retry delay (from Retry-After), in ms. */
17
+ retryAfterMs;
18
+ constructor(message, options) {
19
+ super(message, { cause: options.cause });
20
+ this.name = "FetchContentfulError";
21
+ this.code = options.code;
22
+ this.status = options.status;
23
+ this.errors = options.errors;
24
+ this.retryable = options.retryable ?? false;
25
+ this.retryAfterMs = options.retryAfterMs;
26
+ }
27
+ };
28
+ function isFetchContentfulError(value) {
29
+ return value instanceof FetchContentfulError;
30
+ }
31
+
32
+ // src/retry.ts
33
+ function defaultSleep(ms) {
34
+ return new Promise((resolve) => setTimeout(resolve, ms));
35
+ }
36
+ function backoffDelay(attempt, config) {
37
+ const exponential = config.baseDelayMs * 2 ** attempt;
38
+ const jitter = config.random() * config.baseDelayMs;
39
+ return Math.min(exponential + jitter, config.maxDelayMs);
40
+ }
41
+ async function withRetries(fn, config) {
42
+ const random = config.random ?? Math.random;
43
+ const sleep = config.sleep ?? defaultSleep;
44
+ let attempt = 0;
45
+ while (true) {
46
+ try {
47
+ return await fn(attempt);
48
+ } catch (error) {
49
+ const retryable = isFetchContentfulError(error) && error.retryable;
50
+ if (!retryable || attempt >= config.retries) {
51
+ throw error;
52
+ }
53
+ const delay = error.retryAfterMs ?? backoffDelay(attempt, { ...config, random });
54
+ await sleep(delay);
55
+ attempt += 1;
56
+ }
57
+ }
58
+ }
59
+ var SPLIT_DIRECTIVE = "split";
60
+ var NO_SPLIT_DIRECTIVE = "_noSplit";
61
+ var SYS_ID_ALIAS = "_splitSysId";
62
+ var TYPENAME_ALIAS = "_splitTypename";
63
+ var IDS_VARIABLE = "_splitIds";
64
+ var COLLECTION_SUFFIX = "Collection";
65
+ function lowerFirst(value) {
66
+ return value.charAt(0).toLowerCase() + value.slice(1);
67
+ }
68
+ function chunk(items, size) {
69
+ const chunks = [];
70
+ for (let index = 0; index < items.length; index += size) {
71
+ chunks.push(items.slice(index, index + size));
72
+ }
73
+ return chunks;
74
+ }
75
+ function directivesOf(field) {
76
+ return field.directives ?? [];
77
+ }
78
+ function hasDirective(field, name) {
79
+ return directivesOf(field).some((d) => d.name.value === name);
80
+ }
81
+ function withoutDirective(field, name) {
82
+ return {
83
+ ...field,
84
+ directives: directivesOf(field).filter((d) => d.name.value !== name)
85
+ };
86
+ }
87
+ function withDirective(field, name) {
88
+ const directive = {
89
+ kind: graphql.Kind.DIRECTIVE,
90
+ name: { kind: graphql.Kind.NAME, value: name }
91
+ };
92
+ return { ...field, directives: [...directivesOf(field), directive] };
93
+ }
94
+ function responseKeyOf(field) {
95
+ return field.alias?.value ?? field.name.value;
96
+ }
97
+ function isCollectionField(field) {
98
+ if (!field.name.value.endsWith(COLLECTION_SUFFIX) || field.name.value === COLLECTION_SUFFIX || !field.selectionSet) {
99
+ return false;
100
+ }
101
+ return field.selectionSet.selections.some(
102
+ (selection) => selection.kind === graphql.Kind.FIELD && selection.name.value === "items"
103
+ );
104
+ }
105
+ function namedField(name, alias, selections) {
106
+ return {
107
+ kind: graphql.Kind.FIELD,
108
+ name: { kind: graphql.Kind.NAME, value: name },
109
+ ...alias ? { alias: { kind: graphql.Kind.NAME, value: alias } } : {},
110
+ ...selections ? {
111
+ selectionSet: {
112
+ kind: graphql.Kind.SELECTION_SET,
113
+ selections
114
+ }
115
+ } : {}
116
+ };
117
+ }
118
+ function sysIdMarker() {
119
+ return namedField("sys", SYS_ID_ALIAS, [namedField("id")]);
120
+ }
121
+ function typenameMarker() {
122
+ return namedField("__typename", TYPENAME_ALIAS);
123
+ }
124
+ function collectVariableNames(field) {
125
+ const names = /* @__PURE__ */ new Set();
126
+ graphql.visit(field, {
127
+ Variable(node) {
128
+ names.add(node.name.value);
129
+ }
130
+ });
131
+ return names;
132
+ }
133
+ function getOperation(document) {
134
+ const operations = document.definitions.filter(
135
+ (definition) => definition.kind === graphql.Kind.OPERATION_DEFINITION
136
+ );
137
+ const operation = operations[0];
138
+ if (!operation || operations.length > 1) {
139
+ throw new FetchContentfulError(
140
+ "fetch-contentful expects exactly one operation per document.",
141
+ { code: "CONFIG" }
142
+ );
143
+ }
144
+ if (operation.operation !== "query") {
145
+ throw new FetchContentfulError(
146
+ "fetch-contentful only supports query operations.",
147
+ { code: "CONFIG" }
148
+ );
149
+ }
150
+ return operation;
151
+ }
152
+ function inlineFragments(document) {
153
+ const fragments = /* @__PURE__ */ new Map();
154
+ for (const definition of document.definitions) {
155
+ if (definition.kind === graphql.Kind.FRAGMENT_DEFINITION) {
156
+ fragments.set(definition.name.value, definition);
157
+ }
158
+ }
159
+ 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
+ };
185
+ }
186
+ if (selection.selectionSet) {
187
+ return {
188
+ ...selection,
189
+ selectionSet: inlineSelectionSet(selection.selectionSet, stack)
190
+ };
191
+ }
192
+ return selection;
193
+ }
194
+ );
195
+ return { ...selectionSet, selections };
196
+ }
197
+ const definitions = document.definitions.filter((definition) => definition.kind !== graphql.Kind.FRAGMENT_DEFINITION).map(
198
+ (definition) => definition.kind === graphql.Kind.OPERATION_DEFINITION ? {
199
+ ...definition,
200
+ selectionSet: inlineSelectionSet(definition.selectionSet, [])
201
+ } : definition
202
+ );
203
+ return { ...document, definitions };
204
+ }
205
+ function planSplits(document, options) {
206
+ const operation = getOperation(document);
207
+ const variableDefinitions = operation.variableDefinitions ?? [];
208
+ const plans = [];
209
+ function processSelectionSet(selectionSet, path, depth, typeCondition) {
210
+ const selections = [];
211
+ let needsMarkers = false;
212
+ for (const selection of selectionSet.selections) {
213
+ if (selection.kind === graphql.Kind.INLINE_FRAGMENT) {
214
+ const condition = selection.typeCondition?.name.value;
215
+ selections.push({
216
+ ...selection,
217
+ selectionSet: processSelectionSet(
218
+ selection.selectionSet,
219
+ path,
220
+ depth,
221
+ // `Entry` is Contentful's interface type: it never equals a
222
+ // concrete `__typename`, so it must not constrain the plan.
223
+ condition && condition !== "Entry" ? condition : typeCondition
224
+ )
225
+ });
226
+ continue;
227
+ }
228
+ if (selection.kind !== graphql.Kind.FIELD) {
229
+ throw new FetchContentfulError(
230
+ "Fragment spreads must be inlined before split planning.",
231
+ { code: "CONFIG" }
232
+ );
233
+ }
234
+ const field = selection;
235
+ const shouldSplit = depth > 0 && field.selectionSet !== void 0 && !hasDirective(field, NO_SPLIT_DIRECTIVE) && (hasDirective(field, SPLIT_DIRECTIVE) || options.autoSplitNestedCollections && isCollectionField(field));
236
+ if (shouldSplit) {
237
+ const planned = withDirective(
238
+ withoutDirective(field, SPLIT_DIRECTIVE),
239
+ NO_SPLIT_DIRECTIVE
240
+ );
241
+ plans.push({
242
+ path: [...path],
243
+ responseKey: responseKeyOf(field),
244
+ field: planned,
245
+ variableDefinitions: variableDefinitions.filter(
246
+ (definition) => collectVariableNames(field).has(definition.variable.name.value)
247
+ ),
248
+ ...typeCondition !== void 0 ? { typeCondition } : {}
249
+ });
250
+ needsMarkers = true;
251
+ continue;
252
+ }
253
+ if (field.selectionSet) {
254
+ selections.push({
255
+ ...field,
256
+ selectionSet: processSelectionSet(
257
+ field.selectionSet,
258
+ [...path, responseKeyOf(field)],
259
+ depth + 1,
260
+ // Type conditions do not carry across a field boundary: the
261
+ // field's own type governs its children.
262
+ void 0
263
+ )
264
+ });
265
+ } else {
266
+ selections.push(field);
267
+ }
268
+ }
269
+ if (needsMarkers) {
270
+ selections.push(sysIdMarker(), typenameMarker());
271
+ }
272
+ return { ...selectionSet, selections };
273
+ }
274
+ const processedOperation = {
275
+ ...operation,
276
+ selectionSet: processSelectionSet(operation.selectionSet, [], 0, void 0)
277
+ };
278
+ return {
279
+ document: {
280
+ ...document,
281
+ definitions: document.definitions.map(
282
+ (definition) => definition === operation ? processedOperation : definition
283
+ )
284
+ },
285
+ plans
286
+ };
287
+ }
288
+ function stringArgument(name, value) {
289
+ return {
290
+ kind: graphql.Kind.ARGUMENT,
291
+ name: { kind: graphql.Kind.NAME, value: name },
292
+ value: { kind: graphql.Kind.STRING, value }
293
+ };
294
+ }
295
+ function booleanArgument(name, value) {
296
+ return {
297
+ kind: graphql.Kind.ARGUMENT,
298
+ name: { kind: graphql.Kind.NAME, value: name },
299
+ value: { kind: graphql.Kind.BOOLEAN, value }
300
+ };
301
+ }
302
+ function intArgument(name, value) {
303
+ return {
304
+ kind: graphql.Kind.ARGUMENT,
305
+ name: { kind: graphql.Kind.NAME, value: name },
306
+ value: { kind: graphql.Kind.INT, value: String(value) }
307
+ };
308
+ }
309
+ function whereIdInArgument() {
310
+ return {
311
+ kind: graphql.Kind.ARGUMENT,
312
+ name: { kind: graphql.Kind.NAME, value: "where" },
313
+ value: {
314
+ kind: graphql.Kind.OBJECT,
315
+ fields: [
316
+ {
317
+ kind: graphql.Kind.OBJECT_FIELD,
318
+ name: { kind: graphql.Kind.NAME, value: "sys" },
319
+ value: {
320
+ kind: graphql.Kind.OBJECT,
321
+ fields: [
322
+ {
323
+ kind: graphql.Kind.OBJECT_FIELD,
324
+ name: { kind: graphql.Kind.NAME, value: "id_in" },
325
+ value: {
326
+ kind: graphql.Kind.VARIABLE,
327
+ name: { kind: graphql.Kind.NAME, value: IDS_VARIABLE }
328
+ }
329
+ }
330
+ ]
331
+ }
332
+ }
333
+ ]
334
+ }
335
+ };
336
+ }
337
+ function buildSubquery(plan, typename, batchSize, context) {
338
+ const rootKey = `${lowerFirst(typename)}${COLLECTION_SUFFIX}`;
339
+ const args = [
340
+ whereIdInArgument(),
341
+ intArgument("limit", batchSize)
342
+ ];
343
+ if (context.preview) {
344
+ args.push(booleanArgument("preview", true));
345
+ }
346
+ if (context.locale !== void 0) {
347
+ args.push(stringArgument("locale", context.locale));
348
+ }
349
+ const rootField = {
350
+ ...namedField(rootKey, void 0, [
351
+ namedField("items", void 0, [
352
+ namedField("sys", void 0, [namedField("id")]),
353
+ plan.field
354
+ ])
355
+ ]),
356
+ arguments: args
357
+ };
358
+ const idsVariable = {
359
+ kind: graphql.Kind.VARIABLE_DEFINITION,
360
+ variable: {
361
+ kind: graphql.Kind.VARIABLE,
362
+ name: { kind: graphql.Kind.NAME, value: IDS_VARIABLE }
363
+ },
364
+ type: {
365
+ kind: graphql.Kind.NON_NULL_TYPE,
366
+ type: {
367
+ kind: graphql.Kind.LIST_TYPE,
368
+ type: {
369
+ kind: graphql.Kind.NON_NULL_TYPE,
370
+ type: {
371
+ kind: graphql.Kind.NAMED_TYPE,
372
+ name: { kind: graphql.Kind.NAME, value: "String" }
373
+ }
374
+ }
375
+ }
376
+ }
377
+ };
378
+ const operation = {
379
+ kind: graphql.Kind.OPERATION_DEFINITION,
380
+ operation: "query",
381
+ name: { kind: graphql.Kind.NAME, value: "FetchContentfulSplit" },
382
+ variableDefinitions: [idsVariable, ...plan.variableDefinitions],
383
+ selectionSet: {
384
+ kind: graphql.Kind.SELECTION_SET,
385
+ selections: [rootField]
386
+ }
387
+ };
388
+ return {
389
+ document: { kind: graphql.Kind.DOCUMENT, definitions: [operation] },
390
+ rootKey
391
+ };
392
+ }
393
+ function stripInternalDirectives(document) {
394
+ return graphql.visit(document, {
395
+ Directive(node) {
396
+ if (node.name.value === SPLIT_DIRECTIVE || node.name.value === NO_SPLIT_DIRECTIVE) {
397
+ return null;
398
+ }
399
+ return void 0;
400
+ }
401
+ });
402
+ }
403
+
404
+ // src/client.ts
405
+ function graphqlEndpoint(space, environment) {
406
+ return `https://graphql.contentful.com/content/v1/spaces/${encodeURIComponent(
407
+ space
408
+ )}/environments/${encodeURIComponent(environment)}`;
409
+ }
410
+ function parseRetryAfter(header) {
411
+ if (!header) return void 0;
412
+ const seconds = Number(header);
413
+ if (Number.isFinite(seconds)) {
414
+ return Math.max(0, seconds * 1e3);
415
+ }
416
+ const date = Date.parse(header);
417
+ if (Number.isFinite(date)) {
418
+ return Math.max(0, date - Date.now());
419
+ }
420
+ return void 0;
421
+ }
422
+ function pickDeclaredVariables(document, variables) {
423
+ const declared = new Set(
424
+ (getOperation(document).variableDefinitions ?? []).map(
425
+ (definition) => definition.variable.name.value
426
+ )
427
+ );
428
+ const picked = {};
429
+ for (const [name, value] of Object.entries(variables)) {
430
+ if (declared.has(name)) {
431
+ picked[name] = value;
432
+ }
433
+ }
434
+ return picked;
435
+ }
436
+ function isRetryableStatus(status) {
437
+ return status === 408 || status === 429 || status >= 500;
438
+ }
439
+ async function rawRequest(document, variables, context) {
440
+ const query = graphql.print(stripInternalDirectives(document));
441
+ const body = JSON.stringify({
442
+ query,
443
+ variables: pickDeclaredVariables(document, variables)
444
+ });
445
+ const url = graphqlEndpoint(context.space, context.environment);
446
+ return withRetries(async () => {
447
+ let response;
448
+ try {
449
+ const init = {
450
+ method: "POST",
451
+ headers: {
452
+ "Content-Type": "application/json",
453
+ Authorization: `Bearer ${context.token}`
454
+ },
455
+ body
456
+ };
457
+ if (context.signal) init.signal = context.signal;
458
+ if (context.cache) init.cache = context.cache;
459
+ if (context.next) init.next = context.next;
460
+ response = await context.fetch(url, init);
461
+ } catch (cause) {
462
+ throw new FetchContentfulError(
463
+ `Network error while contacting Contentful: ${String(cause)}`,
464
+ { code: "NETWORK", retryable: true, cause }
465
+ );
466
+ }
467
+ if (!response.ok) {
468
+ const retryable = isRetryableStatus(response.status);
469
+ throw new FetchContentfulError(
470
+ `Contentful responded with HTTP ${response.status}.`,
471
+ {
472
+ code: "HTTP",
473
+ status: response.status,
474
+ retryable,
475
+ retryAfterMs: retryable ? parseRetryAfter(response.headers.get("Retry-After")) : void 0
476
+ }
477
+ );
478
+ }
479
+ let payload;
480
+ try {
481
+ payload = await response.json();
482
+ } catch (cause) {
483
+ throw new FetchContentfulError(
484
+ "Contentful returned an unreadable response body.",
485
+ { code: "NETWORK", retryable: true, cause }
486
+ );
487
+ }
488
+ if (payload.errors && payload.errors.length > 0) {
489
+ throw new FetchContentfulError(
490
+ `Contentful returned GraphQL errors: ${payload.errors.map((error) => error.message).join("; ")}`,
491
+ { code: "GRAPHQL", errors: payload.errors }
492
+ );
493
+ }
494
+ if (!payload.data || typeof payload.data !== "object") {
495
+ throw new FetchContentfulError(
496
+ "Contentful returned no data and no errors.",
497
+ { code: "GRAPHQL" }
498
+ );
499
+ }
500
+ return payload.data;
501
+ }, context.retry);
502
+ }
503
+
504
+ // src/env.ts
505
+ function orUndefined(value) {
506
+ return value || void 0;
507
+ }
508
+ function readEnvSettings() {
509
+ if (typeof process === "undefined" || !process.env) {
510
+ return {
511
+ space: void 0,
512
+ environment: void 0,
513
+ token: void 0,
514
+ previewToken: void 0
515
+ };
516
+ }
517
+ return {
518
+ space: orUndefined(
519
+ process.env.CONTENTFUL_SPACE_ID || process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID
520
+ ),
521
+ environment: orUndefined(
522
+ process.env.CONTENTFUL_ENVIRONMENT || process.env.NEXT_PUBLIC_CONTENTFUL_ENVIRONMENT
523
+ ),
524
+ token: orUndefined(
525
+ process.env.CONTENTFUL_ACCESS_TOKEN || process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN
526
+ ),
527
+ previewToken: orUndefined(
528
+ process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN || process.env.NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN
529
+ )
530
+ };
531
+ }
532
+ function argumentsOf(field) {
533
+ return field.arguments ?? [];
534
+ }
535
+ function hasArgument(field, name) {
536
+ return argumentsOf(field).some((argument) => argument.name.value === name);
537
+ }
538
+ function previewArgument() {
539
+ return {
540
+ kind: graphql.Kind.ARGUMENT,
541
+ name: { kind: graphql.Kind.NAME, value: "preview" },
542
+ value: { kind: graphql.Kind.BOOLEAN, value: true }
543
+ };
544
+ }
545
+ function localeArgument(locale) {
546
+ return {
547
+ kind: graphql.Kind.ARGUMENT,
548
+ name: { kind: graphql.Kind.NAME, value: "locale" },
549
+ value: { kind: graphql.Kind.STRING, value: locale }
550
+ };
551
+ }
552
+ function injectIntoField(field, args) {
553
+ if (field.name.value.startsWith("__")) {
554
+ return field;
555
+ }
556
+ const additions = [];
557
+ if (args.preview && !hasArgument(field, "preview")) {
558
+ additions.push(previewArgument());
559
+ }
560
+ if (args.locale !== void 0 && !hasArgument(field, "locale")) {
561
+ additions.push(localeArgument(args.locale));
562
+ }
563
+ if (additions.length === 0) {
564
+ return field;
565
+ }
566
+ return { ...field, arguments: [...argumentsOf(field), ...additions] };
567
+ }
568
+ function injectIntoSelectionSet(selectionSet, args) {
569
+ const selections = selectionSet.selections.map(
570
+ (selection) => {
571
+ if (selection.kind === graphql.Kind.FIELD) {
572
+ return injectIntoField(selection, args);
573
+ }
574
+ if (selection.kind === graphql.Kind.INLINE_FRAGMENT) {
575
+ return {
576
+ ...selection,
577
+ selectionSet: injectIntoSelectionSet(selection.selectionSet, args)
578
+ };
579
+ }
580
+ return selection;
581
+ }
582
+ );
583
+ return { ...selectionSet, selections };
584
+ }
585
+ function injectRootArgs(document, args) {
586
+ if (!args.preview && args.locale === void 0) {
587
+ return document;
588
+ }
589
+ const operation = getOperation(document);
590
+ const injected = {
591
+ ...operation,
592
+ selectionSet: injectIntoSelectionSet(operation.selectionSet, args)
593
+ };
594
+ return {
595
+ ...document,
596
+ definitions: document.definitions.map(
597
+ (definition) => definition === operation ? injected : definition
598
+ )
599
+ };
600
+ }
601
+
602
+ // src/locales.ts
603
+ var cache = /* @__PURE__ */ new Map();
604
+ function cacheKey(context) {
605
+ return `${context.space}:${context.environment}:${context.preview ? 1 : 0}`;
606
+ }
607
+ function localesEndpoint(context) {
608
+ const host = context.preview ? "preview.contentful.com" : "cdn.contentful.com";
609
+ return `https://${host}/spaces/${encodeURIComponent(
610
+ context.space
611
+ )}/environments/${encodeURIComponent(context.environment)}/locales`;
612
+ }
613
+ async function fetchLocales(context) {
614
+ return withRetries(async () => {
615
+ let response;
616
+ try {
617
+ const init = {
618
+ headers: { Authorization: `Bearer ${context.token}` }
619
+ };
620
+ if (context.signal) init.signal = context.signal;
621
+ response = await context.fetch(localesEndpoint(context), init);
622
+ } catch (cause) {
623
+ throw new FetchContentfulError(
624
+ `Network error while fetching locales: ${String(cause)}`,
625
+ { code: "NETWORK", retryable: true, cause }
626
+ );
627
+ }
628
+ if (!response.ok) {
629
+ throw new FetchContentfulError(
630
+ `Locale request failed with HTTP ${response.status}.`,
631
+ {
632
+ code: "HTTP",
633
+ status: response.status,
634
+ retryable: response.status === 429 || response.status >= 500
635
+ }
636
+ );
637
+ }
638
+ const payload = await response.json();
639
+ if (!Array.isArray(payload.items)) {
640
+ throw new FetchContentfulError(
641
+ "Locale response did not include an items array.",
642
+ { code: "NETWORK" }
643
+ );
644
+ }
645
+ return payload.items.map((item) => ({
646
+ code: item.code,
647
+ name: item.name,
648
+ default: item.default,
649
+ fallbackCode: item.fallbackCode
650
+ }));
651
+ }, context.retry);
652
+ }
653
+ function getLocales(context) {
654
+ const key = cacheKey(context);
655
+ const cached = cache.get(key);
656
+ if (cached) {
657
+ return cached;
658
+ }
659
+ const pending = fetchLocales(context).catch((error) => {
660
+ cache.delete(key);
661
+ throw error;
662
+ });
663
+ cache.set(key, pending);
664
+ return pending;
665
+ }
666
+ function clearLocaleCache() {
667
+ cache.clear();
668
+ }
669
+
670
+ // src/shape.ts
671
+ var COLLECTION_SUFFIX2 = "Collection";
672
+ function isRecord(value) {
673
+ return typeof value === "object" && value !== null && !Array.isArray(value);
674
+ }
675
+ function isCollectionValue(value) {
676
+ return isRecord(value) && Array.isArray(value["items"]);
677
+ }
678
+ function shapeData(data) {
679
+ if (Array.isArray(data)) {
680
+ return data.map((item) => shapeData(item));
681
+ }
682
+ if (isRecord(data)) {
683
+ const shaped = {};
684
+ for (const [key, value] of Object.entries(data)) {
685
+ if (key.endsWith(COLLECTION_SUFFIX2) && key !== COLLECTION_SUFFIX2 && isCollectionValue(value)) {
686
+ shaped[key.slice(0, -COLLECTION_SUFFIX2.length)] = shapeData(
687
+ value.items
688
+ );
689
+ } else {
690
+ shaped[key] = shapeData(value);
691
+ }
692
+ }
693
+ return shaped;
694
+ }
695
+ return data;
696
+ }
697
+ function unwrapSingleRoot(data) {
698
+ if (isRecord(data)) {
699
+ const keys = Object.keys(data);
700
+ if (keys.length === 1) {
701
+ return data[keys[0]];
702
+ }
703
+ }
704
+ return data;
705
+ }
706
+
707
+ // src/index.ts
708
+ var DEFAULT_RETRIES = 5;
709
+ var DEFAULT_RETRY_DELAY_MS = 250;
710
+ var DEFAULT_MAX_RETRY_DELAY_MS = 8e3;
711
+ var DEFAULT_SPLIT_BATCH_SIZE = 50;
712
+ function isRecord2(value) {
713
+ return typeof value === "object" && value !== null && !Array.isArray(value);
714
+ }
715
+ function collectAtPath(data, path) {
716
+ let current = [data];
717
+ for (const segment of path) {
718
+ const next = [];
719
+ for (const value of current) {
720
+ if (isRecord2(value)) {
721
+ const child = value[segment];
722
+ if (Array.isArray(child)) {
723
+ next.push(...child);
724
+ } else if (child !== null && child !== void 0) {
725
+ next.push(child);
726
+ }
727
+ }
728
+ }
729
+ current = next;
730
+ }
731
+ return current.filter(isRecord2);
732
+ }
733
+ function resolveContext(options) {
734
+ const env = readEnvSettings();
735
+ const space = options.space ?? env.space;
736
+ const environment = options.environment ?? env.environment ?? "master";
737
+ const preview = options.preview ?? false;
738
+ const token = options.token ?? (preview ? env.previewToken : env.token);
739
+ if (!space || !token) {
740
+ const missing = [];
741
+ if (!space) {
742
+ missing.push(
743
+ "space (pass `space` or set CONTENTFUL_SPACE_ID / NEXT_PUBLIC_CONTENTFUL_SPACE_ID)"
744
+ );
745
+ }
746
+ if (!token) {
747
+ missing.push(
748
+ preview ? "preview access token (pass `token` or set CONTENTFUL_PREVIEW_ACCESS_TOKEN / NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN)" : "access token (pass `token` or set CONTENTFUL_ACCESS_TOKEN / NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN)"
749
+ );
750
+ }
751
+ throw new FetchContentfulError(
752
+ `Missing required Contentful configuration: ${missing.join("; ")}.`,
753
+ { code: "CONFIG" }
754
+ );
755
+ }
756
+ const retry = {
757
+ retries: options.retries ?? DEFAULT_RETRIES,
758
+ baseDelayMs: options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS,
759
+ maxDelayMs: options.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS
760
+ };
761
+ const fetchImpl = options.fetch ?? globalThis.fetch;
762
+ const context = {
763
+ space,
764
+ environment,
765
+ token,
766
+ preview,
767
+ locale: options.locale,
768
+ fetch: fetchImpl,
769
+ retry,
770
+ autoSplitNestedCollections: options.autoSplitNestedCollections ?? true,
771
+ splitBatchSize: options.splitBatchSize ?? DEFAULT_SPLIT_BATCH_SIZE
772
+ };
773
+ if (options.signal) context.signal = options.signal;
774
+ if (options.next) context.next = options.next;
775
+ if (options.cache) context.cache = options.cache;
776
+ return context;
777
+ }
778
+ async function resolvePlan(plan, data, variables, context) {
779
+ const parents = collectAtPath(data, plan.path);
780
+ const relevant = parents.filter(
781
+ (parent) => typeof parent[TYPENAME_ALIAS] === "string" && (plan.typeCondition === void 0 || parent[TYPENAME_ALIAS] === plan.typeCondition)
782
+ );
783
+ if (relevant.length === 0) {
784
+ return;
785
+ }
786
+ const byType = /* @__PURE__ */ new Map();
787
+ for (const parent of relevant) {
788
+ const typename = parent[TYPENAME_ALIAS];
789
+ const sys = parent[SYS_ID_ALIAS];
790
+ if (!isRecord2(sys) || typeof sys["id"] !== "string") {
791
+ throw new FetchContentfulError(
792
+ `Cannot split "${plan.responseKey}": a parent entry is missing its sys id.`,
793
+ { code: "STITCH" }
794
+ );
795
+ }
796
+ const group = byType.get(typename) ?? [];
797
+ group.push(parent);
798
+ byType.set(typename, group);
799
+ }
800
+ const resolved = /* @__PURE__ */ new Map();
801
+ await Promise.all(
802
+ [...byType.entries()].map(async ([typename, group]) => {
803
+ const ids = [
804
+ ...new Set(
805
+ group.map((parent) => {
806
+ const sys = parent[SYS_ID_ALIAS];
807
+ return sys.id;
808
+ })
809
+ )
810
+ ];
811
+ await Promise.all(
812
+ chunk(ids, context.splitBatchSize).map(async (idBatch) => {
813
+ const { document, rootKey } = buildSubquery(
814
+ plan,
815
+ typename,
816
+ idBatch.length,
817
+ context
818
+ );
819
+ const subData = await executeDocument(
820
+ document,
821
+ { ...variables, _splitIds: idBatch },
822
+ context
823
+ );
824
+ const collection = subData[rootKey];
825
+ const items = isRecord2(collection) && Array.isArray(collection["items"]) ? collection["items"] : void 0;
826
+ if (!items) {
827
+ throw new FetchContentfulError(
828
+ `Split subquery for "${typename}" returned no "${rootKey}.items". Check that the content type follows Contentful naming conventions.`,
829
+ { code: "STITCH" }
830
+ );
831
+ }
832
+ for (const item of items) {
833
+ if (isRecord2(item)) {
834
+ const sys = item["sys"];
835
+ if (isRecord2(sys) && typeof sys["id"] === "string") {
836
+ resolved.set(`${typename}:${sys["id"]}`, item[plan.responseKey]);
837
+ }
838
+ }
839
+ }
840
+ })
841
+ );
842
+ })
843
+ );
844
+ for (const parent of relevant) {
845
+ const typename = parent[TYPENAME_ALIAS];
846
+ const sys = parent[SYS_ID_ALIAS];
847
+ const key = `${typename}:${sys.id}`;
848
+ if (!resolved.has(key)) {
849
+ throw new FetchContentfulError(
850
+ `Split subquery did not return entry "${sys.id}" of type "${typename}" for field "${plan.responseKey}".`,
851
+ { code: "STITCH" }
852
+ );
853
+ }
854
+ parent[plan.responseKey] = resolved.get(key);
855
+ }
856
+ }
857
+ function cleanupMarkers(data, plans) {
858
+ for (const plan of plans) {
859
+ for (const parent of collectAtPath(data, plan.path)) {
860
+ delete parent[SYS_ID_ALIAS];
861
+ delete parent[TYPENAME_ALIAS];
862
+ }
863
+ }
864
+ }
865
+ async function executeDocument(document, variables, context) {
866
+ const { document: outer, plans } = planSplits(document, context);
867
+ const data = await rawRequest(outer, variables, context);
868
+ await Promise.all(
869
+ plans.map((plan) => resolvePlan(plan, data, variables, context))
870
+ );
871
+ cleanupMarkers(data, plans);
872
+ return data;
873
+ }
874
+ function toDocument(query) {
875
+ if (typeof query !== "string") {
876
+ return query;
877
+ }
878
+ try {
879
+ return graphql.parse(query);
880
+ } catch (cause) {
881
+ throw new FetchContentfulError(
882
+ `Failed to parse GraphQL query: ${String(cause)}`,
883
+ { code: "CONFIG", cause }
884
+ );
885
+ }
886
+ }
887
+ function withAutoVariables(document, variables, context) {
888
+ const declared = new Set(
889
+ (getOperation(document).variableDefinitions ?? []).map(
890
+ (definition) => definition.variable.name.value
891
+ )
892
+ );
893
+ const merged = { ...variables };
894
+ if (declared.has("preview") && merged["preview"] === void 0) {
895
+ merged["preview"] = context.preview;
896
+ }
897
+ if (declared.has("locale") && merged["locale"] === void 0 && context.locale !== void 0) {
898
+ merged["locale"] = context.locale;
899
+ }
900
+ return merged;
901
+ }
902
+ async function fetchContentful(query, options = {}) {
903
+ const context = resolveContext(options);
904
+ let document = inlineFragments(toDocument(query));
905
+ if (options.autoInjectArgs ?? true) {
906
+ document = injectRootArgs(document, {
907
+ preview: context.preview,
908
+ locale: context.locale
909
+ });
910
+ }
911
+ if (options.validateLocale && context.locale !== void 0) {
912
+ const locales = await getLocales(context);
913
+ if (!locales.some((locale) => locale.code === context.locale)) {
914
+ throw new FetchContentfulError(
915
+ `Locale "${context.locale}" is not configured in space "${context.space}" (available: ${locales.map((locale) => locale.code).join(", ")}).`,
916
+ { code: "LOCALE" }
917
+ );
918
+ }
919
+ }
920
+ const variables = withAutoVariables(
921
+ document,
922
+ options.variables ?? {},
923
+ context
924
+ );
925
+ const data = await executeDocument(document, variables, context);
926
+ const result = options.shapeResponseData === false ? data : shapeData(data);
927
+ if (options.unwrapRootField === false) {
928
+ return result;
929
+ }
930
+ return unwrapSingleRoot(result);
931
+ }
932
+ function createFetchContentful(defaults = {}) {
933
+ function bound(query, options = {}) {
934
+ const merged = {
935
+ ...defaults,
936
+ ...options
937
+ };
938
+ if (merged.shapeResponseData === false) {
939
+ if (merged.unwrapRootField === false) {
940
+ return fetchContentful(query, {
941
+ ...merged,
942
+ shapeResponseData: false,
943
+ unwrapRootField: false
944
+ });
945
+ }
946
+ return fetchContentful(query, {
947
+ ...merged,
948
+ shapeResponseData: false
949
+ });
950
+ }
951
+ if (merged.unwrapRootField === false) {
952
+ return fetchContentful(query, {
953
+ ...merged,
954
+ unwrapRootField: false
955
+ });
956
+ }
957
+ return fetchContentful(query, merged);
958
+ }
959
+ return bound;
960
+ }
961
+ var index_default = fetchContentful;
962
+
963
+ exports.FetchContentfulError = FetchContentfulError;
964
+ exports.clearLocaleCache = clearLocaleCache;
965
+ exports.collectAtPath = collectAtPath;
966
+ exports.createFetchContentful = createFetchContentful;
967
+ exports.default = index_default;
968
+ exports.fetchContentful = fetchContentful;
969
+ exports.getLocales = getLocales;
970
+ exports.injectRootArgs = injectRootArgs;
971
+ exports.inlineFragments = inlineFragments;
972
+ exports.isFetchContentfulError = isFetchContentfulError;
973
+ exports.readEnvSettings = readEnvSettings;
974
+ exports.shapeData = shapeData;
975
+ exports.unwrapSingleRoot = unwrapSingleRoot;
976
+ //# sourceMappingURL=index.cjs.map
977
+ //# sourceMappingURL=index.cjs.map