Skip to content

Commit

Permalink
Merge pull request #111 from seasonedcc/export-mdf
Browse files Browse the repository at this point in the history
  • Loading branch information
gustavoguichard authored Sep 12, 2023
2 parents 6af045d + ddd7e76 commit b147337
Show file tree
Hide file tree
Showing 19 changed files with 251 additions and 297 deletions.
95 changes: 48 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,14 @@ This documentation will use Node.JS imports by convention, just replace `domain-
```tsx
import type { ActionFunction } from 'remix'
import { useActionData, redirect } from 'remix'
import { makeDomainFunction, inputFromForm } from 'domain-functions'
// You can also use the short version of makeDomainFunction: mdf
import { mdf, inputFromForm } from 'domain-functions'
import * as z from 'zod'

const schema = z.object({ number: z.coerce.number() })

export const action: ActionFunction = async ({ request }) => {
const increment = makeDomainFunction(schema)(({ number }) => number + 1)
const increment = mdf(schema)(({ number }) => number + 1)
const result = await increment(await inputFromForm(request))

if (!result.success) return result
Expand Down Expand Up @@ -142,7 +143,7 @@ Sometimes you want to ensure the safety of certain values that weren't explicitl

```tsx
// In some app/domain/*.server.ts file
const sendEmail = makeDomainFunction(
const sendEmail = mdf(
z.object({ email: z.string().email() }), // user input schema
z.object({ origin: z.string() }) // environment schema
)(
Expand Down Expand Up @@ -171,7 +172,7 @@ We usually use the environment for ensuring authenticated requests.
In this case, assume you have a `currentUser` function that returns the authenticated user:

```tsx
const dangerousFunction = makeDomainFunction(
const dangerousFunction = mdf(
someInputSchema,
z.object({ user: z.object({ id: z.string(), admin: z.literal(true) }) })
)(async (input, { user }) => {
Expand All @@ -195,7 +196,7 @@ type ErrorResult = {
The `inputErrors` and `environmentErrors` fields will be the errors from parsing the corresponding Zod schemas, and the `errors` field will be for any exceptions thrown inside the domain function (in which case we keep a reference to the original exception):
```ts
const alwaysFails = makeDomainFunction(input, environment)(async () => {
const alwaysFails = mdf(input, environment)(async () => {
throw new Error('Some error')
})

Expand All @@ -217,7 +218,7 @@ failedResult = {
Whenever you want more control over the domain function's `ErrorResult`, you can throw a `ResultError` from the domain function's handler. You will then be able to add multiple error messages to the structure:

```ts
const alwaysFails = makeDomainFunction(inputSchema)(async () => {
const alwaysFails = mdf(inputSchema)(async () => {
throw new ResultError({
errors: [{ message: 'Some error' }],
inputErrors: [{ path: ['number'], message: 'Expected number, received nan' }],
Expand All @@ -231,7 +232,7 @@ const alwaysFails = makeDomainFunction(inputSchema)(async () => {
You can also throw an `InputError` whenever you want a custom input error that cannot be generated by your schema.

```ts
const alwaysFails = makeDomainFunction(input, environment)(async () => {
const alwaysFails = mdf(input, environment)(async () => {
throw new InputError('Email already taken', 'email')
})

Expand All @@ -249,7 +250,7 @@ failedResult = {
To throw several input errors at once, you can use the pluralized version `InputErrors` like this:

```ts
const alwaysFails = makeDomainFunction(input, environment)(async () => {
const alwaysFails = mdf(input, environment)(async () => {
throw new InputErrors([{message: 'Email already taken', path: 'email'}, {message: 'Password too short', path: 'password'}])
})

Expand Down Expand Up @@ -327,9 +328,9 @@ It will pass the same input and environment to each provided function.
If __all constituent functions__ are successful, The `data` field (on the composite domain function's result) will be a tuple containing each function's output.

```ts
const a = makeDomainFunction(z.object({ id: z.number() }))(({ id }) => String(id))
const b = makeDomainFunction(z.object({ id: z.number() }))(({ id }) => id + 1)
const c = makeDomainFunction(z.object({ id: z.number() }))(({ id }) => Boolean(id))
const a = mdf(z.object({ id: z.number() }))(({ id }) => String(id))
const b = mdf(z.object({ id: z.number() }))(({ id }) => id + 1)
const c = mdf(z.object({ id: z.number() }))(({ id }) => Boolean(id))

const results = await all(a, b, c)({ id: 1 })
```
Expand All @@ -349,10 +350,10 @@ For the example above, the result type will be `Result<[string, number, boolean]
If any of the constituent functions fail, the `errors` field (on the composite domain function's result) will be an array of the concatenated errors from each failing function:

```ts
const a = makeDomainFunction(z.object({ id: z.number() }))(() => {
const a = mdf(z.object({ id: z.number() }))(() => {
throw new Error('Error A')
})
const b = makeDomainFunction(z.object({ id: z.number() }))(() => {
const b = mdf(z.object({ id: z.number() }))(() => {
throw new Error('Error B')
})

Expand All @@ -376,9 +377,9 @@ const results = await all(a, b)({ id: 1 })
The motivation for this is that an object with named fields is often preferable to long tuples, when composing many domain functions.

```ts
const a = makeDomainFunction(z.object({}))(() => '1')
const b = makeDomainFunction(z.object({}))(() => 2)
const c = makeDomainFunction(z.object({}))(() => true)
const a = mdf(z.object({}))(() => '1')
const b = mdf(z.object({}))(() => 2)
const c = mdf(z.object({}))(() => true)

const results = await collect({ a, b, c })({})
```
Expand Down Expand Up @@ -410,13 +411,13 @@ map(all(a, b, c), mergeObjects)
The resulting data of every domain function will be merged into one object. __This could potentially lead to values of the leftmost functions being overwritten by the rightmost ones__.

```ts
const a = makeDomainFunction(z.object({}))(() => ({
const a = mdf(z.object({}))(() => ({
resultA: 'string',
resultB: 'string',
resultC: 'string',
}))
const b = makeDomainFunction(z.object({}))(() => ({ resultB: 2 }))
const c = makeDomainFunction(z.object({}))(async () => ({ resultC: true }))
const b = mdf(z.object({}))(() => ({ resultB: 2 }))
const c = mdf(z.object({}))(async () => ({ resultC: true }))

const results = await merge(a, b, c)({})
```
Expand Down Expand Up @@ -450,10 +451,10 @@ __Be mindful of__ each constituent domain function's return type. If any domain
__It is important to notice__ that all constituent domain functions will be executed in parallel, so be mindful of the side effects.

```ts
const a = makeDomainFunction(
const a = mdf(
z.object({ n: z.number(), operation: z.literal('increment') }),
)(({ n }) => n + 1)
const b = makeDomainFunction(
const b = mdf(
z.object({ n: z.number(), operation: z.literal('decrement') }),
)(({ n }) => n - 1)

Expand All @@ -475,10 +476,10 @@ For the example above, the result type will be `Result<number>`:
The composite domain function's result type will be a union of each constituent domain function's result type.

```ts
const a = makeDomainFunction(z.object({ operation: z.literal('A') }))(() => ({
const a = mdf(z.object({ operation: z.literal('A') }))(() => ({
resultA: 'A',
}))
const b = makeDomainFunction(z.object({ operation: z.literal('B') }))(() => ({
const b = mdf(z.object({ operation: z.literal('B') }))(() => ({
resultB: 'B',
}))

Expand All @@ -492,10 +493,10 @@ return console.log('function B succeeded')
If every constituent domain function fails, the `errors` field will contain the concatenated errors from each failing function's result:

```ts
const a = makeDomainFunction(z.object({ id: z.number() }))(() => {
const a = mdf(z.object({ id: z.number() }))(() => {
throw new Error('Error A')
})
const b = makeDomainFunction(z.object({ id: z.number() }))(() => {
const b = mdf(z.object({ id: z.number() }))(() => {
throw new Error('Error B')
})

Expand All @@ -521,17 +522,17 @@ The resulting data will be the output of the rightmost function.
Note that there is no type-level assurance that a function's output will align with and be succesfully parsed by the next function in the pipeline.

```ts
const a = makeDomainFunction(z.object({ aNumber: z.number() }))(
const a = mdf(z.object({ aNumber: z.number() }))(
({ aNumber }) => ({
aString: String(aNumber),
}),
)
const b = makeDomainFunction(z.object({ aString: z.string() }))(
const b = mdf(z.object({ aString: z.string() }))(
({ aString }) => ({
aBoolean: aString == '1',
}),
)
const c = makeDomainFunction(z.object({ aBoolean: z.boolean() }))(
const c = mdf(z.object({ aBoolean: z.boolean() }))(
async ({ aBoolean }) => !aBoolean,
)

Expand Down Expand Up @@ -560,8 +561,8 @@ If one functions fails, execution halts and the error is returned.
Instead of the `data` field being the output of the last domain function, it will be a tuple containing each intermediate output (similar to the `all` function).

```ts
const a = makeDomainFunction(z.number())((aNumber) => String(aNumber))
const b = makeDomainFunction(z.string())((aString) => aString === '1')
const a = mdf(z.number())((aNumber) => String(aNumber))
const b = mdf(z.string())((aString) => aString === '1')

const c = sequence(a, b)

Expand All @@ -585,10 +586,10 @@ If you'd rather have an object instead of a tuple (similar to the `merge` functi
```ts
import { mergeObjects } from 'domain-functions'

const a = makeDomainFunction(z.number())((aNumber) => ({
const a = mdf(z.number())((aNumber) => ({
aString: String(aNumber)
}))
const b = makeDomainFunction(z.object({ aString: z.string() }))(
const b = mdf(z.object({ aString: z.string() }))(
({ aString }) => ({ aBoolean: aString === '1' })
)

Expand All @@ -603,16 +604,16 @@ For the example above, the result type will be `Result<{ aString: string, aBoole

`collectSequence` is very similar to the `collect` function, except __it runs in the sequence of the keys' order like a `pipe`__.

It receives its constituent functions inside a record with string keys that identify each one.
It receives its constituent functions inside a record with string keys that identify each one.
The shape of this record will be preserved for the `data` property in successful results.

This feature relies on JS's order of objects' keys (guaranteed since ECMAScript2015).

**NOTE :** For number-like object keys (eg: { 2: dfA, 1: dfB }) JS will follow ascendent order.

```ts
const a = makeDomainFunction(z.number())((aNumber) => String(aNumber))
const b = makeDomainFunction(z.string())((aString) => aString === '1')
const a = mdf(z.number())((aNumber) => String(aNumber))
const b = mdf(z.string())((aString) => aString === '1')

const c = collectSequence({ a, b })

Expand All @@ -636,10 +637,10 @@ If you'd rather have an object instead of a tuple (similar to the `merge` functi
```ts
import { mergeObjects } from 'domain-functions'

const a = makeDomainFunction(z.number())((aNumber) => ({
const a = mdf(z.number())((aNumber) => ({
aString: String(aNumber)
}))
const b = makeDomainFunction(z.object({ aString: z.string() }))(
const b = mdf(z.object({ aString: z.string() }))(
({ aString }) => ({ aBoolean: aString === '1' })
)

Expand All @@ -658,13 +659,13 @@ Use `branch` to add conditional logic to your domain functions' compositions.
It receives a domain function and a predicate function that should return the next domain function to be executed based on the previous domain function's output.

```ts
const getIdOrEmail = makeDomainFunction(z.object({ id: z.number().optional, email: z.string().optional() }))((data) => {
const getIdOrEmail = mdf(z.object({ id: z.number().optional, email: z.string().optional() }))((data) => {
return data.id ?? data.email
})
const findUserById = makeDomainFunction(z.number())((id) => {
const findUserById = mdf(z.number())((id) => {
return db.users.find({ id })
})
const findUserByEmail = makeDomainFunction(z.string().email())((email) => {
const findUserByEmail = mdf(z.string().email())((email) => {
return db.users.find({ email })
})
const findUserByIdOrEmail = branch(
Expand Down Expand Up @@ -712,14 +713,14 @@ If successful, the `data` field will contain the output of the first function ar
This can be useful when composing domain functions. For example, you might need to align input/output types in a pipeline:

```ts
const fetchAsText = makeDomainFunction(z.object({ userId: z.number() }))(
const fetchAsText = mdf(z.object({ userId: z.number() }))(
({ userId }) =>
fetch(`https://reqres.in/api/users/${String(userId)}`).then((r) =>
r.json(),
),
)

const fullName = makeDomainFunction(
const fullName = mdf(
z.object({ first_name: z.string(), last_name: z.string() }),
)(({ first_name, last_name }) => `${first_name} ${last_name}`)

Expand Down Expand Up @@ -752,7 +753,7 @@ This could be useful when adding any layer of error handling.
In the example below, we are counting the errors but disregarding the contents:

```ts
const increment = makeDomainFunction(z.object({ id: z.number() }))(
const increment = mdf(z.object({ id: z.number() }))(
({ id }) => id + 1,
)

Expand Down Expand Up @@ -790,7 +791,7 @@ For the example above, the `result` will be:
Whenever the composition utilities fall short, and you want to call other domain functions from inside your current one, you can use the `fromSuccess` function to create a domain function that is expected to always succeed.

```ts
const domainFunctionA = makeDomainFunction(
const domainFunctionA = mdf(
z.object({ id: z.string() }),
)(async ({ id }) => {
const valueB = await fromSuccess(domainFunctionB)({ userId: id })
Expand Down Expand Up @@ -829,7 +830,7 @@ The resulting object will be:
`UnpackData` infers the returned data of a successful domain function:

```ts
const fn = makeDomainFunction()(async () => '')
const fn = mdf()(async () => '')

type Data = UnpackData<typeof fn>
// Data = string
Expand All @@ -840,7 +841,7 @@ type Data = UnpackData<typeof fn>
`UnpackSuccess` infers the success result of a domain function:
```ts
const fn = makeDomainFunction()(async () => '')
const fn = mdf()(async () => '')

type Success = UnpackSuccess<typeof fn>
// Success = { success: true, data: string, errors: [], inputErrors: [], environmentErrors: [] }
Expand All @@ -851,7 +852,7 @@ type Success = UnpackSuccess<typeof fn>
`UnpackResult` infers the result of a domain function:
```ts
const fn = makeDomainFunction()(async () => '')
const fn = mdf()(async () => '')

type Result = UnpackResult<typeof fn>
/*
Expand Down
Loading

0 comments on commit b147337

Please sign in to comment.