@fourtwelvelabs/fetch-contentful 0.1.0 → 0.2.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,71 @@
1
+ # @fourtwelvelabs/fetch-contentful
2
+
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 641367c: `@split` on a **root field** now throws instead of being silently ignored.
8
+
9
+ Root fields have no parent entry to stitch a result back onto, so they have
10
+ never been splittable — but the directive was quietly stripped before the
11
+ request went out, leaving the query just as expensive as before with nothing
12
+ to explain why. It now raises a `CONFIG` `FetchContentfulError` naming the
13
+ field.
14
+
15
+ **This can surface in existing queries.** If a query annotates a root field,
16
+ that call will now reject where it previously succeeded (having ignored the
17
+ directive). The fix is to remove the directive — it was never doing anything
18
+ — and page through the field with its own `limit` / `skip` arguments instead.
19
+ Nested `@split` annotations are unaffected.
20
+
21
+ Also clarifies and pins the relationship between `@split` and
22
+ `autoSplitNestedCollections`: the option gates **only** the automatic
23
+ detection of nested `*Collection` fields, while `@split` is an independent
24
+ explicit trigger that splits its field — collection or one-to-one reference —
25
+ regardless of the option. This enables a "manual mode"
26
+ (`autoSplitNestedCollections: false` plus targeted annotations) where only
27
+ the fields that actually exceed Contentful's complexity limit are split. That
28
+ behavior was already correct; it is now documented and covered by tests.
29
+
30
+ ## 0.2.0
31
+
32
+ ### Minor Changes
33
+
34
+ - 9c9eef3: Add a gql.tada companion.
35
+
36
+ - **`@fourtwelvelabs/fetch-contentful/tada`** — a new type-only subpath export
37
+ providing `ContentfulScalars`, the scalar map for Contentful's GraphQL
38
+ Content API, for `initGraphQLTada<{ scalars: ContentfulScalars }>`. Nothing
39
+ in it imports gql.tada at runtime.
40
+ - **`tada-init` and `tada-refresh` CLI commands**, via a new
41
+ `fetch-contentful` binary. `tada-init` downloads the space's schema as SDL,
42
+ registers `@0no-co/graphqlsp` in the project's `tsconfig.json` (preserving
43
+ comments and formatting, and updating an existing entry rather than
44
+ duplicating it) and writes a `graphql.ts` bound to the schema and the scalar
45
+ map. `tada-refresh` re-downloads the schema only, leaving the file untouched
46
+ when nothing changed. Both support `--dry-run`, resolve credentials through
47
+ the same environment variables the library reads, and never print the access
48
+ token.
49
+ - **Typed document support in `fetchContentful`** — passing a
50
+ `TypedDocumentNode` (gql.tada, or graphql-codegen's client preset) infers
51
+ both the result and the variable types, with response shaping and single-root
52
+ unwrapping applied to the inferred type. `variables` is required exactly when
53
+ the document declares one the library does not inject itself, and unknown
54
+ variable names are rejected. Existing string and `DocumentNode` usage is
55
+ unchanged.
56
+
57
+ `gql.tada` and `@0no-co/graphqlsp` are optional peer dependencies, needed only
58
+ at author time.
59
+
60
+ ## 0.1.0
61
+
62
+ ### Minor Changes
63
+
64
+ - b6e2577: Initial release. A type-safe GraphQL fetching utility for Contentful:
65
+
66
+ - Automatic query splitting for nested reference collections (and `@split`-annotated one-to-one references), fetched in id batches and stitched back into the original response shape.
67
+ - Automatic retries with exponential backoff and jitter, honouring `Retry-After`.
68
+ - Response shaping (`fooCollection.items` → `foo`) and single-root unwrapping, at runtime and in the return type.
69
+ - Automatic `preview` / `locale` argument injection, with optional locale validation against the space.
70
+ - Typed `FetchContentfulError` carrying an error code, HTTP status, and GraphQL errors.
71
+ - Next.js App Router support via `next.revalidate` / `next.tags` passthrough.
package/README.md CHANGED
@@ -9,6 +9,7 @@ A foolproof, type-safe GraphQL fetching utility for Contentful. One function in,
9
9
  - **No preview/locale boilerplate** — `preview` and `locale` are injected as arguments onto your query's root fields automatically (Contentful cascades them to every nested field), so queries never need to declare or thread them.
10
10
  - **All-or-nothing promise** — resolves only when every request in the tree succeeded; rejects with a typed `FetchContentfulError` if anything fails.
11
11
  - **Locale awareness** — locales are fetched from Contentful once and cached in-module, so validation costs nothing per fetch.
12
+ - **Inferred types, optionally** — pass a [gql.tada](https://gql-tada.0no.co) or graphql-codegen document and the result and variable types come from the query itself. One command (`tada-init`) sets gql.tada up against your space. See [Typed queries with gql.tada](#typed-queries-with-gqltada).
12
13
  - **ESM + CJS, Next.js-ready** — first-class App Router support (`next.revalidate` / `next.tags` passthrough), works in Pages Router and any Node ≥ 18 runtime.
13
14
 
14
15
  ## Install
@@ -101,6 +102,41 @@ const data = await fetchContentful<PagesQuery>(
101
102
 
102
103
  Notice there is no `$preview` or `$locale` anywhere in the query: the options are injected as `preview: true` / `locale: "en-US"` arguments on the root field before the query is sent, and Contentful cascades both to every nested field. Arguments you write explicitly are never overridden, so per-field overrides like `title(locale: "de-DE")` keep working. Set `autoInjectArgs: false` to opt out. (The older style still works too — `$preview: Boolean` / `$locale: String` variables are auto-filled whenever a query declares them.)
103
104
 
105
+ ## Typed queries with gql.tada
106
+
107
+ Everything above types the response by hand (`fetchContentful<PagesQuery>`). You can instead have the types come from the query itself. Install the author-time peers, then run the setup command:
108
+
109
+ ```bash
110
+ yarn add -D gql.tada @0no-co/graphqlsp
111
+ npx @fourtwelvelabs/fetch-contentful tada-init
112
+ ```
113
+
114
+ That downloads your space's schema as SDL, registers `@0no-co/graphqlsp` in your `tsconfig.json` and writes `src/graphql.ts`. It reads the same environment variables the library does, is safe to re-run, and takes `--dry-run`. Restart the TypeScript server afterwards, and queries type themselves:
115
+
116
+ ```ts
117
+ import { fetchContentful } from '@fourtwelvelabs/fetch-contentful';
118
+ import { graphql } from './graphql';
119
+
120
+ const PageQuery = graphql(`
121
+ query Page($slug: String!) {
122
+ pageCollection(where: { slug: $slug }, limit: 1) {
123
+ items {
124
+ title
125
+ sectionsCollection { items { heading } }
126
+ }
127
+ }
128
+ }
129
+ `);
130
+
131
+ const pages = await fetchContentful(PageQuery, { variables: { slug: 'home' } });
132
+ // ^? Array<{ title: string | null;
133
+ // sections: Array<{ heading: string | null }> }> | null
134
+ ```
135
+
136
+ No type argument, and `variables` is required exactly when the document declares one the library doesn't inject itself (`$preview` and `$locale` are filled in automatically). Response shaping and single-root unwrapping are applied to the inferred type, so what you see is what you get. After a content-model change, run `tada-refresh` to re-download the schema.
137
+
138
+ **[Full gql.tada guide →](./docs/tada.md)** — the scalar map, manual setup without the CLI, the CLI reference, and a CI recipe that opens a PR when your content model changes.
139
+
104
140
  ## Next.js
105
141
 
106
142
  ### App Router (recommended)
@@ -167,7 +203,8 @@ fetchContentful(query, {
167
203
 
168
204
  // Query rewriting
169
205
  autoInjectArgs: true, // inject preview/locale args on root fields
170
- autoSplitNestedCollections: true, // split every non-root *Collection field
206
+ autoSplitNestedCollections: true, // auto-detect non-root *Collection fields
207
+ // (@split works regardless — see below)
171
208
  splitBatchSize: 50, // parent ids per subquery
172
209
  });
173
210
  ```
@@ -226,7 +263,7 @@ The standalone `shapeData` / `ShapeCollections` exports exist only for advanced
226
263
 
227
264
  Contentful rejects queries whose complexity exceeds its limit — usually caused by nested reference fields multiplying. `fetch-contentful` avoids this transparently:
228
265
 
229
- 1. Named fragments are inlined and the query is scanned. Every **nested** `*Collection` field (auto) and every field annotated **`@split`** (one-to-one references) is removed from the outer query and recorded as a plan.
266
+ 1. Named fragments are inlined and the query is scanned. Every **nested** `*Collection` field (auto) and every field annotated **`@split`** is removed from the outer query and recorded as a plan.
230
267
  2. The outer query runs with tiny markers (`sys { id }`, `__typename`) injected where fields were removed.
231
268
  3. Parents are grouped by their concrete `__typename`, and each split field is re-fetched via `"{typeName}Collection"(where: { sys: { id_in: [...] } })` in batches of `splitBatchSize`, in parallel. Subqueries go through the same pipeline, so splits nest recursively to any depth.
232
269
  4. Results are stitched back onto their parents by `sys.id`. If any entry can't be resolved, the whole promise rejects with a `STITCH` error — you never get silently incomplete data.
@@ -249,6 +286,59 @@ query {
249
286
 
250
287
  The `@split` directive is stripped before anything is sent to Contentful.
251
288
 
289
+ ### `@split` and `autoSplitNestedCollections` are independent
290
+
291
+ `autoSplitNestedCollections` controls **only automatic detection** — whether nested `*Collection` fields are split without being asked. `@split` is an explicit instruction that always applies to the field it annotates, whatever the option is set to, and works on **nested collections just as well as one-to-one references**. An annotated collection takes exactly the same path as an auto-detected one: its arguments (`limit`, `where`, `order`, …) and selection set are re-selected verbatim in the subquery.
292
+
293
+ The two triggers never double up — a field caught by both produces one subquery, not two.
294
+
295
+ ### Manual mode
296
+
297
+ Turn auto-splitting off and annotate only the fields that actually blow the complexity limit. Everything else then runs as a single round trip, which is usually faster and always easier to reason about:
298
+
299
+ ```ts
300
+ const data = await fetchContentful(QUERY, {
301
+ autoSplitNestedCollections: false,
302
+ });
303
+ ```
304
+
305
+ ```graphql
306
+ query {
307
+ pageCollection(limit: 10) {
308
+ items {
309
+ title
310
+ # Small and cheap: fetched inline, in the same request.
311
+ seo { description }
312
+ tagsCollection(limit: 5) { items { name } }
313
+
314
+ # The expensive one: fetched separately, batched by parent id.
315
+ sectionsCollection(limit: 50) @split {
316
+ items {
317
+ heading
318
+ mediaCollection(limit: 10) { items { url } }
319
+ }
320
+ }
321
+ }
322
+ }
323
+ }
324
+ ```
325
+
326
+ That sends two requests instead of the four auto mode would. Splits still nest recursively in manual mode: a `@split` inside a subquery is planned when that subquery runs, so you can annotate as deeply as you need.
327
+
328
+ ### Root fields cannot be split
329
+
330
+ `@split` on a **root** field raises a `CONFIG` `FetchContentfulError` rather than being quietly ignored:
331
+
332
+ ```graphql
333
+ query {
334
+ pageCollection @split { # ✗ CONFIG error
335
+ items { title }
336
+ }
337
+ }
338
+ ```
339
+
340
+ A root field has no parent entry to stitch its result back onto, so there is nothing for the directive to do. Reach for `limit` and `skip` on the field itself instead. (Before 0.2.1 the directive was silently stripped here — if an existing query relies on that, remove the directive.)
341
+
252
342
  ## Error handling
253
343
 
254
344
  Every rejection is a `FetchContentfulError`:
@@ -280,6 +370,9 @@ import {
280
370
  injectRootArgs, // the preview/locale argument injector (advanced AST use)
281
371
  } from '@fourtwelvelabs/fetch-contentful';
282
372
  import type { ShapeCollections, FetchContentfulOptions } from '@fourtwelvelabs/fetch-contentful';
373
+
374
+ // Type-only subpath, for wiring gql.tada up by hand (see docs/tada.md).
375
+ import type { ContentfulScalars, JsonValue } from '@fourtwelvelabs/fetch-contentful/tada';
283
376
  ```
284
377
 
285
378
  `ShapeCollections<T>` and `UnwrapSingleRoot<T>` are the type-level twins of `shapeData` and `unwrapSingleRoot`. Neither is needed alongside `fetchContentful` (shaping is built into it) — they're for shaping Contentful-shaped data that arrived some other way.