Skip to content

Commit

Permalink
chore(add-zod): Replace .nonempty with .refine(...refinementNonEmpty)
Browse files Browse the repository at this point in the history
z.array().nonempty() influences the type definitions, and causes issues with Array.prototype.map() not being aware of the underlying array's cardinality.
  • Loading branch information
FlorianWendelborn committed Sep 24, 2021
1 parent df0174d commit 1639884
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 5 deletions.
2 changes: 1 addition & 1 deletion packages/documentation/components/NavBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export default defineComponent({
: ''
}`,
}),
) as unknown as Kotti.Navbar.Section['links'], // TS doesn’t understand the non-empty arrays without the cast due to the .map
),
title: section.title,
}),
),
Expand Down
6 changes: 4 additions & 2 deletions packages/kotti-ui/source/kotti-navbar/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { yocoIconSchema } from '@3yourmind/yoco'
import { z } from 'zod'

import { refinementNonEmpty } from '../zod-refinements'

export namespace KottiNavbar {
export const notificationSchema = z.object({
count: z.number().int(),
Expand All @@ -26,7 +28,7 @@ export namespace KottiNavbar {
export type SectionLink = z.infer<typeof sectionLinkSchema>

export const sectionSchema = z.object({
links: z.array(sectionLinkSchema).nonempty(),
links: z.array(sectionLinkSchema).refine(...refinementNonEmpty),
title: z.string().nullable(),
})
export type Section = z.infer<typeof sectionSchema>
Expand All @@ -43,7 +45,7 @@ export namespace KottiNavbar {
logoUrl: z.string(),
notification: notificationSchema.nullable().default(null),
quickLinks: z.array(quickLinkSchema).default(() => []),
sections: z.array(sectionSchema).nonempty(),
sections: z.array(sectionSchema).refine(...refinementNonEmpty),
theme: themeSchema.nullable().default(null),
})

Expand Down
6 changes: 4 additions & 2 deletions packages/kotti-ui/source/kotti-user-menu/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { z } from 'zod'

import { refinementNonEmpty } from '../zod-refinements'

export namespace KottiUserMenu {
export const sectionLinkSchema = z
.object({
Expand All @@ -10,14 +12,14 @@ export namespace KottiUserMenu {
export type SectionLink = z.infer<typeof sectionLinkSchema>

export const sectionSchema = z.object({
links: z.array(sectionLinkSchema).nonempty(),
links: z.array(sectionLinkSchema).refine(...refinementNonEmpty),
title: z.string().nullable(),
})

export type Section = z.infer<typeof sectionSchema>

export const propsSchema = z.object({
sections: z.array(sectionSchema).nonempty(),
sections: z.array(sectionSchema).refine(...refinementNonEmpty),
userAvatar: z.string().nullable().default(null),
userName: z.string().nullable().default(null),
userStatus: z.string(),
Expand Down
21 changes: 21 additions & 0 deletions packages/kotti-ui/source/make-props.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { z } from 'zod'

import { makeProps } from './make-props'
import { silenceConsole } from './test-utils/silence-console'
import { refinementNonEmpty } from './zod-refinements'

declare global {
namespace jest {
Expand Down Expand Up @@ -176,6 +177,26 @@ describe('array', () => {
expect(prop).toValidate(...ARRAY_SUCCESS, null, undefined)
expect(prop).not.toValidate(...ARRAY_FAILURE)
})

it('generates vue prop for schema “z.array(z.number()).nullable().default().refine(...refinementNonEmpty)”', () => {
const schema = z.object({
prop: z
.array(z.number())
.refine(...refinementNonEmpty)
.nullable()
.default(null),
})
const { prop } = makeProps(schema)

expect(prop).toDefaultTo(null)
expect(prop).toBeType(Array)
expect(prop).toValidate(
...ARRAY_SUCCESS.filter((x) => x.length !== 0),
null,
undefined,
)
expect(prop).not.toValidate(...ARRAY_FAILURE, [])
})
})

describe('boolean', () => {
Expand Down
12 changes: 12 additions & 0 deletions packages/kotti-ui/source/make-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,18 @@ const walkSchemaTypes = <SCHEMA extends z.ZodTypeAny>(
return result
}

// e.g. z.refine()
case z.ZodFirstPartyTypeKind.ZodEffects: {
const def = schema._def as z.ZodEffectsDef

if (DEBUG_WALK_SCHEMA_TYPES)
console.log(
`walkSchemaTypes: walking schema of “ZodFirstPartyTypeKind.ZodEffects”`,
)

return walkSchemaTypes(def.schema)
}

case z.ZodFirstPartyTypeKind.ZodDefault:
case z.ZodFirstPartyTypeKind.ZodNullable:
case z.ZodFirstPartyTypeKind.ZodOptional: {
Expand Down
24 changes: 24 additions & 0 deletions packages/kotti-ui/source/zod-refinements.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { z } from 'zod'

import { refinementNonEmpty } from './zod-refinements'

describe('refinementNonEmpty', () => {
const schema = z.array(z.number()).refine(...refinementNonEmpty)

it('works for arrays > 1', () => {
expect(schema.safeParse([1])).toEqual({ data: [1], success: true })
})

it('rejects arrays without items', () => {
const result = schema.safeParse([])

expect(result.success).toBe(false)

// type hint
if ('error' in result)
expect(result.error.errors).toEqual([
{ code: 'custom', message: 'array may not be empty', path: [] },
])
else throw new Error('error.result doesn’t exist')
})
})
18 changes: 18 additions & 0 deletions packages/kotti-ui/source/zod-refinements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { z } from 'zod'

/**
* Similar to z.array().nonempty(), but doesn’t alter type definitions
*
* We previously had issues with `.nonempty()` causing arrays generated from
* `Array.prototype.map` to be detected as possibly empty.
*
* @example
*
* z.array(z.number()).refine(...refinementNonEmpty)
*/
export const refinementNonEmpty: Parameters<
z.ZodArray<z.ZodAny, 'many'>['refine']
> = [
(array: unknown[]) => array.length > 0,
{ message: 'array may not be empty' },
]

0 comments on commit 1639884

Please sign in to comment.