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