@fourtwelvelabs/fetch-contentful 0.1.0 → 0.3.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 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.mjs"}
package/docs/tada.md ADDED
@@ -0,0 +1,327 @@
1
+ # Typed queries with gql.tada
2
+
3
+ [gql.tada](https://gql-tada.0no.co) infers TypeScript types from your GraphQL
4
+ documents *as you write them* — no codegen step in your dev loop, no
5
+ hand-written result interfaces that drift from the query. This package ships
6
+ a companion that points it at a Contentful space: one command writes the
7
+ schema, the editor plugin config and the `graphql` helper, and
8
+ `fetchContentful` infers both the result and the variables from whatever
9
+ document you hand it.
10
+
11
+ - [Five-minute quickstart](#five-minute-quickstart)
12
+ - [What you get](#what-you-get)
13
+ - [Manual setup](#manual-setup)
14
+ - [Keeping the schema fresh](#keeping-the-schema-fresh)
15
+ - [CI recipe](#ci-recipe)
16
+ - [CLI reference](#cli-reference)
17
+
18
+ ## Five-minute quickstart
19
+
20
+ **1. Install the peers.** gql.tada and its TypeScript plugin are optional
21
+ peer dependencies — they only matter at author time, so they belong in
22
+ `devDependencies`:
23
+
24
+ ```bash
25
+ yarn add -D gql.tada @0no-co/graphqlsp
26
+ ```
27
+
28
+ **2. Run `tada-init`.** With `CONTENTFUL_SPACE_ID` and
29
+ `CONTENTFUL_ACCESS_TOKEN` already in your environment (the same variables the
30
+ library reads at runtime — see the README's Configuration section), this
31
+ needs no arguments:
32
+
33
+ ```bash
34
+ npx @fourtwelvelabs/fetch-contentful tada-init
35
+ ```
36
+
37
+ ```
38
+ Configuring gql.tada for space abc123, environment master.
39
+ created contentful-schema.graphql (412.7 kB)
40
+ updated tsconfig.json
41
+ created src/graphql.ts
42
+ ```
43
+
44
+ It downloads the schema as SDL, adds `@0no-co/graphqlsp` to
45
+ `compilerOptions.plugins` in your `tsconfig.json`, and writes `src/graphql.ts`.
46
+ Add `--dry-run` first if you'd rather see the changes before they happen.
47
+
48
+ That last file is the one you import from as you write queries. It's yours —
49
+ commit it, edit it — and it exists because it has to be bound to *your*
50
+ space's schema, so it could never ship inside the package:
51
+
52
+ ```ts
53
+ // src/graphql.ts
54
+ import { initGraphQLTada } from 'gql.tada';
55
+ import type { ContentfulScalars } from '@fourtwelvelabs/fetch-contentful/tada';
56
+ import type { introspection } from './contentful-env.d.ts';
57
+
58
+ export const graphql = initGraphQLTada<{
59
+ introspection: introspection;
60
+ scalars: ContentfulScalars;
61
+ }>();
62
+
63
+ export { readFragment } from 'gql.tada';
64
+ export type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada';
65
+ ```
66
+
67
+ `graphql` is a tagged template function that does two jobs at once: at runtime
68
+ it parses your query into a GraphQL document, and at compile time it infers
69
+ that query's result and variable types by matching it against `introspection`.
70
+ See [Manual setup](#manual-setup) for writing this file yourself.
71
+
72
+ **3. Restart the TypeScript server** so the editor picks up the plugin. In
73
+ VS Code: <kbd>⌘⇧P</kbd> → *TypeScript: Restart TS Server*.
74
+
75
+ **4. Write a query**, importing `graphql` from the file step 2 generated.
76
+ Autocomplete, field validation and types all come from your space's real
77
+ content model:
78
+
79
+ ```ts
80
+ // src/lib/pages.ts
81
+ import { fetchContentful } from '@fourtwelvelabs/fetch-contentful';
82
+ import { graphql } from '../graphql';
83
+
84
+ const PageQuery = graphql(`
85
+ query Page($slug: String!) {
86
+ pageCollection(where: { slug: $slug }, limit: 1) {
87
+ items {
88
+ title
89
+ publishedAt
90
+ sectionsCollection {
91
+ items { heading }
92
+ }
93
+ }
94
+ }
95
+ }
96
+ `);
97
+
98
+ const pages = await fetchContentful(PageQuery, { variables: { slug: 'home' } });
99
+ // ^? Array<{ title: string | null; publishedAt: string | null;
100
+ // sections: Array<{ heading: string | null }> }> | null
101
+ ```
102
+
103
+ No type arguments, no result interface, no cast.
104
+
105
+ ## What you get
106
+
107
+ **The result type follows the response you actually receive.** gql.tada types
108
+ the raw wire shape; `fetchContentful` then applies its own shaping and
109
+ unwrapping to *that* type, so `pageCollection.items` arrives as `pages` — an
110
+ array — in both the value and the type. `shapeResponseData: false` and
111
+ `unwrapRootField: false` are reflected too:
112
+
113
+ ```ts
114
+ const raw = await fetchContentful(PageQuery, {
115
+ variables: { slug: 'home' },
116
+ shapeResponseData: false,
117
+ unwrapRootField: false,
118
+ });
119
+ // ^? { pageCollection: { items: [...] } | null }
120
+ ```
121
+
122
+ **Variables are required when — and only when — you have to supply them.**
123
+ `$preview` and `$locale` are injected by the library, so a document that
124
+ declares only those needs no `variables` at all. Anything else is enforced,
125
+ and a typo is a compile error rather than a silently dropped argument:
126
+
127
+ ```ts
128
+ await fetchContentful(PageQuery);
129
+ // ~~~~~~~~~ `variables` is required: $slug
130
+
131
+ await fetchContentful(PageQuery, { variables: { slgu: 'home' } });
132
+ // ~~~~ not a variable of this document
133
+ ```
134
+
135
+ **Contentful's custom scalars are typed**, via the `ContentfulScalars` map
136
+ that `tada-init` wires up. Without it every one of them would be `unknown`:
137
+
138
+ | Scalar | TypeScript |
139
+ | --- | --- |
140
+ | `DateTime` | `string` (ISO 8601 at UTC) |
141
+ | `Dimension` | `number` (1–4000, image transforms) |
142
+ | `HexColor` | `string` (`"rgb:ffffff"`) |
143
+ | `JSON` | `JsonValue` (recursive JSON) |
144
+ | `Quality` | `number` (1–100) |
145
+ | `Circle` | `{ lat, lon, radius }` — a `_within_circle` search area |
146
+ | `Rectangle` | `{ topLeftLat, topLeftLon, bottomRightLat, bottomRightLon }` |
147
+
148
+ `Circle` and `Rectangle` are geographic search areas used by location
149
+ filters, not numbers — Contentful rejects anything else.
150
+
151
+ **Fragments work as usual.** `tada-init` re-exports the helpers you need
152
+ from the generated module:
153
+
154
+ ```ts
155
+ import { readFragment, type FragmentOf } from './graphql';
156
+ ```
157
+
158
+ ## Manual setup
159
+
160
+ The CLI is a convenience, not a requirement. `ContentfulScalars` is exported
161
+ as a plain type, so you can hand it to `initGraphQLTada` yourself — useful if
162
+ you already generate the schema another way, or keep several spaces side by
163
+ side:
164
+
165
+ ```ts
166
+ // src/graphql.ts
167
+ import { initGraphQLTada } from 'gql.tada';
168
+ import type { ContentfulScalars } from '@fourtwelvelabs/fetch-contentful/tada';
169
+ import type { introspection } from './contentful-env.d.ts';
170
+
171
+ export const graphql = initGraphQLTada<{
172
+ introspection: introspection;
173
+ scalars: ContentfulScalars;
174
+ }>();
175
+
176
+ export { readFragment } from 'gql.tada';
177
+ export type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada';
178
+ ```
179
+
180
+ with, in `tsconfig.json`:
181
+
182
+ ```jsonc
183
+ {
184
+ "compilerOptions": {
185
+ "plugins": [
186
+ {
187
+ "name": "@0no-co/graphqlsp",
188
+ "schema": "./contentful-schema.graphql",
189
+ "tadaOutputLocation": "./src/contentful-env.d.ts"
190
+ }
191
+ ]
192
+ }
193
+ }
194
+ ```
195
+
196
+ The `/tada` entry point is types only — nothing in it imports gql.tada, and
197
+ the built module is empty — so importing it adds nothing to your bundle.
198
+
199
+ `fetchContentful` targets `TypedDocumentNode`, which gql.tada's documents
200
+ satisfy structurally. Documents from graphql-codegen's client preset satisfy
201
+ it too, so everything above applies equally if you use codegen instead.
202
+
203
+ ## Keeping the schema fresh
204
+
205
+ The SDL file is a snapshot of your content model. After you publish a
206
+ content-type change, re-download it:
207
+
208
+ ```bash
209
+ npx @fourtwelvelabs/fetch-contentful tada-refresh
210
+ ```
211
+
212
+ If nothing changed, the file is left untouched — same bytes, same mtime, no
213
+ entry in `git status`. Commit `contentful-schema.graphql`: it's what makes a
214
+ fresh checkout type-check without network access.
215
+
216
+ The gql.tada output file (`src/contentful-env.d.ts`) is regenerated from the
217
+ SDL by gql.tada itself, either by the editor plugin or with
218
+ `npx gql.tada generate-output`.
219
+
220
+ ## CI recipe
221
+
222
+ Contentful can tell you when the content model changes; the schema file can
223
+ then update itself through a pull request rather than by someone remembering
224
+ to run a command.
225
+
226
+ **1. Add a Contentful webhook.** In *Settings → Webhooks*, create one that
227
+ fires on **ContentType → publish** and **unpublish**, pointing at GitHub's
228
+ `repository_dispatch` endpoint:
229
+
230
+ - URL: `https://api.github.com/repos/<owner>/<repo>/dispatches`
231
+ - Method: `POST`
232
+ - Headers: `Accept: application/vnd.github+json`,
233
+ `Authorization: Bearer <a GitHub token with contents:write>`
234
+ - Payload (custom): `{ "event_type": "contentful-schema-changed" }`
235
+
236
+ **2. Add the workflow:**
237
+
238
+ ```yaml
239
+ # .github/workflows/contentful-schema.yml
240
+ name: Refresh Contentful schema
241
+
242
+ on:
243
+ repository_dispatch:
244
+ types: [contentful-schema-changed]
245
+ schedule:
246
+ - cron: '0 6 * * 1' # a Monday safety net, in case a webhook is missed
247
+ workflow_dispatch:
248
+
249
+ jobs:
250
+ refresh:
251
+ runs-on: ubuntu-latest
252
+ permissions:
253
+ contents: write
254
+ pull-requests: write
255
+ steps:
256
+ - uses: actions/checkout@v4
257
+ - uses: actions/setup-node@v4
258
+ with:
259
+ node-version: 22
260
+ cache: yarn
261
+ - run: yarn install --immutable
262
+
263
+ - run: yarn fetch-contentful tada-refresh
264
+ env:
265
+ CONTENTFUL_SPACE_ID: ${{ secrets.CONTENTFUL_SPACE_ID }}
266
+ CONTENTFUL_ACCESS_TOKEN: ${{ secrets.CONTENTFUL_ACCESS_TOKEN }}
267
+
268
+ # gql.tada's generated types follow the SDL.
269
+ - run: yarn gql.tada generate-output
270
+
271
+ # Fails the PR if the new schema broke an existing query.
272
+ - run: yarn tsc --noEmit
273
+ continue-on-error: true
274
+
275
+ - uses: peter-evans/create-pull-request@v7
276
+ with:
277
+ title: 'chore: refresh Contentful schema'
278
+ body: |
279
+ The Contentful content model changed. Review the schema diff and
280
+ check that every query still type-checks.
281
+ branch: chore/contentful-schema
282
+ commit-message: 'chore: refresh Contentful schema'
283
+ add-paths: |
284
+ contentful-schema.graphql
285
+ src/contentful-env.d.ts
286
+ ```
287
+
288
+ `create-pull-request` opens a PR only when something actually changed, and
289
+ `tada-refresh` writes nothing when the schema is identical — so a no-op
290
+ webhook costs one CI run and produces no noise.
291
+
292
+ Use a **Content Delivery API** token here, not a Content Management one: it
293
+ is read-only, and introspection is all this needs.
294
+
295
+ ## CLI reference
296
+
297
+ ```
298
+ fetch-contentful tada-init [options] Set up gql.tada in this project
299
+ fetch-contentful tada-refresh [options] Re-download the schema only
300
+ ```
301
+
302
+ Both commands accept the same options. Flags win over environment variables.
303
+
304
+ | Option | Default | Notes |
305
+ | --- | --- | --- |
306
+ | `--space <id>` | `CONTENTFUL_SPACE_ID` | Required |
307
+ | `--environment <id>` | `CONTENTFUL_ENVIRONMENT`, then `master` | |
308
+ | `--token <token>` | `CONTENTFUL_ACCESS_TOKEN` | Content Delivery API token |
309
+ | `--schema-path <path>` | `./contentful-schema.graphql` | Where the SDL is written |
310
+ | `--tada-output <path>` | `./src/contentful-env.d.ts` | Where gql.tada writes its types |
311
+ | `--graphql-file <path>` | `./src/graphql.ts` | `tada-init` only |
312
+ | `--tsconfig <path>` | `./tsconfig.json` | `tada-init` only |
313
+ | `--dry-run` | | Print the changes; write nothing |
314
+ | `--force` | | Overwrite an existing graphql file |
315
+ | `--cwd <path>` | | Run against another directory |
316
+ | `-h`, `--help` | | |
317
+
318
+ The `NEXT_PUBLIC_`-prefixed variable names are read as fallbacks, exactly as
319
+ the library reads them at runtime.
320
+
321
+ `tada-init` is safe to run repeatedly. It updates the graphqlsp entry in your
322
+ tsconfig in place rather than adding a second one, preserves your comments
323
+ and formatting, and never overwrites `graphql.ts` unless you pass `--force`.
324
+ If it cannot patch a tsconfig safely, it changes nothing and prints the block
325
+ for you to paste in.
326
+
327
+ The access token is never printed — not in output, not in error messages.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fourtwelvelabs/fetch-contentful",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Foolproof, type-safe GraphQL fetching utility for Contentful with automatic query splitting, retries, and response shaping.",
5
5
  "keywords": [
6
6
  "contentful",
@@ -22,6 +22,7 @@
22
22
  },
23
23
  "type": "module",
24
24
  "sideEffects": false,
25
+ "bin": "./dist/cli/index.mjs",
25
26
  "main": "./dist/index.cjs",
26
27
  "module": "./dist/index.mjs",
27
28
  "types": "./dist/index.d.ts",
@@ -36,10 +37,21 @@
36
37
  "default": "./dist/index.cjs"
37
38
  }
38
39
  },
40
+ "./tada": {
41
+ "import": {
42
+ "types": "./dist/tada/index.d.ts",
43
+ "default": "./dist/tada/index.mjs"
44
+ },
45
+ "require": {
46
+ "types": "./dist/tada/index.d.cts",
47
+ "default": "./dist/tada/index.cjs"
48
+ }
49
+ },
39
50
  "./package.json": "./package.json"
40
51
  },
41
52
  "files": [
42
53
  "dist",
54
+ "docs",
43
55
  "README.md",
44
56
  "LICENSE"
45
57
  ],
@@ -59,18 +71,32 @@
59
71
  "test:coverage": "vitest run --coverage",
60
72
  "clean": "rm -rf dist .turbo"
61
73
  },
74
+ "dependencies": {
75
+ "@graphql-typed-document-node/core": "^3.2.0"
76
+ },
62
77
  "peerDependencies": {
78
+ "@0no-co/graphqlsp": "^1.12.0",
79
+ "gql.tada": "^1.8.0",
63
80
  "graphql": "^16.14.2 || ^17.0.0"
64
81
  },
82
+ "peerDependenciesMeta": {
83
+ "@0no-co/graphqlsp": {
84
+ "optional": true
85
+ },
86
+ "gql.tada": {
87
+ "optional": true
88
+ }
89
+ },
65
90
  "devDependencies": {
66
91
  "@repo/eslint-config": "*",
67
92
  "@repo/typescript-config": "*",
68
93
  "@types/node": "^22.15.3",
69
94
  "@vitest/coverage-v8": "^2.1.9",
70
95
  "eslint": "^9.39.1",
96
+ "gql.tada": "^1.11.3",
71
97
  "graphql": "^16.14.2",
72
98
  "tsup": "^8.5.1",
73
99
  "typescript": "5.9.2",
74
100
  "vitest": "^2.1.9"
75
101
  }
76
- }
102
+ }