@xndrjs/contentful-to-zod 0.1.0-alpha.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/README.md +207 -0
- package/dist/cli.js +983 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +160 -0
- package/dist/index.js +751 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# @xndrjs/contentful-to-zod
|
|
2
|
+
|
|
3
|
+
Generate **Zod 4** schemas from Contentful content types (CMA). Stop hand-writing codegen and get precise `z.infer` types where graphql-codegen stays on `string`.
|
|
4
|
+
|
|
5
|
+
This package outputs **Zod schemas and optional locale helpers only** — no `domain.shape` in the generated file. If you use [xndrjs](https://github.com/xndrjs/toolkit), wire schemas with `zodToValidator` from [`@xndrjs/domain-zod`](../domain-zod) in your own code.
|
|
6
|
+
|
|
7
|
+
## Principles
|
|
8
|
+
|
|
9
|
+
- **1:1 mapping** from the CMA content model to Zod (field type + `validations` + `required`).
|
|
10
|
+
- **Two schema shapes** (configurable): **flat / CMA** (single value per field) and **delivery** (`localized: true` → `z.record(ContentfulLocaleCodeSchema, T)`).
|
|
11
|
+
- **Default `locale.mode: "both"`** — each content type exports flat + delivery schemas (e.g. `BlogPostSchema` + `BlogPostDeliverySchema`) plus `flatten*` helpers when both are emitted.
|
|
12
|
+
- **Locales from your space** — enum and constants are generated from a CMA `/locales` snapshot; `CONTENTFUL_DEFAULT_LOCALE` is only the default parameter for helpers (no runtime rule that the default locale must exist in every record).
|
|
13
|
+
- **Self-contained output** — generated file depends only on `zod`; shared primitives (entry/asset links, location, …) are inlined once at the top.
|
|
14
|
+
- **Optional Object overrides** — CMA declares `Object` without inner shape; supply Zod schemas via config for `{contentTypeId}.{fieldId}` keys.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add @xndrjs/contentful-to-zod zod@^4
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## CLI
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
contentful-to-zod \
|
|
26
|
+
--space-id $SPACE \
|
|
27
|
+
--environment master \
|
|
28
|
+
--management-token $TOKEN \
|
|
29
|
+
--out ./src/generated/contentful.schemas.ts \
|
|
30
|
+
--snapshot ./src/generated/content-types.json \
|
|
31
|
+
--snapshot-locales ./src/generated/locales.json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Offline / CI (no CMA calls):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
contentful-to-zod \
|
|
38
|
+
--from-snapshot \
|
|
39
|
+
--snapshot ./src/generated/content-types.json \
|
|
40
|
+
--snapshot-locales ./src/generated/locales.json \
|
|
41
|
+
--out ./src/generated/contentful.schemas.ts
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Other flags: `--content-types blogPost,author`, `--config ./contentful-to-zod.config.ts`, `--dry-run` (print to stdout).
|
|
45
|
+
|
|
46
|
+
Environment fallbacks: `CONTENTFUL_MANAGEMENT_TOKEN`, `CONTENTFUL_SPACE_ID`, `CONTENTFUL_ENVIRONMENT`.
|
|
47
|
+
|
|
48
|
+
`--snapshot-locales` is required when using `--from-snapshot` and locale mode is `delivery` or `both` (the default).
|
|
49
|
+
|
|
50
|
+
## Programmatic API
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { fetchContentTypes, fetchLocales, generateZodSchemas } from "@xndrjs/contentful-to-zod";
|
|
54
|
+
import { writeFile } from "node:fs/promises";
|
|
55
|
+
|
|
56
|
+
const cma = { spaceId, accessToken, environmentId: "master" };
|
|
57
|
+
|
|
58
|
+
const [contentTypes, locales] = await Promise.all([fetchContentTypes(cma), fetchLocales(cma)]);
|
|
59
|
+
|
|
60
|
+
const source = generateZodSchemas(contentTypes, {
|
|
61
|
+
locales,
|
|
62
|
+
config: { locale: { mode: "both" } },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await writeFile("./src/generated/contentful.schemas.ts", source, "utf8");
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`generateZodSchemas` options: `contentTypeIds`, `locales` (required when mode is `delivery` or `both`), `localeMode`, `config`.
|
|
69
|
+
|
|
70
|
+
## Locale mode
|
|
71
|
+
|
|
72
|
+
In `contentful-to-zod.config.ts` (or `generateZodSchemas` options):
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { defineConfig } from "@xndrjs/contentful-to-zod";
|
|
76
|
+
|
|
77
|
+
export default defineConfig({
|
|
78
|
+
locale: {
|
|
79
|
+
/** Default: "both" */
|
|
80
|
+
mode: "both", // "cma" | "delivery" | "both"
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
| `locale.mode` | Generated exports |
|
|
86
|
+
| ------------------ | ----------------------------------------------------------------------- |
|
|
87
|
+
| `"cma"` | Flat schemas only (`BlogPostSchema`, `BlogPostFields`) |
|
|
88
|
+
| `"delivery"` | Delivery schemas + `pickLocale` + locale enum/constants |
|
|
89
|
+
| `"both"` (default) | Flat + delivery + `pickLocale` + `flatten{Type}Fields` per content type |
|
|
90
|
+
|
|
91
|
+
Rules:
|
|
92
|
+
|
|
93
|
+
- **Flat and delivery** field schemas are **nullable** (Preview/draft and `pickLocale` can yield `null`; optional CMA fields also `.optional()`).
|
|
94
|
+
- **`localized: true`** — flat uses `T`; delivery uses `z.record(ContentfulLocaleCodeSchema, T)` (same nullability rules on the outer field).
|
|
95
|
+
- **`disabled` / `omitted`** fields are still included (full blueprint).
|
|
96
|
+
|
|
97
|
+
### Generated locale primitives
|
|
98
|
+
|
|
99
|
+
When delivery or both mode is active, the file starts with:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
/** @generated from space locales snapshot */
|
|
103
|
+
export const ContentfulLocaleCodeSchema = z.enum(["en-US", "it-IT"]);
|
|
104
|
+
export type ContentfulLocaleCode = z.infer<typeof ContentfulLocaleCodeSchema>;
|
|
105
|
+
|
|
106
|
+
export const CONTENTFUL_LOCALE_CODES = ContentfulLocaleCodeSchema.options;
|
|
107
|
+
export const CONTENTFUL_DEFAULT_LOCALE = "en-US" as const;
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Flat vs delivery example
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
// flat / CMA — single value per field (nullable for pickLocale / flatten)
|
|
114
|
+
export const BlogPostSchema = z.object({
|
|
115
|
+
title: z.string().max(256).nullable(),
|
|
116
|
+
slug: z.string().nullable(),
|
|
117
|
+
author: ContentfulEntryLinkSchema.nullable().optional(),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
export type BlogPostFields = z.infer<typeof BlogPostSchema>;
|
|
121
|
+
|
|
122
|
+
// delivery — REST/Preview (all fields nullable; optional CMA fields also .optional())
|
|
123
|
+
export const BlogPostDeliverySchema = z.object({
|
|
124
|
+
title: z.record(ContentfulLocaleCodeSchema, z.string().max(256)).nullable(),
|
|
125
|
+
slug: z.string().nullable(),
|
|
126
|
+
author: ContentfulEntryLinkSchema.nullable().optional(),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
export type BlogPostDeliveryFields = z.infer<typeof BlogPostDeliverySchema>;
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Generated helpers
|
|
133
|
+
|
|
134
|
+
Helpers are pure functions in the same output file. They **do not validate** — parse after flattening:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { BlogPostSchema, flattenBlogPostFields, pickLocale } from "./generated/contentful.schemas";
|
|
138
|
+
|
|
139
|
+
const flat = flattenBlogPostFields(deliveryFields, "it-IT");
|
|
140
|
+
const post = BlogPostSchema.parse(flat);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
- **`pickLocale`** — read one locale from a localized delivery field (`Record<ContentfulLocaleCode, T> | null`); missing locale or `null` input → `null`. Default locale parameter is `CONTENTFUL_DEFAULT_LOCALE`.
|
|
144
|
+
- **`flatten{ContentType}Fields`** — map `*DeliveryFields` → flat `*Fields` for one locale (one per content type when `mode` is `both`). Passes `null` through for absent localized values.
|
|
145
|
+
|
|
146
|
+
There is no runtime dependency on `@xndrjs/contentful-to-zod` in production — only the generated file and `zod`.
|
|
147
|
+
|
|
148
|
+
## Object field overrides
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
// contentful-to-zod.config.ts
|
|
152
|
+
import { z } from "zod";
|
|
153
|
+
import { defineConfig } from "@xndrjs/contentful-to-zod";
|
|
154
|
+
|
|
155
|
+
export default defineConfig({
|
|
156
|
+
objects: {
|
|
157
|
+
"blogPost.metadata": z.object({
|
|
158
|
+
seoTitle: z.string(),
|
|
159
|
+
noIndex: z.boolean().optional(),
|
|
160
|
+
}),
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Overrides apply to the **base field type** `T`. In delivery mode, localized fields wrap `z.record(ContentfulLocaleCodeSchema, T).nullable()` around that base (plus `.optional()` when applicable).
|
|
166
|
+
|
|
167
|
+
Overrides are inlined at codegen time — the config is not imported at runtime.
|
|
168
|
+
|
|
169
|
+
## Mapping Delivery / REST data
|
|
170
|
+
|
|
171
|
+
1. Parse or type raw `fields` as `*DeliveryFields` (or use `flatten*` when `mode` is `both`).
|
|
172
|
+
2. Validate the flat shape with `*Schema.parse(...)`.
|
|
173
|
+
|
|
174
|
+
Entry/asset link objects and CMA validations (size, range, regex, etc.) are reflected in the generated Zod chains.
|
|
175
|
+
|
|
176
|
+
## xndrjs recipe (optional)
|
|
177
|
+
|
|
178
|
+
Wire flat field schemas and the locale enum into `@xndrjs/domain-zod`:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { domain, zodToValidator } from "@xndrjs/domain-zod";
|
|
182
|
+
import { BlogPostSchema, ContentfulLocaleCodeSchema } from "./generated/contentful.schemas";
|
|
183
|
+
|
|
184
|
+
export const BlogPost = domain.shape("BlogPost", zodToValidator(BlogPostSchema));
|
|
185
|
+
|
|
186
|
+
export const SupportedLocale = domain.primitive(
|
|
187
|
+
"SupportedLocale",
|
|
188
|
+
zodToValidator(ContentfulLocaleCodeSchema)
|
|
189
|
+
);
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Use `SupportedLocale` (or your own name) wherever application code should accept only locales known to the space snapshot.
|
|
193
|
+
|
|
194
|
+
## CMA field mapping (summary)
|
|
195
|
+
|
|
196
|
+
| CMA `type` | Zod base |
|
|
197
|
+
| ------------ | ------------------------------------------------------ |
|
|
198
|
+
| Symbol, Text | `z.string()` + validations |
|
|
199
|
+
| Integer | `z.number().int()` |
|
|
200
|
+
| Number | `z.number()` |
|
|
201
|
+
| Boolean | `z.boolean()` |
|
|
202
|
+
| Date | `z.string()` / `z.iso.datetime()` |
|
|
203
|
+
| Location | `z.object({ lat, lon })` |
|
|
204
|
+
| Object | `z.record(z.string(), z.unknown())` or config override |
|
|
205
|
+
| Link | Contentful link object |
|
|
206
|
+
| Array | `z.array(itemSchema)` |
|
|
207
|
+
| Rich Text | `z.looseObject({ nodeType: z.literal("document") })` |
|