@fourtwelvelabs/fetch-contentful 0.2.1 → 0.4.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/CHANGELOG.md +86 -1
- package/README.md +49 -12
- package/dist/cli/index.mjs +13 -8
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +13 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +52 -6
- package/dist/index.d.ts +52 -6
- package/dist/index.mjs +13 -9
- package/dist/index.mjs.map +1 -1
- package/dist/tada/index.d.cts +2 -2
- package/dist/tada/index.d.ts +2 -2
- package/docs/tada.md +29 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,90 @@
|
|
|
1
1
|
# @fourtwelvelabs/fetch-contentful
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 73b5ec2: Separate `deliveryToken` and `previewToken`, and stop reading the preview
|
|
8
|
+
token from a `NEXT_PUBLIC_` variable.
|
|
9
|
+
|
|
10
|
+
### The preview token can no longer leak into a client bundle
|
|
11
|
+
|
|
12
|
+
`readEnvSettings` used to fall back to
|
|
13
|
+
`NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN`. Next.js inlines
|
|
14
|
+
`NEXT_PUBLIC_` variables into client bundles by string-replacing the literal
|
|
15
|
+
`process.env.NEXT_PUBLIC_…` accesses this package ships, so a project that
|
|
16
|
+
set that variable had its preview token baked into public JavaScript —
|
|
17
|
+
whether or not any client code ever requested preview content. Importing the
|
|
18
|
+
library was enough.
|
|
19
|
+
|
|
20
|
+
The preview token is now resolved **only** from the unprefixed
|
|
21
|
+
`CONTENTFUL_PREVIEW_ACCESS_TOKEN`. In Next.js that name is still readable on
|
|
22
|
+
the server, so server-side preview keeps working; the migration is to drop
|
|
23
|
+
the `NEXT_PUBLIC_` prefix from that one variable. The space, environment and
|
|
24
|
+
delivery token keep their client-safe names — a delivery token is read-only
|
|
25
|
+
and commonly public.
|
|
26
|
+
|
|
27
|
+
If your app genuinely needs preview in the browser, pass `previewToken`
|
|
28
|
+
explicitly.
|
|
29
|
+
|
|
30
|
+
### `deliveryToken` and `previewToken`
|
|
31
|
+
|
|
32
|
+
A single `token` could not serve both modes: it applied to whichever mode was
|
|
33
|
+
active, so a factory configured with one sent the wrong token as soon as a
|
|
34
|
+
call passed `preview: true` — and there was no way to configure both
|
|
35
|
+
explicitly at all.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
export const fetchContentful = createFetchContentful({
|
|
39
|
+
deliveryToken: process.env.CONTENTFUL_ACCESS_TOKEN,
|
|
40
|
+
previewToken: process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await fetchContentful(QUERY); // → deliveryToken
|
|
44
|
+
await fetchContentful(QUERY, { preview: true }); // → previewToken
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`token` still works and still applies to the active mode, so existing code is
|
|
48
|
+
unaffected, but it is deprecated in favour of the two specific options.
|
|
49
|
+
`readEnvSettings()` gains `deliveryToken`; its `token` field remains as a
|
|
50
|
+
deprecated alias of the same value.
|
|
51
|
+
|
|
52
|
+
The GraphQL endpoint is unchanged — unlike Contentful's REST APIs, preview
|
|
53
|
+
uses the same `graphql.contentful.com` host and is selected by the token plus
|
|
54
|
+
the injected `preview: true` argument. (The REST locale lookup already
|
|
55
|
+
switched between `cdn.` and `preview.` hosts correctly.)
|
|
56
|
+
|
|
57
|
+
## 0.3.0
|
|
58
|
+
|
|
59
|
+
### Minor Changes
|
|
60
|
+
|
|
61
|
+
- 6013f57: `locale` now defaults to `"en-US"`.
|
|
62
|
+
|
|
63
|
+
Previously, omitting `locale` meant no `locale` argument was sent at all, and
|
|
64
|
+
Contentful served whichever locale the space itself defaults to. Now `en-US`
|
|
65
|
+
is requested explicitly unless you say otherwise.
|
|
66
|
+
|
|
67
|
+
**This changes which content comes back for a space whose default locale is
|
|
68
|
+
not `en-US`.** If that describes your space, either set the locale once for
|
|
69
|
+
the project:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
export const fetchContentful = createFetchContentful({ locale: "de-DE" });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
or restore the previous behavior — no `locale` argument, space decides — with
|
|
76
|
+
the new `null` opt-out:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
await fetchContentful(QUERY, { locale: null });
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`locale: null` is accepted anywhere `locale` is, including as a
|
|
83
|
+
`createFetchContentful` default and as a per-call override of one. A space
|
|
84
|
+
that has no `en-US` locale configured will now reject with a `LOCALE` error
|
|
85
|
+
under `validateLocale: true`, rather than silently returning the space
|
|
86
|
+
default.
|
|
87
|
+
|
|
3
88
|
## 0.2.1
|
|
4
89
|
|
|
5
90
|
### Patch Changes
|
|
@@ -64,7 +149,7 @@
|
|
|
64
149
|
- b6e2577: Initial release. A type-safe GraphQL fetching utility for Contentful:
|
|
65
150
|
|
|
66
151
|
- 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,
|
|
152
|
+
- Automatic retries with exponential backoff and jitter, honoring `Retry-After`.
|
|
68
153
|
- Response shaping (`fooCollection.items` → `foo`) and single-root unwrapping, at runtime and in the return type.
|
|
69
154
|
- Automatic `preview` / `locale` argument injection, with optional locale validation against the space.
|
|
70
155
|
- Typed `FetchContentfulError` carrying an error code, HTTP status, and GraphQL errors.
|
package/README.md
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
A foolproof, type-safe GraphQL fetching utility for Contentful. One function in, shaped data out:
|
|
4
4
|
|
|
5
5
|
- **Automatic query splitting** — nested reference collections (and `@split`-annotated one-to-one references) are broken into separate subqueries, fetched in id batches, and stitched back into the original response shape. Fully recursive, so deeply nested queries never hit Contentful's query complexity limit.
|
|
6
|
-
- **Automatic retries** — transient failures (network errors, 408/429/5xx) retry with exponential backoff + jitter,
|
|
6
|
+
- **Automatic retries** — transient failures (network errors, 408/429/5xx) retry with exponential backoff + jitter, honoring `Retry-After`. Configurable, default 5.
|
|
7
7
|
- **Response shaping (built in)** — before the promise resolves, every `fooCollection.items` in the response becomes `foo` (a plain array), at runtime *and* in the return type. No separate call needed.
|
|
8
8
|
- **Single-root unwrapping (built in)** — when a query has one root field, the promise resolves with that field's contents directly (`data` *is* the array/entry, not `{ siteSettings: ... }`). Multi-root queries stay wrapped.
|
|
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.
|
|
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. The locale defaults to `en-US`.
|
|
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
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).
|
|
@@ -22,7 +22,7 @@ yarn add @fourtwelvelabs/fetch-contentful graphql
|
|
|
22
22
|
|
|
23
23
|
Settings resolve in this order — first hit wins, per setting:
|
|
24
24
|
|
|
25
|
-
1. **Per-call options** — `fetchContentful(query, { space,
|
|
25
|
+
1. **Per-call options** — `fetchContentful(query, { space, deliveryToken, ... })`
|
|
26
26
|
2. **Factory defaults** — set once with `createFetchContentful` (below)
|
|
27
27
|
3. **Environment variables** — framework-neutral names first, then `NEXT_PUBLIC_`-prefixed equivalents
|
|
28
28
|
|
|
@@ -34,12 +34,22 @@ If `space` or the appropriate token can't be resolved, the promise rejects with
|
|
|
34
34
|
| --- | --- | --- |
|
|
35
35
|
| `space` | `CONTENTFUL_SPACE_ID` | `NEXT_PUBLIC_CONTENTFUL_SPACE_ID` |
|
|
36
36
|
| `environment` | `CONTENTFUL_ENVIRONMENT` | `NEXT_PUBLIC_CONTENTFUL_ENVIRONMENT` |
|
|
37
|
-
|
|
|
38
|
-
|
|
|
37
|
+
| `deliveryToken` | `CONTENTFUL_ACCESS_TOKEN` | `NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN` |
|
|
38
|
+
| `previewToken` | `CONTENTFUL_PREVIEW_ACCESS_TOKEN` | **none, by design** |
|
|
39
|
+
|
|
40
|
+
There is no environment variable for the locale: it defaults to **`en-US`**, and a project that wants a different one sets it once with `createFetchContentful` (below).
|
|
39
41
|
|
|
40
42
|
The library reads the already-populated `process.env`; loading `.env` / `.env.local` files from disk is your platform's job (Next.js, Vite, dotenv all do this), so precedence between those files always matches your framework's rules.
|
|
41
43
|
|
|
42
|
-
**In a Next.js project you don't need both names.** Set only the `NEXT_PUBLIC_` variant and it works everywhere: the server reads it like any other env var (the prefix only controls *client* exposure), and client bundles get it because the library reads these variables with literal `process.env.NEXT_PUBLIC_...` accesses — the exact pattern Next's build-time inliner string-replaces. Server-only projects (or any other framework) should use the neutral names.
|
|
44
|
+
**In a Next.js project you don't need both names.** Set only the `NEXT_PUBLIC_` variant and it works everywhere: the server reads it like any other env var (the prefix only controls *client* exposure), and client bundles get it because the library reads these variables with literal `process.env.NEXT_PUBLIC_...` accesses — the exact pattern Next's build-time inliner string-replaces. Server-only projects (or any other framework) should use the neutral names. This applies to the space, the environment and the delivery token; the preview token has no client-safe name, for the reason below.
|
|
45
|
+
|
|
46
|
+
### The preview token is never read from a `NEXT_PUBLIC_` variable
|
|
47
|
+
|
|
48
|
+
Next.js exposes `NEXT_PUBLIC_` variables to the browser by string-replacing literal `process.env.NEXT_PUBLIC_…` accesses at build time. This library contains such accesses, so if it read the preview token from a prefixed name, setting that variable would bake the token into your public JavaScript — whether or not any client code ever asked for preview content. Importing the library would be enough.
|
|
49
|
+
|
|
50
|
+
A delivery token is read-only and commonly public. A **preview token reads unpublished content**, so it is resolved only from the unprefixed `CONTENTFUL_PREVIEW_ACCESS_TOKEN`. In Next.js that name is still readable on the server — the prefix only controls *client* exposure — so server-side preview (Draft Mode, route handlers, server components) works unchanged.
|
|
51
|
+
|
|
52
|
+
If you have decided that your app genuinely needs preview in the browser, pass `previewToken` explicitly. Shipping it is then your deliberate choice rather than something the library did on your behalf.
|
|
43
53
|
|
|
44
54
|
### Project-level defaults with `createFetchContentful`
|
|
45
55
|
|
|
@@ -51,9 +61,13 @@ import { createFetchContentful } from '@fourtwelvelabs/fetch-contentful';
|
|
|
51
61
|
|
|
52
62
|
export const fetchContentful = createFetchContentful({
|
|
53
63
|
space: 'abc123', // or leave unset to use env vars
|
|
54
|
-
locale: 'en-US'
|
|
64
|
+
locale: 'de-DE', // override the 'en-US' default for the whole project
|
|
55
65
|
retries: 3,
|
|
56
66
|
});
|
|
67
|
+
|
|
68
|
+
// Configure both tokens once, and every call picks the right one:
|
|
69
|
+
// fetchContentful(QUERY) → deliveryToken
|
|
70
|
+
// fetchContentful(QUERY, { preview: true }) → previewToken
|
|
57
71
|
```
|
|
58
72
|
|
|
59
73
|
```ts
|
|
@@ -102,6 +116,8 @@ const data = await fetchContentful<PagesQuery>(
|
|
|
102
116
|
|
|
103
117
|
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.)
|
|
104
118
|
|
|
119
|
+
`locale` is optional here — it defaults to `en-US`, so the same argument is injected whether or not you pass it. Pass a different code to override it, or `locale: null` to send no `locale` argument at all and let the space serve its own default locale.
|
|
120
|
+
|
|
105
121
|
## Typed queries with gql.tada
|
|
106
122
|
|
|
107
123
|
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:
|
|
@@ -111,11 +127,31 @@ yarn add -D gql.tada @0no-co/graphqlsp
|
|
|
111
127
|
npx @fourtwelvelabs/fetch-contentful tada-init
|
|
112
128
|
```
|
|
113
129
|
|
|
114
|
-
|
|
130
|
+
It reads the same environment variables the library does, is safe to re-run, and takes `--dry-run`. Three things happen: your space's schema is downloaded as SDL, `@0no-co/graphqlsp` is registered in your `tsconfig.json`, and **`src/graphql.ts`** is written — a small file of your own that binds gql.tada to your schema:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
// src/graphql.ts — generated for you. Commit it; edit it freely.
|
|
134
|
+
import { initGraphQLTada } from 'gql.tada';
|
|
135
|
+
import type { ContentfulScalars } from '@fourtwelvelabs/fetch-contentful/tada';
|
|
136
|
+
import type { introspection } from './contentful-env.d.ts';
|
|
137
|
+
|
|
138
|
+
export const graphql = initGraphQLTada<{
|
|
139
|
+
introspection: introspection; // your space's content model, as types
|
|
140
|
+
scalars: ContentfulScalars; // DateTime → string, JSON → JsonValue, …
|
|
141
|
+
}>();
|
|
142
|
+
|
|
143
|
+
export { readFragment } from 'gql.tada';
|
|
144
|
+
export type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada';
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`graphql` is a tagged template function doing two jobs at once: at runtime it parses your query into a GraphQL document, and at compile time it infers that query's result and variable types from the schema. It can't be an export of this package — it has to be bound to *your* space — which is why the command generates it.
|
|
148
|
+
|
|
149
|
+
Restart the TypeScript server so `@0no-co/graphqlsp` loads (it's what gives you autocomplete and field validation inside the backticks). Then import `graphql` from wherever `tada-init` put it, and queries type themselves:
|
|
115
150
|
|
|
116
151
|
```ts
|
|
152
|
+
// src/lib/pages.ts
|
|
117
153
|
import { fetchContentful } from '@fourtwelvelabs/fetch-contentful';
|
|
118
|
-
import { graphql } from '
|
|
154
|
+
import { graphql } from '../graphql';
|
|
119
155
|
|
|
120
156
|
const PageQuery = graphql(`
|
|
121
157
|
query Page($slug: String!) {
|
|
@@ -183,11 +219,12 @@ fetchContentful(query, {
|
|
|
183
219
|
space: 'abc123', // default: NEXT_PUBLIC_CONTENTFUL_SPACE
|
|
184
220
|
environment: 'master', // default: NEXT_PUBLIC_CONTENTFUL_ENVIRONMENT ?? 'master'
|
|
185
221
|
preview: false, // default: false
|
|
186
|
-
locale: '
|
|
222
|
+
locale: 'en-US', // default: 'en-US'. null sends no locale at all
|
|
187
223
|
validateLocale: true, // reject with LOCALE if locale isn't configured
|
|
188
|
-
|
|
224
|
+
deliveryToken: '...', // CDA token, used when preview is false
|
|
225
|
+
previewToken: '...', // CPA token, used when preview is true
|
|
189
226
|
|
|
190
|
-
// Request
|
|
227
|
+
// Request behavior
|
|
191
228
|
variables: { slug: 'home' },
|
|
192
229
|
retries: 5, // retry count after the initial attempt
|
|
193
230
|
retryDelayMs: 250, // base backoff delay
|
package/dist/cli/index.mjs
CHANGED
|
@@ -12,10 +12,14 @@ function readEnvSettings() {
|
|
|
12
12
|
return {
|
|
13
13
|
space: void 0,
|
|
14
14
|
environment: void 0,
|
|
15
|
+
deliveryToken: void 0,
|
|
15
16
|
token: void 0,
|
|
16
17
|
previewToken: void 0
|
|
17
18
|
};
|
|
18
19
|
}
|
|
20
|
+
const deliveryToken = orUndefined(
|
|
21
|
+
process.env.CONTENTFUL_ACCESS_TOKEN || process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN
|
|
22
|
+
);
|
|
19
23
|
return {
|
|
20
24
|
space: orUndefined(
|
|
21
25
|
process.env.CONTENTFUL_SPACE_ID || process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID
|
|
@@ -23,12 +27,10 @@ function readEnvSettings() {
|
|
|
23
27
|
environment: orUndefined(
|
|
24
28
|
process.env.CONTENTFUL_ENVIRONMENT || process.env.NEXT_PUBLIC_CONTENTFUL_ENVIRONMENT
|
|
25
29
|
),
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
previewToken: orUndefined(
|
|
30
|
-
process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN || process.env.NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN
|
|
31
|
-
)
|
|
30
|
+
deliveryToken,
|
|
31
|
+
token: deliveryToken,
|
|
32
|
+
// No `NEXT_PUBLIC_` fallback: see the note at the top of this file.
|
|
33
|
+
previewToken: orUndefined(process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN)
|
|
32
34
|
};
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -673,7 +675,7 @@ the library reads them at runtime.`;
|
|
|
673
675
|
function resolveConfig(args) {
|
|
674
676
|
const env = readEnvSettings();
|
|
675
677
|
const space = args.values.space ?? env.space;
|
|
676
|
-
const token = args.values.token ?? env.
|
|
678
|
+
const token = args.values.token ?? env.deliveryToken;
|
|
677
679
|
const environment = args.values.environment ?? env.environment ?? "master";
|
|
678
680
|
const missing = [];
|
|
679
681
|
if (!space) {
|
|
@@ -916,7 +918,10 @@ async function dispatch(argv, io) {
|
|
|
916
918
|
}
|
|
917
919
|
function knownTokens(argv) {
|
|
918
920
|
const env = readEnvSettings();
|
|
919
|
-
const tokens = [
|
|
921
|
+
const tokens = [
|
|
922
|
+
env.deliveryToken,
|
|
923
|
+
env.previewToken
|
|
924
|
+
];
|
|
920
925
|
for (const [index, argument] of argv.entries()) {
|
|
921
926
|
if (argument.startsWith("--token=")) tokens.push(argument.slice(8));
|
|
922
927
|
else if (argument === "--token") tokens.push(argv[index + 1]);
|