@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 +71 -0
- package/README.md +95 -2
- package/dist/cli/index.mjs +949 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/index.cjs +20 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -10
- package/dist/index.d.ts +91 -10
- package/dist/index.mjs +20 -26
- package/dist/index.mjs.map +1 -1
- package/dist/tada/index.cjs +4 -0
- package/dist/tada/index.cjs.map +1 -0
- package/dist/tada/index.d.cts +106 -0
- package/dist/tada/index.d.ts +106 -0
- package/dist/tada/index.mjs +3 -0
- package/dist/tada/index.mjs.map +1 -0
- package/docs/tada.md +301 -0
- package/package.json +28 -2
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, //
|
|
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`**
|
|
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.
|