@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.
@@ -0,0 +1,324 @@
1
+ import { DocumentNode } from 'graphql';
2
+
3
+ /**
4
+ * Public types for fetch-contentful.
5
+ *
6
+ * This file is type-only (no runtime code) and is excluded from coverage.
7
+ */
8
+ /** JSON-compatible variable values accepted by the GraphQL endpoint. */
9
+ type GraphQLVariables = Record<string, unknown>;
10
+ /** Next.js-specific fetch extensions (App Router data cache). */
11
+ interface NextFetchOptions {
12
+ revalidate?: number | false;
13
+ tags?: string[];
14
+ }
15
+ /** Error categories a {@link FetchContentfulError} can carry. */
16
+ type FetchContentfulErrorCode = 'CONFIG' | 'NETWORK' | 'HTTP' | 'GRAPHQL' | 'STITCH' | 'LOCALE';
17
+ /** Shape of a GraphQL error object returned by Contentful. */
18
+ interface ContentfulGraphQLError {
19
+ message: string;
20
+ locations?: Array<{
21
+ line: number;
22
+ column: number;
23
+ }>;
24
+ path?: Array<string | number>;
25
+ extensions?: Record<string, unknown>;
26
+ }
27
+ /** Options accepted by {@link fetchContentful}. */
28
+ interface FetchContentfulOptions<TVariables extends GraphQLVariables = GraphQLVariables> {
29
+ /** GraphQL variables for the query. */
30
+ variables?: TVariables;
31
+ /**
32
+ * Contentful environment id.
33
+ * Defaults to `process.env.NEXT_PUBLIC_CONTENTFUL_ENVIRONMENT`, then `"master"`.
34
+ */
35
+ environment?: string;
36
+ /**
37
+ * Contentful space id.
38
+ * Defaults to `process.env.NEXT_PUBLIC_CONTENTFUL_SPACE`. Required.
39
+ */
40
+ space?: string;
41
+ /**
42
+ * Use the Preview API. Selects the preview token by default, is injected
43
+ * as a `preview: true` argument on root query fields (see
44
+ * {@link FetchContentfulOptions.autoInjectArgs}), and auto-fills a
45
+ * `$preview: Boolean` variable when your query declares one.
46
+ * Defaults to `false`.
47
+ */
48
+ preview?: boolean;
49
+ /**
50
+ * Locale code (e.g. `"en-US"`). Injected as a `locale` argument on root
51
+ * query fields (see {@link FetchContentfulOptions.autoInjectArgs}),
52
+ * auto-fills a `$locale: String` variable when your query declares one,
53
+ * and is applied to generated subqueries.
54
+ */
55
+ locale?: string;
56
+ /**
57
+ * Automatically add `preview: true` / `locale: "..."` arguments to the
58
+ * root fields of your query based on the `preview` and `locale` options,
59
+ * so queries never need to declare or thread those arguments themselves.
60
+ * Contentful cascades both arguments to all nested fields. Arguments you
61
+ * write explicitly are never overridden. Defaults to `true`.
62
+ */
63
+ autoInjectArgs?: boolean;
64
+ /**
65
+ * When `true`, the provided `locale` is validated against the locales
66
+ * configured in Contentful (fetched once per space/environment and cached).
67
+ */
68
+ validateLocale?: boolean;
69
+ /**
70
+ * Access token. Defaults to `process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN`
71
+ * when `preview` is true, otherwise `process.env.CONTENTFUL_ACCESS_TOKEN`
72
+ * (with `NEXT_PUBLIC_`-prefixed fallbacks for client-side use).
73
+ */
74
+ token?: string;
75
+ /**
76
+ * Number of retry attempts after the first failed request (per network
77
+ * request, including subqueries). Defaults to `5`.
78
+ */
79
+ retries?: number;
80
+ /** Base delay in ms for exponential backoff. Defaults to `250`. */
81
+ retryDelayMs?: number;
82
+ /** Maximum backoff delay in ms. Defaults to `8000`. */
83
+ maxRetryDelayMs?: number;
84
+ /**
85
+ * Automatically split nested reference collections (any `*Collection`
86
+ * field that is not a root query field) into their own subqueries to stay
87
+ * under Contentful's query complexity limits. Defaults to `true`.
88
+ * One-to-one references can be split by annotating them with `@split`.
89
+ */
90
+ autoSplitNestedCollections?: boolean;
91
+ /** How many parent entry ids to resolve per subquery request. Defaults to `50`. */
92
+ splitBatchSize?: number;
93
+ /**
94
+ * Whether to unwrap single-root responses. When the (optionally shaped)
95
+ * response object has exactly one root key — e.g. a query for only
96
+ * `siteSettingsCollection` — the promise resolves with that key's value
97
+ * directly instead of a one-key wrapper object. Responses with multiple
98
+ * root fields are always returned as-is. Defaults to `true`.
99
+ */
100
+ unwrapRootField?: boolean;
101
+ /**
102
+ * Whether to run the response through the collection shaper
103
+ * (`fooCollection.items` → `foo`) before resolving. When `false`, the data
104
+ * resolves in Contentful's raw wire shape and the return type reflects
105
+ * that. Defaults to `true`.
106
+ */
107
+ shapeResponseData?: boolean;
108
+ /** Custom fetch implementation (useful for tests). Defaults to `globalThis.fetch`. */
109
+ fetch?: typeof fetch;
110
+ /** AbortSignal forwarded to every underlying fetch call. */
111
+ signal?: AbortSignal;
112
+ /** Next.js App Router data-cache options, forwarded to fetch. */
113
+ next?: NextFetchOptions;
114
+ /** Standard fetch cache mode, forwarded to fetch. */
115
+ cache?: RequestCache;
116
+ }
117
+ /** A single locale as configured in Contentful. */
118
+ interface ContentfulLocale {
119
+ code: string;
120
+ name: string;
121
+ default: boolean;
122
+ fallbackCode: string | null;
123
+ }
124
+ /**
125
+ * Recursively rewrites Contentful collection wrappers at the type level:
126
+ * every `fooCollection: { items: T[] }` becomes `foo: T[]`, preserving
127
+ * nullability. Mirrors the runtime behavior of `shapeData`.
128
+ */
129
+ type ShapeCollections<T> = T extends ReadonlyArray<infer U> ? Array<ShapeCollections<U>> : T extends object ? {
130
+ [K in keyof T as ShapedKey<K, T[K]>]: ShapedValue<K, T[K]>;
131
+ } : T;
132
+ type IsCollectionEntry<K, V> = K extends `${infer Base}Collection` ? Base extends '' ? false : NonNullable<V> extends {
133
+ items: ReadonlyArray<unknown>;
134
+ } ? true : false : false;
135
+ type ShapedKey<K, V> = IsCollectionEntry<K, V> extends true ? K extends `${infer Base}Collection` ? Base : K : K;
136
+ type ShapedValue<K, V> = IsCollectionEntry<K, V> extends true ? NonNullable<V> extends {
137
+ items: ReadonlyArray<infer I>;
138
+ } ? Array<ShapeCollections<I>> | Extract<V, null | undefined> : never : ShapeCollections<V>;
139
+ /** `true` when `T` is a union of two or more members. */
140
+ type IsUnion<T, U = T> = T extends unknown ? [U] extends [T] ? false : true : never;
141
+ /**
142
+ * The type-level twin of the single-root unwrap: when `T` is an object with
143
+ * exactly one statically-known key, resolves to that key's value type;
144
+ * otherwise resolves to `T` unchanged. Objects typed with index signatures
145
+ * (like the default `Record<string, unknown>`) are left as-is, since their
146
+ * keys aren't statically known.
147
+ */
148
+ type UnwrapSingleRoot<T> = T extends ReadonlyArray<unknown> ? T : T extends object ? string extends keyof T ? T : [keyof T] extends [never] ? T : IsUnion<keyof T> extends true ? T : T[keyof T] : T;
149
+
150
+ /** Error thrown (i.e. the rejection value) for every failure in fetch-contentful. */
151
+ declare class FetchContentfulError extends Error {
152
+ readonly code: FetchContentfulErrorCode;
153
+ readonly status: number | undefined;
154
+ readonly errors: ContentfulGraphQLError[] | undefined;
155
+ /** Internal: whether a retry may succeed. */
156
+ readonly retryable: boolean;
157
+ /** Internal: server-requested retry delay (from Retry-After), in ms. */
158
+ readonly retryAfterMs: number | undefined;
159
+ constructor(message: string, options: {
160
+ code: FetchContentfulErrorCode;
161
+ status?: number;
162
+ errors?: ContentfulGraphQLError[];
163
+ retryable?: boolean;
164
+ retryAfterMs?: number;
165
+ cause?: unknown;
166
+ });
167
+ }
168
+ /** Type guard for {@link FetchContentfulError}. */
169
+ declare function isFetchContentfulError(value: unknown): value is FetchContentfulError;
170
+
171
+ /**
172
+ * Environment-derived configuration.
173
+ *
174
+ * Two families of variable names are supported, checked in this order:
175
+ *
176
+ * 1. Framework-neutral names — `CONTENTFUL_SPACE_ID`, `CONTENTFUL_ENVIRONMENT`,
177
+ * `CONTENTFUL_ACCESS_TOKEN`, `CONTENTFUL_PREVIEW_ACCESS_TOKEN`.
178
+ * 2. `NEXT_PUBLIC_`-prefixed equivalents for Next.js projects that call
179
+ * Contentful from the browser.
180
+ *
181
+ * IMPORTANT: every lookup below is written as a literal
182
+ * `process.env.SOME_NAME` property access, never a dynamic `process.env[x]`.
183
+ * Next.js exposes `NEXT_PUBLIC_` variables to client bundles by
184
+ * string-replacing those literal expressions at build time — dynamic access
185
+ * is invisible to the inliner and resolves to `undefined` in the browser.
186
+ * Do not "refactor" these into a loop or a name table.
187
+ *
188
+ * This module reads the already-populated `process.env`; loading `.env`
189
+ * files from disk is deliberately left to the platform (Next.js, Vite,
190
+ * dotenv, ...), whose precedence rules would otherwise be duplicated here.
191
+ */
192
+ interface EnvSettings {
193
+ space: string | undefined;
194
+ environment: string | undefined;
195
+ token: string | undefined;
196
+ previewToken: string | undefined;
197
+ }
198
+ /** Reads Contentful settings from `process.env` (neutral names first). */
199
+ declare function readEnvSettings(): EnvSettings;
200
+
201
+ /**
202
+ * Contentful cascades `preview` and `locale` from a root query field down to
203
+ * every nested field, so injecting the arguments at the root is equivalent to
204
+ * threading them through the whole query by hand — without the boilerplate.
205
+ */
206
+ interface InjectableArgs {
207
+ preview: boolean;
208
+ locale: string | undefined;
209
+ }
210
+ /**
211
+ * Adds `preview: true` and/or `locale: "..."` arguments to every root field
212
+ * of the operation, unless the author already wrote that argument. Nested
213
+ * fields are left untouched — Contentful inherits both arguments downward.
214
+ */
215
+ declare function injectRootArgs(document: DocumentNode, args: InjectableArgs): DocumentNode;
216
+
217
+ interface RetryConfig {
218
+ /** Number of retries after the initial attempt. */
219
+ retries: number;
220
+ /** Base backoff delay in ms. */
221
+ baseDelayMs: number;
222
+ /** Maximum backoff delay in ms. */
223
+ maxDelayMs: number;
224
+ /** Random source, injectable for deterministic tests. */
225
+ random?: () => number;
226
+ /** Sleep implementation, injectable for deterministic tests. */
227
+ sleep?: (ms: number) => Promise<void>;
228
+ }
229
+
230
+ interface LocalesContext {
231
+ space: string;
232
+ environment: string;
233
+ preview: boolean;
234
+ token: string;
235
+ fetch: typeof fetch;
236
+ retry: RetryConfig;
237
+ signal?: AbortSignal;
238
+ }
239
+ /**
240
+ * Returns the locales configured in Contentful for a space/environment.
241
+ * Results are cached in-module, so repeated fetches are free; failed
242
+ * lookups are evicted so they can be retried later.
243
+ */
244
+ declare function getLocales(context: LocalesContext): Promise<ContentfulLocale[]>;
245
+ /** Clears the in-module locale cache (mainly useful in tests). */
246
+ declare function clearLocaleCache(): void;
247
+
248
+ /**
249
+ * Recursively rewrites Contentful collection wrappers:
250
+ * `{ fooCollection: { items: [...] } }` becomes `{ foo: [...] }`.
251
+ * Non-collection data is returned untouched (deeply copied).
252
+ */
253
+ declare function shapeData<T>(data: T): ShapeCollections<T>;
254
+ /**
255
+ * Unwraps a single-root response: `{ siteSettings: X }` becomes `X` when
256
+ * `siteSettings` is the only root key. Anything else — multiple roots,
257
+ * arrays, primitives — is returned untouched.
258
+ */
259
+ declare function unwrapSingleRoot<T>(data: T): UnwrapSingleRoot<T>;
260
+
261
+ /**
262
+ * Replaces every named fragment spread with an equivalent inline fragment so
263
+ * split planning can see the whole query shape uniformly.
264
+ */
265
+ declare function inlineFragments(document: DocumentNode): DocumentNode;
266
+
267
+ /** Collect every object reachable at a response path, flattening arrays. */
268
+ declare function collectAtPath(data: unknown, path: string[]): Record<string, unknown>[];
269
+
270
+ /**
271
+ * Fetches data from Contentful's GraphQL API.
272
+ *
273
+ * - Splits nested reference collections (and `@split`-annotated references)
274
+ * into recursive subqueries to stay under query complexity limits, then
275
+ * stitches everything back together.
276
+ * - Retries transient failures with exponential backoff.
277
+ * - Rejects if any request in the tree fails; resolves only when the full
278
+ * query has succeeded.
279
+ * - Shapes the response so every `fooCollection.items` becomes `foo`
280
+ * (disable with `shapeResponseData: false` to receive the raw wire shape).
281
+ */
282
+ declare function fetchContentful<TData = Record<string, unknown>, TVariables extends GraphQLVariables = GraphQLVariables>(query: string | DocumentNode, options: FetchContentfulOptions<TVariables> & {
283
+ shapeResponseData: false;
284
+ unwrapRootField: false;
285
+ }): Promise<TData>;
286
+ declare function fetchContentful<TData = Record<string, unknown>, TVariables extends GraphQLVariables = GraphQLVariables>(query: string | DocumentNode, options: FetchContentfulOptions<TVariables> & {
287
+ shapeResponseData: false;
288
+ }): Promise<UnwrapSingleRoot<TData>>;
289
+ declare function fetchContentful<TData = Record<string, unknown>, TVariables extends GraphQLVariables = GraphQLVariables>(query: string | DocumentNode, options: FetchContentfulOptions<TVariables> & {
290
+ unwrapRootField: false;
291
+ }): Promise<ShapeCollections<TData>>;
292
+ declare function fetchContentful<TData = Record<string, unknown>, TVariables extends GraphQLVariables = GraphQLVariables>(query: string | DocumentNode, options?: FetchContentfulOptions<TVariables>): Promise<UnwrapSingleRoot<ShapeCollections<TData>>>;
293
+ /**
294
+ * Creates a `fetchContentful` bound to default options — the recommended way
295
+ * to configure the utility once per project:
296
+ *
297
+ * ```ts
298
+ * // lib/contentful.ts
299
+ * export const fetchContentful = createFetchContentful({
300
+ * space: 'abc123',
301
+ * locale: 'en-US',
302
+ * retries: 3,
303
+ * });
304
+ * ```
305
+ *
306
+ * Per-call options win over defaults (top-level shallow merge). Note that
307
+ * `shapeResponseData: false` only changes the *return type* when written on
308
+ * the call itself, so prefer setting it per call.
309
+ */
310
+ declare function createFetchContentful<TDefaultVariables extends GraphQLVariables = GraphQLVariables>(defaults?: FetchContentfulOptions<TDefaultVariables>): {
311
+ <TData = Record<string, unknown>, TVariables extends GraphQLVariables = GraphQLVariables>(query: string | DocumentNode, options: FetchContentfulOptions<TVariables> & {
312
+ shapeResponseData: false;
313
+ unwrapRootField: false;
314
+ }): Promise<TData>;
315
+ <TData = Record<string, unknown>, TVariables extends GraphQLVariables = GraphQLVariables>(query: string | DocumentNode, options: FetchContentfulOptions<TVariables> & {
316
+ shapeResponseData: false;
317
+ }): Promise<UnwrapSingleRoot<TData>>;
318
+ <TData = Record<string, unknown>, TVariables extends GraphQLVariables = GraphQLVariables>(query: string | DocumentNode, options: FetchContentfulOptions<TVariables> & {
319
+ unwrapRootField: false;
320
+ }): Promise<ShapeCollections<TData>>;
321
+ <TData = Record<string, unknown>, TVariables extends GraphQLVariables = GraphQLVariables>(query: string | DocumentNode, options?: FetchContentfulOptions<TVariables>): Promise<UnwrapSingleRoot<ShapeCollections<TData>>>;
322
+ };
323
+
324
+ export { type ContentfulGraphQLError, type ContentfulLocale, FetchContentfulError, type FetchContentfulErrorCode, type FetchContentfulOptions, type GraphQLVariables, type NextFetchOptions, type ShapeCollections, type UnwrapSingleRoot, clearLocaleCache, collectAtPath, createFetchContentful, fetchContentful as default, fetchContentful, getLocales, injectRootArgs, inlineFragments, isFetchContentfulError, readEnvSettings, shapeData, unwrapSingleRoot };