Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BREAKING CHANGE: Removes errorMessagesForSchema utility #95

Merged
merged 1 commit into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 0 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ It does this by enforcing the parameters' types at runtime (through [zod](https:
- [Other error constructors](#other-error-constructors)
- [Using error messages in the UI](#using-error-messages-in-the-ui)
- [errorMessagesFor](#errormessagesfor)
- [errorMessagesForSchema](#errormessagesforschema)
- [Tracing](#tracing)
- [Combining domain functions](#combining-domain-functions)
- [all](#all)
Expand Down Expand Up @@ -286,28 +285,6 @@ errorMessagesFor(result.inputErrors, 'email') // will be an empty array: []
errorMessagesFor(result.environmentErrors, 'host')[0] === 'Must not be empty'
```

#### errorMessagesForSchema

Given an array of `SchemaError` -- be it from `inputErrors` or `environmentErrors` -- and a Zod Schema, `errorMessagesForSchema` returns an object with a list of error messages for each key in the schema's shape.

```tsx
const schema = z.object({ email: z.string().nonEmpty(), password: z.string().nonEmpty() })
const result = {
success: false,
errors: [],
inputErrors: [{ message: 'Must not be empty', path: ['email'] }, { message: 'Must be a string', path: ['email'] }, { message: 'Must not be empty', path: ['password'] }],
environmentErrors: []
}

errorMessagesForSchema(result.inputErrors, schema)
/*
{
email: ['Must not be empty', 'Must be a string'],
password: ['Must not be empty']
}
*/
```

### Tracing

Whenever you need to intercept inputs and a domain function result without changing them, there is a function called `trace` that can help you.
Expand Down
162 changes: 1 addition & 161 deletions src/errors.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import { describe, it } from 'https://deno.land/std@0.156.0/testing/bdd.ts'
import { assertEquals } from 'https://deno.land/std@0.117.0/testing/asserts.ts'
import { z } from 'https://deno.land/x/zod@v3.21.4/mod.ts'

import { makeDomainFunction } from './constructor.ts'

import {
errorMessagesFor,
errorMessagesForSchema,
schemaError,
} from './errors.ts'
import { errorMessagesFor, schemaError } from './errors.ts'

const errors = [
{ path: ['a'], message: 'a' },
Expand Down Expand Up @@ -39,156 +32,3 @@ describe('errorMessagesFor', () => {
assertEquals(errorMessagesFor(err, 'person.0.email'), ['Invalid email'])
})
})

const schema = z.object({
a: z.string(),
b: z.string(),
})
describe('errorMessagesForSchema', () => {
it('returns an object with error messages for every key of the given schema', () => {
assertEquals(errorMessagesForSchema(errors, schema), {
a: ['a'],
b: ['b', 'c'],
})
})

it('has type inference for the results of this function', () => {
assertEquals(errorMessagesForSchema(errors, schema).a, ['a'])
})

it('handles nested data errors', async () => {
const data = {
a: 'bar',
b: 'foo',
c: {
c1: 'c1 foo',
c2: 'c2 bar',
},
d: {
d1: 'd1 foo',
d2: 'd2 bar',
},
}

const schema = z.object({
a: z.string(),
b: z.string(),
c: z.object({
c1: z.string(),
c2: z.array(z.string()),
}),
d: z.object({
d1: z.object({
d1a: z.string(),
d1b: z.number(),
}),
d2: z.array(z.string()),
}),
})
const domainFn = makeDomainFunction(schema)((data) => data)
const result = await domainFn(data)

const errors = errorMessagesForSchema(result.inputErrors, schema)
assertEquals(errors, {
c: { c2: ['Expected array, received string'] },
d: {
d1: ['Expected object, received string'],
d2: ['Expected array, received string'],
},
})
})

it('handles nested data errors inside arrays of strings', async () => {
const data = {
a: 'bar',
b: 'foo',
c: {
c1: 'c1 foo',
c2: ['c2 bar', 6, { foo: 'test' }],
},
d: {
d1: 'd1 foo',
d2: 'd2 bar',
},
}

const schema = z.object({
a: z.string(),
b: z.string(),
c: z.object({
c1: z.string(),
c2: z.array(z.string()),
}),
d: z.object({
d1: z.object({
d1a: z.string(),
d1b: z.number(),
}),
d2: z.array(z.string()),
}),
})
const domainFn = makeDomainFunction(schema)((data) => data)
const result = await domainFn(data)

const errors = errorMessagesForSchema(result.inputErrors, schema)
assertEquals(errors, {
c: {
c2: {
'1': ['Expected string, received number'],
'2': ['Expected string, received object'],
},
},
d: {
d1: ['Expected object, received string'],
d2: ['Expected array, received string'],
},
})
})

it('handles nested data errors inside arrays of objects', async () => {
const data = {
a: 'bar',
b: 'foo',
c: {
c1: 'c1 foo',
c2: ['c2 bar', { c2a: 'test' }, { c2a: 1 }],
},
d: {
d1: 'd1 foo',
d2: 'd2 bar',
},
}

const schema = z.object({
a: z.string(),
b: z.string(),
c: z.object({
c1: z.string(),
c2: z.array(z.object({ c2a: z.number() })),
}),
d: z.object({
d1: z.object({
d1a: z.string(),
d1b: z.number(),
}),
d2: z.array(z.string()),
}),
})
const domainFn = makeDomainFunction(schema)((data) => data)
const result = await domainFn(data)

const errors = errorMessagesForSchema(result.inputErrors, schema)
assertEquals(errors, {
c: {
c2: {
'0': ['Expected object, received string'],
'1': { c2a: ['Expected number, received string'] },
},
},
d: {
d1: ['Expected object, received string'],
d2: ['Expected array, received string'],
},
})
})
})
58 changes: 0 additions & 58 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { z } from 'https://deno.land/x/zod@v3.21.4/mod.ts'
import type {
ErrorWithMessage,
SchemaError,
Expand Down Expand Up @@ -59,62 +58,6 @@ function errorMessagesFor(errors: SchemaError[], name: string) {
.map(({ message }) => message)
}

type NestedErrors<SchemaType> = {
[Property in keyof SchemaType]: string[] | NestedErrors<SchemaType[Property]>
}

function errorMessagesForSchema<T extends z.ZodTypeAny>(
errors: SchemaError[],
_schema: T,
): NestedErrors<z.infer<T>> {
type SchemaType = z.infer<T>
type ErrorObject = { path: string[]; messages: string[] }

const nest = (
{ path, messages }: ErrorObject,
root: Record<string, unknown>,
) => {
const [head, ...tail] = path
root[head] =
tail.length === 0
? messages
: nest(
{ path: tail, messages },
(root[head] as Record<string, unknown>) ?? {},
)
return root
}

const compareStringArrays = (a: string[]) => (b: string[]) =>
JSON.stringify(a) === JSON.stringify(b)

const toErrorObject = (errors: SchemaError[]): ErrorObject[] =>
errors.map(({ path, message }) => ({
path,
messages: [message],
}))

const unifyPaths = (errors: SchemaError[]) =>
toErrorObject(errors).reduce((memo, error) => {
const comparePath = compareStringArrays(error.path)
const mergeErrorMessages = ({ path, messages }: ErrorObject) =>
comparePath(path)
? { path, messages: [...messages, ...error.messages] }
: { path, messages }
const existingPath = memo.find(({ path }) => comparePath(path))

return existingPath ? memo.map(mergeErrorMessages) : [...memo, error]
}, [] as ErrorObject[])

const errorTree = unifyPaths(errors).reduce((memo, schemaError) => {
const errorBranch = nest(schemaError, memo)

return { ...memo, ...errorBranch }
}, {}) as NestedErrors<SchemaType>

return errorTree
}

/**
* A custom error class for input errors.
* @example
Expand Down Expand Up @@ -186,7 +129,6 @@ class ResultError extends Error {

export {
errorMessagesFor,
errorMessagesForSchema,
schemaError,
toErrorWithMessage,
InputError,
Expand Down
Loading