Skip to content

Commit f116d7e

Browse files
committed
remove io-ts, use zod
1 parent 27207a1 commit f116d7e

File tree

4 files changed

+136
-255
lines changed

4 files changed

+136
-255
lines changed

packages/fields-document/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,6 @@
6363
"@types/react": "catalog:",
6464
"apply-ref": "^1.0.0",
6565
"graphql": "catalog:",
66-
"io-ts": "^2.2.16",
67-
"io-ts-excess": "^1.0.1",
6866
"is-hotkey": "^0.2.0",
6967
"match-sorter": "^6.3.1",
7068
"mdast-util-from-markdown": "^0.8.5",
@@ -77,7 +75,8 @@
7775
"scroll-into-view-if-needed": "^3.0.0",
7876
"slate": "^0.103.0",
7977
"slate-history": "^0.100.0",
80-
"slate-react": "^0.107.0"
78+
"slate-react": "^0.107.0",
79+
"zod": "^3.23.8"
8180
},
8281
"devDependencies": {
8382
"@keystone-6/core": "workspace:^",
Lines changed: 130 additions & 221 deletions
Original file line numberDiff line numberDiff line change
@@ -1,232 +1,141 @@
1-
import * as t from 'io-ts'
2-
import excess from 'io-ts-excess'
1+
import { z } from 'zod'
32
import { type RelationshipData } from './DocumentEditor/component-blocks/api-shared'
4-
import { type Mark } from './DocumentEditor/utils'
53
import { isValidURL } from './DocumentEditor/isValidURL'
6-
// note that this validation isn't about ensuring that a document has nodes in the right positions and things
7-
// it's just about validating that it's a valid slate structure
8-
// we'll then run normalize on it which will enforce more things
9-
const markValue = t.union([t.undefined, t.literal(true)])
10-
11-
const text: t.Type<TextWithMarks> = excess(
12-
t.type({
13-
text: t.string,
14-
bold: markValue,
15-
italic: markValue,
16-
underline: markValue,
17-
strikethrough: markValue,
18-
code: markValue,
19-
superscript: markValue,
20-
subscript: markValue,
21-
keyboard: markValue,
22-
insertMenu: markValue,
23-
})
24-
)
25-
export type TextWithMarks = { type?: never, text: string } & {
26-
[Key in Mark | 'insertMenu']: true | undefined;
27-
}
28-
29-
type Inline = TextWithMarks | Link | Relationship
30-
31-
type Link = { type: 'link', href: string, children: Children }
32-
33-
class URLType extends t.Type<string> {
34-
readonly _tag: 'URLType' = 'URLType' as const
35-
constructor () {
36-
super(
37-
'string',
38-
(u): u is string => typeof u === 'string' && isValidURL(u),
39-
(u, c) => (this.is(u) ? t.success(u) : t.failure(u, c)),
40-
t.identity
41-
)
42-
}
43-
}
44-
45-
const urlType = new URLType()
46-
47-
const link: t.Type<Link> = t.recursion('Link', () =>
48-
excess(
49-
t.type({
50-
type: t.literal('link'),
51-
href: urlType,
52-
children,
53-
})
54-
)
55-
)
56-
57-
type Relationship = {
58-
type: 'relationship'
59-
relationship: string
60-
data: RelationshipData | null
61-
children: Children
62-
}
63-
64-
const relationship: t.Type<Relationship> = t.recursion('Relationship', () =>
65-
excess(
66-
t.type({
67-
type: t.literal('relationship'),
68-
relationship: t.string,
69-
data: t.union([t.null, relationshipData]),
70-
children,
71-
})
72-
)
73-
)
74-
75-
const inline = t.union([text, link, relationship])
76-
77-
type Children = (Block | Inline)[]
78-
79-
const layoutArea: t.Type<Layout> = t.recursion('Layout', () =>
80-
excess(
81-
t.type({
82-
type: t.literal('layout'),
83-
layout: t.array(t.number),
84-
children,
85-
})
86-
)
87-
)
88-
89-
type Layout = {
90-
type: 'layout'
91-
layout: number[]
92-
children: Children
93-
}
94-
95-
const onlyChildrenElements: t.Type<OnlyChildrenElements> = t.recursion('OnlyChildrenElements', () =>
96-
excess(
97-
t.type({
98-
type: t.union([
99-
t.literal('blockquote'),
100-
t.literal('layout-area'),
101-
t.literal('code'),
102-
t.literal('divider'),
103-
t.literal('list-item'),
104-
t.literal('list-item-content'),
105-
t.literal('ordered-list'),
106-
t.literal('unordered-list'),
107-
]),
108-
children,
109-
})
110-
)
111-
)
112-
113-
type OnlyChildrenElements = {
114-
type:
115-
| 'blockquote'
116-
| 'layout-area'
117-
| 'code'
118-
| 'divider'
119-
| 'list-item'
120-
| 'list-item-content'
121-
| 'ordered-list'
122-
| 'unordered-list'
123-
children: Children
124-
}
125-
126-
const textAlign = t.union([t.undefined, t.literal('center'), t.literal('end')])
127-
128-
const heading: t.Type<Heading> = t.recursion('Heading', () =>
129-
excess(
130-
t.type({
131-
type: t.literal('heading'),
132-
textAlign,
133-
level: t.union([
134-
t.literal(1),
135-
t.literal(2),
136-
t.literal(3),
137-
t.literal(4),
138-
t.literal(5),
139-
t.literal(6),
140-
]),
141-
children,
142-
})
143-
)
144-
)
145-
146-
type Heading = {
147-
type: 'heading'
148-
level: 1 | 2 | 3 | 4 | 5 | 6
149-
textAlign: 'center' | 'end' | undefined
150-
children: Children
151-
}
152-
153-
type Paragraph = {
154-
type: 'paragraph'
155-
textAlign: 'center' | 'end' | undefined
156-
children: Children
157-
}
158-
159-
const paragraph: t.Type<Paragraph> = t.recursion('Paragraph', () =>
160-
excess(
161-
t.type({
162-
type: t.literal('paragraph'),
163-
textAlign,
164-
children,
165-
})
166-
)
167-
)
168-
169-
const relationshipData: t.Type<RelationshipData> = excess(
170-
t.type({
171-
id: t.string,
172-
label: t.union([t.undefined, t.string]),
173-
data: t.union([t.undefined, t.record(t.string, t.any)]),
174-
})
175-
)
176-
177-
type ComponentBlock = {
178-
type: 'component-block'
179-
component: string
180-
props: Record<string, any>
181-
children: Children
182-
}
183-
184-
const componentBlock: t.Type<ComponentBlock> = t.recursion('ComponentBlock', () =>
185-
excess(
186-
t.type({
187-
type: t.literal('component-block'),
188-
component: t.string,
189-
props: t.record(t.string, t.any),
190-
children,
191-
})
192-
)
193-
)
194-
195-
type ComponentProp = {
196-
type: 'component-inline-prop' | 'component-block-prop'
197-
propPath: (string | number)[] | undefined
198-
children: Children
199-
}
200-
201-
const componentProp: t.Type<ComponentProp> = t.recursion('ComponentProp', () =>
202-
excess(
203-
t.type({
204-
type: t.union([t.literal('component-inline-prop'), t.literal('component-block-prop')]),
205-
propPath: t.union([t.array(t.union([t.string, t.number])), t.undefined]),
206-
children,
207-
})
208-
)
209-
)
210-
211-
type Block = Layout | OnlyChildrenElements | Heading | ComponentBlock | ComponentProp | Paragraph
212-
213-
const block: t.Type<Block> = t.recursion('Element', () =>
214-
t.union([layoutArea, onlyChildrenElements, heading, componentBlock, componentProp, paragraph])
215-
)
216-
217-
export type ElementFromValidation = Block | Inline
218-
219-
const children: t.Type<Children> = t.recursion('Children', () => t.array(t.union([block, inline])))
2204

221-
export const editorCodec = t.array(block)
5+
// leaf types
6+
const zMarkValue = z.union([
7+
z.undefined(),
8+
z.literal(true)
9+
])
10+
11+
const zText = z.object({
12+
text: z.string(),
13+
bold: zMarkValue,
14+
italic: zMarkValue,
15+
underline: zMarkValue,
16+
strikethrough: zMarkValue,
17+
code: zMarkValue,
18+
superscript: zMarkValue,
19+
subscript: zMarkValue,
20+
keyboard: zMarkValue,
21+
insertMenu: zMarkValue,
22+
}).strict()
23+
24+
const zTextAlign = z.union([
25+
z.undefined(),
26+
z.literal('center'),
27+
z.literal('end')
28+
])
29+
30+
// recursive types
31+
const zLink = z.object({
32+
type: z.literal('link'),
33+
href: z.string().refine(isValidURL),
34+
}).strict()
35+
36+
const zHeading = z.object({
37+
type: z.literal('heading'),
38+
textAlign: zTextAlign,
39+
level: z.union([
40+
z.literal(1),
41+
z.literal(2),
42+
z.literal(3),
43+
z.literal(4),
44+
z.literal(5),
45+
z.literal(6),
46+
]),
47+
}).strict()
48+
49+
const zParagraph = z.object({
50+
type: z.literal('paragraph'),
51+
textAlign: zTextAlign,
52+
}).strict()
53+
54+
const zElements = z.object({
55+
type: z.union([
56+
z.literal('blockquote'),
57+
z.literal('layout-area'),
58+
z.literal('code'),
59+
z.literal('divider'),
60+
z.literal('list-item'),
61+
z.literal('list-item-content'),
62+
z.literal('ordered-list'),
63+
z.literal('unordered-list'),
64+
]),
65+
}).strict()
66+
67+
const zLayout = z.object({
68+
type: z.literal('layout'),
69+
layout: z.array(z.number()),
70+
}).strict()
71+
72+
const zRelationshipData = z.object({
73+
id: z.string(),
74+
label: z.union([z.undefined(), z.string()]),
75+
data: z.union([z.undefined(), z.record(z.string(), z.any())]),
76+
}).strict()
77+
78+
const zRelationship = z.object({
79+
type: z.literal('relationship'),
80+
relationship: z.string(),
81+
data: z.union([z.null(), zRelationshipData]),
82+
}).strict()
83+
84+
const zComponentBlock = z.object({
85+
type: z.literal('component-block'),
86+
component: z.string(),
87+
props: z.record(z.string(), z.any()),
88+
}).strict()
89+
90+
const zComponentProp = z.object({
91+
type: z.union([z.literal('component-inline-prop'), z.literal('component-block-prop')]),
92+
propPath: z.union([z.array(z.union([z.string(), z.number()])), z.undefined()]),
93+
}).strict()
94+
95+
type Children =
96+
| z.infer<typeof zMarkValue>
97+
| z.infer<typeof zText>
98+
| z.infer<typeof zTextAlign>
99+
| z.infer<typeof zHeading> & { children: Children[] }
100+
| z.infer<typeof zParagraph> & { children: Children[] }
101+
| z.infer<typeof zLink> & { children: Children[] }
102+
| z.infer<typeof zElements & { children: Children[] }>
103+
| z.infer<typeof zLayout & { children: Children[] } >
104+
| z.infer<typeof zRelationship> & { children: Children[] }
105+
| z.infer<typeof zComponentBlock> & { children: Children[] }
106+
| z.infer<typeof zComponentProp> & { children: Children[] }
107+
108+
const zBlock: z.ZodType<Children> = z.union([
109+
zLayout.extend({ children: z.lazy(() => zChildren) }),
110+
zElements.extend({ children: z.lazy(() => zChildren) }),
111+
zHeading.extend({ children: z.lazy(() => zChildren) }),
112+
zComponentBlock.extend({ children: z.lazy(() => zChildren) }),
113+
zComponentProp.extend({ children: z.lazy(() => zChildren) }),
114+
zParagraph.extend({ children: z.lazy(() => zChildren) }),
115+
])
116+
117+
const zInline: z.ZodType<Children> = z.union([
118+
zText,
119+
zLink.extend({ children: z.lazy(() => zChildren) }),
120+
zRelationship.extend({ children: z.lazy(() => zChildren) }),
121+
])
122+
123+
const zChildren: z.ZodType<Children[]> = z.array(z.union([
124+
zBlock,
125+
zInline,
126+
]))
127+
128+
const zEditorCodec = z.array(zBlock)
129+
130+
export type ElementFromValidation = z.infer<typeof zChildren>[number]
222131

223132
export function isRelationshipData (val: unknown): val is RelationshipData {
224-
return relationshipData.validate(val, [])._tag === 'Right'
133+
return zRelationshipData.safeParse(val).success
225134
}
226135

227136
export function validateDocumentStructure (val: unknown): asserts val is ElementFromValidation[] {
228-
const result = editorCodec.validate(val, [])
229-
if (result._tag === 'Left') {
137+
const result = zEditorCodec.safeParse(val)
138+
if (!result.success) {
230139
throw new Error('Invalid document structure')
231140
}
232141
}

packages/fields-document/src/validation.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const componentBlocks: Record<string, ComponentBlock> = {
5252
}),
5353
}
5454

55-
const validate = (val: unknown) => {
55+
function validate (val: unknown) {
5656
try {
5757
const node = validateAndNormalizeDocument(
5858
val,

0 commit comments

Comments
 (0)