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