New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

composable-functions

Package Overview
Dependencies
Maintainers
0
Versions
24
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

composable-functions - npm Package Compare versions

Comparing version 4.0.1 to 4.1.0

context.md

104

API.md

@@ -27,5 +27,5 @@ # API Reference

- [ErrorList](#errorlist)
- [EnvironmentError](#environmenterror)
- [ContextError](#contexterror)
- [InputError](#inputerror)
- [isEnvironmentError](#isenvironmenterror)
- [isContextError](#iscontexterror)
- [isInputError](#isinputerror)

@@ -40,6 +40,6 @@ - [Type-safe runtime utilities](#type-safe-runtime-utilities)

- [UnpackData](#unpackdata)
- [Combinators with Environment](#combinators-with-environment)
- [environment.branch](#environmentbranch)
- [environment.pipe](#environmentpipe)
- [environment.sequence](#environmentsequence)
- [Combinators with Context](#combinators-with-context)
- [context.branch](#contextbranch)
- [context.pipe](#contextpipe)
- [context.sequence](#contextsequence)
- [Serialization](#serialization)

@@ -53,3 +53,3 @@ - [serialize](#serialize)

## applySchema
It takes a composable and schemas for the input and environment, and returns the same composable with the schemas applied. So the types will be asserted at runtime.
It takes a composable and schemas for the input and context, and returns the same composable with the schemas applied. So the types will be asserted at runtime.

@@ -74,3 +74,3 @@ It is useful when dealing with external data, such as API requests, where you want to ensure the data is in the correct shape before processing it.

type Test = typeof safeFunction
// ^? Composable<(input?: unknown, env?: unknown) => { message: string }>
// ^? Composable<(input?: unknown, ctx?: unknown) => { message: string }>
```

@@ -158,3 +158,3 @@

## withSchema
It creates a composable with unknown input and environment types, and applies the schemas to them so the arguments are assured at runtime.
It creates a composable with unknown input and context types, and applies the schemas to them so the arguments are assured at runtime.

@@ -172,3 +172,3 @@ See `applySchema` above for more information.

If there are input or environment errors, they will be returned in the `errors` property of the result.
If there are input or context errors, they will be returned in the `errors` property of the result.
```ts

@@ -180,3 +180,3 @@ const result = await runtimeSafeAdd('1', null)

// new InputError('Expected number, received string'),
// new EnvironmentError('Expected number, received null')
// new ContextError('Expected number, received null')
// ],

@@ -427,3 +427,3 @@ // }

)
// ^? Composable<(input: unknown, env: { id: number }) => User>
// ^? Composable<(input: unknown, ctx: { id: number }) => User>
```

@@ -646,3 +646,3 @@

new InputError('Custom input error', ['contact', 'id']),
new EnvironmentError('Custom env error', ['currentUser', 'role']),
new ContextError('Custom context error', ['currentUser', 'role']),
])

@@ -655,3 +655,3 @@ })

// new InputError('Custom input error', ['contact', 'id']),
// new EnvironmentError('Custom env error', ['currentUser', 'role']),
// new ContextError('Custom context error', ['currentUser', 'role']),
// ],

@@ -661,6 +661,6 @@ // }

## EnvironmentError
An `EnvironmentError` is a special kind of error that represents an error in the environment schema.
## ContextError
An `ContextError` is a special kind of error that represents an error in the context schema.
It has an optional second parameter that is an array of strings representing the path to the error in the environment schema.
It has an optional second parameter that is an array of strings representing the path to the error in the context schema.

@@ -679,3 +679,3 @@ ```ts

errors: [
new EnvironmentError(
new ContextError(
'Expected string, received number',

@@ -688,7 +688,7 @@ ['user', 'id'],

You can also use the `EnvironmentError` constructor to throw errors within the composable:
You can also use the `ContextError` constructor to throw errors within the composable:
```ts
const fn = composable(() => {
throw new EnvironmentError('Custom env error', ['currentUser', 'role'])
throw new ContextError('Custom context error', ['currentUser', 'role'])
})

@@ -698,10 +698,10 @@ ```

## InputError
Similar to `EnvironmentError`, an `InputError` is a special kind of error that represents an error in the input schema.
Similar to `ContextError`, an `InputError` is a special kind of error that represents an error in the input schema.
## isEnvironmentError
`isEnvironmentError` is a helper function that will check if an error is an instance of `EnvironmentError`.
## isContextError
`isContextError` is a helper function that will check if an error is an instance of `ContextError`.
```ts
isEnvironmentError(new EnvironmentError('yes')) // true
isEnvironmentError(new Error('nope')) // false
isContextError(new ContextError('yes')) // true
isContextError(new Error('nope')) // false
```

@@ -794,16 +794,16 @@

# Combinators with Environment
The environment is a concept of an argument that is passed to every functions of a sequential composition. When it comes to parallel compositions, all arguments are already forwarded to every function.
# Combinators with Context
The context is a concept of an argument that is passed to every functions of a sequential composition. When it comes to parallel compositions, all arguments are already forwarded to every function.
However in sequential compositions, we need a set of special combinators that will forward the environment - the second parameter - to every function in the composition.
However in sequential compositions, we need a set of special combinators that will forward the context - the second parameter - to every function in the composition.
Use the sequential combinators from the namespace `environment` to get this behavior.
Use the sequential combinators from the namespace `context` to get this behavior.
For a deeper explanation check the [`environment` docs](./environments.md).
For a deeper explanation check the [`context` docs](./context.md).
## environment.branch
It is the same as `branch` but it will forward the environment to the next composable.
## context.branch
It is the same as `branch` but it will forward the context to the next composable.
```ts
import { environment } from 'composable-functions'
import { context } from 'composable-functions'

@@ -813,4 +813,4 @@ const getIdOrEmail = composable((data: { id?: number, email?: string }) => {

})
const findUserById = composable((id: number, env: { user: User }) => {
if (!env.user.admin) {
const findUserById = composable((id: number, ctx: { user: User }) => {
if (!ctx.user.admin) {
throw new Error('Unauthorized')

@@ -820,4 +820,4 @@ }

})
const findUserByEmail = composable((email: string, env: { user: User }) => {
if (!env.user.admin) {
const findUserByEmail = composable((email: string, ctx: { user: User }) => {
if (!ctx.user.admin) {
throw new Error('Unauthorized')

@@ -827,3 +827,3 @@ }

})
const findUserByIdOrEmail = environment.branch(
const findUserByIdOrEmail = context.branch(
getIdOrEmail,

@@ -834,13 +834,13 @@ (data) => (typeof data === "number" ? findUserById : findUserByEmail),

```
## environment.pipe
Similar to `pipe` but it will forward the environment to the next composable.
## context.pipe
Similar to `pipe` but it will forward the context to the next composable.
```ts
import { environment } from 'composable-functions'
import { context } from 'composable-functions'
const a = composable((aNumber: number, env: { user: User }) => String(aNumber))
const b = composable((aString: string, env: { user: User }) => aString == '1')
const c = composable((aBoolean: boolean, env: { user: User }) => aBoolean && env.user.admin)
const a = composable((aNumber: number, ctx: { user: User }) => String(aNumber))
const b = composable((aString: string, ctx: { user: User }) => aString == '1')
const c = composable((aBoolean: boolean, ctx: { user: User }) => aBoolean && ctx.user.admin)
const d = environment.pipe(a, b, c)
const d = context.pipe(a, b, c)

@@ -850,13 +850,13 @@ const result = await d(1, { user: { admin: true } })

## environment.sequence
Similar to `sequence` but it will forward the environment to the next composable.
## context.sequence
Similar to `sequence` but it will forward the context to the next composable.
```ts
import { environment } from 'composable-functions'
import { context } from 'composable-functions'
const a = composable((aNumber: number, env: { user: User }) => String(aNumber))
const b = composable((aString: string, env: { user: User }) => aString === '1')
const c = composable((aBoolean: boolean, env: { user: User }) => aBoolean && env.user.admin)
const a = composable((aNumber: number, ctx: { user: User }) => String(aNumber))
const b = composable((aString: string, ctx: { user: User }) => aString === '1')
const c = composable((aBoolean: boolean, ctx: { user: User }) => aBoolean && ctx.user.admin)
const d = environment.sequence(a, b, c)
const d = context.sequence(a, b, c)

@@ -863,0 +863,0 @@ const result = await d(1, { user: { admin: true } })

import { mapErrors } from './combinators.js';
import { EnvironmentError, ErrorList, InputError } from './errors.js';
import { ContextError, ErrorList, InputError } from './errors.js';
/**

@@ -69,9 +69,9 @@ * It receives any data (T) and returns a Success<T> object.

/**
* Creates a composable with unknown input and environment that uses schemas to parse them into known types.
* Creates a composable with unknown input and context that uses schemas to parse them into known types.
* This allows you to code the function with arbitrary types knowinng that they will be enforced in runtime.
* Very useful when piping data coming from any external source into your composables.
* After giving the input and environment schemas, you can pass a handler function that takes type safe input and environment. That function is gonna catch any errors and always return a Result.
* After giving the input and context schemas, you can pass a handler function that takes type safe input and context. That function is gonna catch any errors and always return a Result.
* @param inputSchema the schema for the input
* @param environmentSchema the schema for the environment
* @returns a handler function that takes type safe input and environment
* @param contextSchema the schema for the context
* @returns a handler function that takes type safe input and context
* @example

@@ -88,11 +88,11 @@ * const safeFunction = withSchema(

*/
function withSchema(inputSchema, environmentSchema) {
return (handler) => applySchema(inputSchema, environmentSchema)(composable(handler));
function withSchema(inputSchema, contextSchema) {
return (handler) => applySchema(inputSchema, contextSchema)(composable(handler));
}
/**
* Takes a composable and creates a composable withSchema that will assert the input and environment types according to the given schemas.
* Takes a composable and creates a composable withSchema that will assert the input and context types according to the given schemas.
* @param fn a composable function
* @param inputSchema the schema for the input
* @param environmentSchema the schema for the environment
* @returns a composable function that will assert the input and environment types at runtime.
* @param contextSchema the schema for the context
* @returns a composable function that will assert the input and context types at runtime.
* @example

@@ -114,15 +114,15 @@ * ```ts

*/
function applySchema(inputSchema, environmentSchema) {
return ((fn) => {
return ((input, environment) => {
const envResult = (environmentSchema ?? alwaysUnknownSchema).safeParse(environment);
function applySchema(inputSchema, contextSchema) {
return (fn) => {
return ((input, context) => {
const ctxResult = (contextSchema ?? alwaysUnknownSchema).safeParse(context);
const result = (inputSchema ?? alwaysUnknownSchema).safeParse(input);
if (!result.success || !envResult.success) {
if (!result.success || !ctxResult.success) {
const inputErrors = result.success ? [] : result.error.issues.map((error) => new InputError(error.message, error.path));
const envErrors = envResult.success ? [] : envResult.error.issues.map((error) => new EnvironmentError(error.message, error.path));
return Promise.resolve(failure([...inputErrors, ...envErrors]));
const ctxErrors = ctxResult.success ? [] : ctxResult.error.issues.map((error) => new ContextError(error.message, error.path));
return Promise.resolve(failure([...inputErrors, ...ctxErrors]));
}
return fn(result.data, envResult.data);
return fn(result.data, ctxResult.data);
});
});
};
}

@@ -129,0 +129,0 @@ const alwaysUnknownSchema = {

@@ -26,3 +26,4 @@ /**

/**
* A custom error class for environment errors.
* @deprecated Use `ContextError` instead
* A custom error class for context errors.
*

@@ -38,3 +39,3 @@ * @example

/**
* Path of environment attribute that originated the error.
* Path of context attribute that originated the error.
*/

@@ -52,2 +53,26 @@ Object.defineProperty(this, "path", {

/**
* A custom error class for context errors.
*
* @example
* const aComposable = withSchema()(() => {
* throw new ContextError('Invalid context', 'user.name')
* })
*/
class ContextError extends Error {
constructor(message, path = []) {
super(message);
/**
* Path of context attribute that originated the error.
*/
Object.defineProperty(this, "path", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
this.name = 'ContextError';
this.path = path;
}
}
/**
* A list of errors

@@ -80,7 +105,12 @@ *

/**
* A function to check if an `Error` or a `SerializableError` is an EnvironmentError
* @deprecated Use `isContextError` instead
* A function to check if an `Error` or a `SerializableError` is a ContextError
*/
function isEnvironmentError(e) {
return e.name === 'EnvironmentError';
const isEnvironmentError = isContextError;
/**
* A function to check if an `Error` or a `SerializableError` is a ContextError
*/
function isContextError(e) {
return e.name === 'EnvironmentError' || e.name === 'ContextError';
}
export { EnvironmentError, ErrorList, InputError, isEnvironmentError, isInputError, };
export { ContextError, EnvironmentError, ErrorList, InputError, isContextError, isEnvironmentError, isInputError, };

@@ -5,4 +5,8 @@ export { applySchema, composable, failure, fromSuccess, success, withSchema, } from './constructors.js';

export { serialize, serializeError } from './serializer.js';
export { EnvironmentError, ErrorList, InputError, isEnvironmentError, isInputError, } from './errors.js';
// FUNCTIONS WITH ENVIRONMENT
export * as environment from './environment/index.js';
export { ContextError, EnvironmentError, ErrorList, InputError, isContextError, isEnvironmentError, isInputError, } from './errors.js';
// FUNCTIONS WITH CONTEXT
/**
* @deprecated use `import { context }` instead
*/
export * as environment from './context/index.js';
export * as context from './context/index.js';

@@ -1,2 +0,2 @@

import { EnvironmentError } from './errors.js';
import { ContextError } from './errors.js';
import { InputError } from './errors.js';

@@ -7,3 +7,3 @@ /**

function serializeError(error) {
if (error instanceof InputError || error instanceof EnvironmentError) {
if (error instanceof InputError || error instanceof ContextError) {
return {

@@ -10,0 +10,0 @@ exception: error,

@@ -10,4 +10,4 @@ # Migrating from domain-functions

- 🤌 Simplified Function Creation: **No need to define schemas**. Create composable functions easily and efficiently without the overhead of schema definitions.
- 🕵🏽 Runtime Validation: Use the [`withSchema`](./API.md#withschema) function for optional runtime validation of inputs and environments. This provides flexibility to enforce data integrity when needed without mandating it for every function. Assuming you have a big chain of composables you can use [`applySchema`](./API.md#applyschema) to run your runtime validation only once **avoiding unnecessary processing**.
- 🔀 Flexible Compositions: The new combinators, such as [`environment.pipe`](./API.md#environmentpipe), [`environment.sequence`](./API.md#environmentsequence), and [`environment.branch`](./API.md#environmentbranch), offer powerful ways to manage **typed environments** and contextual information across your compositions.
- 🕵🏽 Runtime Validation: Use the [`withSchema`](./API.md#withschema) function for optional runtime validation of inputs and context. This provides flexibility to enforce data integrity when needed without mandating it for every function. Assuming you have a big chain of composables you can use [`applySchema`](./API.md#applyschema) to run your runtime validation only once **avoiding unnecessary processing**.
- 🔀 Flexible Compositions: The new combinators, such as [`context.pipe`](./API.md#contextpipe), [`context.sequence`](./API.md#contextsequence), and [`context.branch`](./API.md#contextbranch), offer powerful ways to manage **typed context** which are contextual information across your compositions.
- 🛠️ Incremental Migration: Seamlessly migrate your existing codebase incrementally. **Both `domain-functions` and `composable-functions` can coexist**, allowing you to transition module by module.

@@ -21,3 +21,3 @@ - 🛟 Enhanced Combinators: New and improved combinators like [`map`](./API.md#map), [`mapParameters`](./API.md#mapparameters), [`mapErrors`](./API.md#maperrors) and [`catchFailure`](./API.md#catchfailure) provide more control over error handling and transformation, making your **code more resilient**.

- [Combinators which shouldn't be affected](#combinators-which-shouldnt-be-affected)
- [Sequential combinators and the concept of environment](#sequential-combinators-and-the-concept-of-environment)
- [Sequential combinators and the concept of context](#sequential-combinators-and-the-concept-of-context)
- [Modified combinators](#modified-combinators)

@@ -40,5 +40,5 @@ - [map](#map)

## First steps
The first thing you want to know is that the old `DomainFunction<T>` is equivalent to `Composable<(input?: unknown, environment?: unknown) => T>` (AKA `ComposableWithSchema<T>`). We brought the arguments to the type signature so we could type check the compositions. A [commonly requested feature](https://github.com/seasonedcc/domain-functions/issues/80) in domain-functions.
The first thing you want to know is that the old `DomainFunction<T>` is equivalent to `Composable<(input?: unknown, context?: unknown) => T>` (AKA `ComposableWithSchema<T>`). We brought the arguments to the type signature so we could type check the compositions. A [commonly requested feature](https://github.com/seasonedcc/domain-functions/issues/80) in domain-functions.
A composable does not need a schema, but you can still use one for runtime assertion. What we used to call a Domain Function is now a Composable with [environment](./environments.md) and a schema.
A composable does not need a schema, but you can still use one for runtime assertion. What we used to call a Domain Function is now a Composable with [context](./context.md) and a schema.

@@ -50,3 +50,3 @@ The new constructor `withSchema` will work almost exactly as `makeDomainFunction`, except for the `Result` type of the resulting function.

This allows us to preserve stack traces and use the familiar exception interface. To differentiate inputErrors and environmentErrors you can use the `instanceof` operator. It also opens up the possibility create any custom error your system needs.
This allows us to preserve stack traces and use the familiar exception interface. To differentiate inputErrors and environmentErrors - which are now called context errors - you can use the `instanceof` operator. It also opens up the possibility create any custom error your system needs.

@@ -68,3 +68,3 @@ ```ts

new InputError('Required', ['name']),
new EnvironmentError('Unauthorized', ['user']),
new ContextError('Unauthorized', ['user']),
],

@@ -92,17 +92,17 @@ }

## Sequential combinators and the concept of environment
The environment we used to have in domain-functions is already built-in the composable's parallel combinators since all arguments are forwarded to every function. For a deeper explanation check the [`environment` docs](./environments.md).
## Sequential combinators and the concept of context
The `environment` we used to have in domain-functions is now called `context` and it is already built-in the composable's parallel combinators since all arguments are forwarded to every function. For a deeper explanation check the [`context` docs](./context.md).
When it comes to sequential compositions, however, we need special combinators to preserve the environment so they work as the domain-functions' combinators.
When it comes to sequential compositions, however, we need special combinators to preserve the context so they work as the domain-functions' combinators.
Use the sequential combinators from the namespace `environment` to keep this familiar behavior.
Use the sequential combinators from the namespace `context` to keep this familiar behavior.
```ts
import { environment } from 'composable-functions'
import { context } from 'composable-functions'
const result = environment.pipe(fn1, fn2)(input, env)
// same for `environment.sequence` and `environment.branch`
const result = context.pipe(fn1, fn2)(input, ctx)
// same for `context.sequence` and `context.branch`
```
**Note**: The `pipe`, `sequence`, and `branch` outside of the `environment` namespace will not keep the environment through the composition.
**Note**: The `pipe`, `sequence`, and `branch` outside of the `context` namespace will not keep the context through the composition.

@@ -141,9 +141,9 @@ ## Modified combinators

// New Composable code:
import { mapErrors, isInputError, isEnvironmentError } from 'composable-functions'
import { mapErrors, isInputError, isContextError } from 'composable-functions'
const summarizeErrors = (errors: Error[]) =>
[
new Error('Number of errors: ' + errors.filter((e) => !isInputError(e) && !isEnvironmentError(e)).length,
new Error('Number of errors: ' + errors.filter((e) => !isInputError(e) && !isContextError(e)).length,
new InputError('Number of input errors: ' + errors.filter(isInputError).length),
new EnvironmentError('Number of environment errors: ' + errors.filter(isEnvironmentError).length),
new ContextError('Number of context errors: ' + errors.filter(isContextError).length),
]

@@ -217,3 +217,3 @@

import type { Result as DFResult } from 'domain-functions'
import { isInputError, isEnvironmentError } from 'composable-functions'
import { isInputError, isContextError } from 'composable-functions'
import type { Result, SerializableResult } from 'composable-functions'

@@ -232,3 +232,3 @@

}
return result.errors.some(isEnvironmentError)
return result.errors.some(isContextError)
}

@@ -240,3 +240,3 @@ ```

### Dealing with Failures
- In the tests, change the `result.inputErrors` and `result.environmentErrors` for `result.errors`. You can also test for the name: `InputError` or `EnvironmentError`
- In the tests, change the `result.inputErrors` and `result.environmentErrors` for `result.errors`. You can also test for the name: `InputError` or `ContextError`
```ts

@@ -248,3 +248,3 @@ // replace this

```
- Elsewhere, collect the inputErrors and environmentErrors with the [`isInputError`](./API.md#isinputerror) and [`isEnvironmentError`](./API.md#isenvironmenterror) functions.
- Elsewhere, collect the inputErrors and environmentErrors with the [`isInputError`](./API.md#isinputerror) and [`isContextError`](./API.md#iscontexterror) functions.
```ts

@@ -266,8 +266,8 @@ // replace this

|---|---|
| `makeDomainFunction(z.string(), z.number())((input, env) => {})` | `withSchema(z.string, z.number())((input, env) => {})` |
| -- | `applySchema(z.string(), z.number())(composable((input, env) => {}))` |
| `makeDomainFunction(z.string(), z.number())((input, env) => {})` | `withSchema(z.string, z.number())((input, ctx) => {})` |
| -- | `applySchema(z.string(), z.number())(composable((input, ctx) => {}))` |
| `makeSuccessResult(1)` | `success(1)` |
| `makeErrorResult({ errors: [{ message: 'Something went wrong' }] })` | `failure([new Error('Something went wrong')])` |
| `new InputError('required', 'user.name')` | `new InputError('required', ['user', 'name'])` |
| `new EnvironmentError('oops', 'user.name')` | `new EnvironmentError('oops', ['user', 'name'])` |
| `new EnvironmentError('oops', 'user.name')` | `new ContextError('oops', ['user', 'name'])` |
| `new InputErrors([{ message: 'oops', path: 'user.name' }])` | `new ErrorList([new InputError('oops', ['user', 'name'])])` |

@@ -282,9 +282,9 @@ | `new ResultError({ inputErrors: [{ message: 'oops', path: 'user.name' }] })` | `new ErrorList([new InputError('oops', ['user', 'name'])])` |

| `merge(df1, df2)` | `map(all(fn1, fn2), mergeObjects)` |
| `branch(df1, (res) => res ? null : df2)` | `environment.branch(fn1, (res) => res ? null : fn2)` |
| -- | `branch(fn1, (res) => res ? null : fn2)` without environment |
| `pipe(df1, df2)` | `environment.pipe(fn1, fn2)` |
| -- | `pipe(fn1, fn2)` without environment |
| `sequence(df1, df2)` | `environment.sequence(fn1, fn2)` |
| -- | `sequence(fn1, fn2)` without environment |
| `collectSequence({ name: nameDf, age: ageDf })` | `map(environment.sequence(nameDf, ageDf), ([name, age]) => ({ name, age }))` |
| `branch(df1, (res) => res ? null : df2)` | `context.branch(fn1, (res) => res ? null : fn2)` |
| -- | `branch(fn1, (res) => res ? null : fn2)` without context |
| `pipe(df1, df2)` | `context.pipe(fn1, fn2)` |
| -- | `pipe(fn1, fn2)` without context |
| `sequence(df1, df2)` | `context.sequence(fn1, fn2)` |
| -- | `sequence(fn1, fn2)` without context |
| `collectSequence({ name: nameDf, age: ageDf })` | `map(context.sequence(nameDf, ageDf), ([name, age]) => ({ name, age }))` |
| `map(df, (o) => ({ result: o }))` | `map(fn, (o) => ({ result: o }))` |

@@ -310,6 +310,6 @@ | -- | `map(fn, (o, ...args) => ({ result: o, args }))` |

| `{ success: true, data: { name: 'John' }, errors: [], inputErrors: [], environmentErrors: [] }` | `{ success: true, data: { name: 'John' }, errors: [] }` |
| `{ success: false, errors: [{ message: 'Something went wrong' }], inputErrors: [{ message: 'Required', path: ['name'] }], environemntErrors: [{ message: 'Unauthorized', path: ['user'] }] }` | `{ success: false, errors: [new Error('Something went wrong'), new InputError('Required', ['name']), new EnvironmentError('Unauthorized', ['user'])] }` |
| -- | with `serialize`: `{ success: false, errors: [{ message: 'Something went wrong', name: 'Error', path: [] }, { message: 'Required', name: 'InputError', path: ['name'] }, { message: 'Unauthorized', name: 'EnvironmentError', path: ['user'] }] }` |
| `{ success: false, errors: [{ message: 'Something went wrong' }], inputErrors: [{ message: 'Required', path: ['name'] }], environemntErrors: [{ message: 'Unauthorized', path: ['user'] }] }` | `{ success: false, errors: [new Error('Something went wrong'), new InputError('Required', ['name']), new ContextError('Unauthorized', ['user'])] }` |
| -- | with `serialize`: `{ success: false, errors: [{ message: 'Something went wrong', name: 'Error', path: [] }, { message: 'Required', name: 'InputError', path: ['name'] }, { message: 'Unauthorized', name: 'ContextError', path: ['user'] }] }` |
| `result.inputErrors[0]?.message` | `result.errors.find(isInputError)?.message` |
| `result.environmentErrors[0]?.message` | `result.errors.find(isEnvironmentError)?.message` |
| `result.environmentErrors[0]?.message` | `result.errors.find(isContextError)?.message` |
| `result.errors[0]?.exception instanceof CustomError` | `result.errors[0] instanceof CustomError` |
{
"name": "composable-functions",
"version": "4.0.1",
"version": "4.1.0",
"description": "Types and functions to make composition easy and safe",

@@ -5,0 +5,0 @@ "author": "Seasoned",

@@ -12,3 +12,3 @@ <p align="center">

- ⚡ Parallel and Sequential Compositions: Compose functions both in parallel - with `all` and `collect` - and sequentially - with `pipe`, `branch`, and `sequence` -, to manage complex data flows optimizing your code for performance and clarity.
- 🕵️‍♂️ Runtime Validation: Use `withSchema` or `applySchema` with your favorite parser for optional runtime validation of inputs and environments, enforcing data integrity only when needed.
- 🕵️‍♂️ Runtime Validation: Use `withSchema` or `applySchema` with your favorite parser for optional runtime validation of inputs and context, enforcing data integrity only when needed.
- 🚑 Resilient Error Handling: Leverage enhanced combinators like `mapErrors` and `catchFailure` to transform and handle errors more effectively.

@@ -22,2 +22,3 @@ - 📊 Traceable Compositions: Use the `trace` function to log and monitor your composable functions’ inputs and results, simplifying debugging and monitoring.

- [Composing type-safe functions](#composing-type-safe-functions)
- [Adding runtime validation to the Composable](#adding-runtime-validation-to-the-composable)
- [Creating primitive composables](#creating-primitive-composables)

@@ -35,3 +36,3 @@ - [Sequential composition](#sequential-composition)

- [Handling external input](#handling-external-input)
- [Defining constants for multiple functions (environments)](#defining-constants-for-multiple-functions-environments)
- [Defining constants for multiple functions (context)](#defining-constants-for-multiple-functions-context)
- [Using custom parsers](#using-custom-parsers)

@@ -97,2 +98,22 @@ - [Using Deno](#using-deno)

### Adding runtime validation to the Composable
To ensure type safety at runtime, use the `applySchema` or `withSchema` functions to validate external inputs against defined schemas. These schemas can be specified with libraries such as [Zod](https://github.com/colinhacks/zod/) or [ArkType](https://github.com/arktypeio/arktype).
Note that the resulting `Composable` will have unknown types for the parameters now that we rely on runtime validation.
```ts
import { applySchema } from 'composable-functions'
import { z } from 'zod'
const addAndReturnWithRuntimeValidation = applySchema(
z.number(),
z.number(),
)(addAndReturnString)
// Or you could have defined schemas and implementation in one shot:
const add = withSchema(z.number(), z.number())((a, b) => a + b)
```
For more information and examples, check the [Handling external input](./with-schema.md) guide.
## Creating primitive composables

@@ -263,3 +284,3 @@

#### [Handling external input](./with-schema.md)
#### [Defining constants for multiple functions (environments)](./environments.md)
#### [Defining constants for multiple functions (context)](./context.md)
#### [Using custom parsers](./examples/arktype/README.md)

@@ -266,0 +287,0 @@

@@ -76,9 +76,9 @@ "use strict";

/**
* Creates a composable with unknown input and environment that uses schemas to parse them into known types.
* Creates a composable with unknown input and context that uses schemas to parse them into known types.
* This allows you to code the function with arbitrary types knowinng that they will be enforced in runtime.
* Very useful when piping data coming from any external source into your composables.
* After giving the input and environment schemas, you can pass a handler function that takes type safe input and environment. That function is gonna catch any errors and always return a Result.
* After giving the input and context schemas, you can pass a handler function that takes type safe input and context. That function is gonna catch any errors and always return a Result.
* @param inputSchema the schema for the input
* @param environmentSchema the schema for the environment
* @returns a handler function that takes type safe input and environment
* @param contextSchema the schema for the context
* @returns a handler function that takes type safe input and context
* @example

@@ -95,12 +95,12 @@ * const safeFunction = withSchema(

*/
function withSchema(inputSchema, environmentSchema) {
return (handler) => applySchema(inputSchema, environmentSchema)(composable(handler));
function withSchema(inputSchema, contextSchema) {
return (handler) => applySchema(inputSchema, contextSchema)(composable(handler));
}
exports.withSchema = withSchema;
/**
* Takes a composable and creates a composable withSchema that will assert the input and environment types according to the given schemas.
* Takes a composable and creates a composable withSchema that will assert the input and context types according to the given schemas.
* @param fn a composable function
* @param inputSchema the schema for the input
* @param environmentSchema the schema for the environment
* @returns a composable function that will assert the input and environment types at runtime.
* @param contextSchema the schema for the context
* @returns a composable function that will assert the input and context types at runtime.
* @example

@@ -122,15 +122,15 @@ * ```ts

*/
function applySchema(inputSchema, environmentSchema) {
return ((fn) => {
return ((input, environment) => {
const envResult = (environmentSchema ?? alwaysUnknownSchema).safeParse(environment);
function applySchema(inputSchema, contextSchema) {
return (fn) => {
return ((input, context) => {
const ctxResult = (contextSchema ?? alwaysUnknownSchema).safeParse(context);
const result = (inputSchema ?? alwaysUnknownSchema).safeParse(input);
if (!result.success || !envResult.success) {
if (!result.success || !ctxResult.success) {
const inputErrors = result.success ? [] : result.error.issues.map((error) => new errors_js_1.InputError(error.message, error.path));
const envErrors = envResult.success ? [] : envResult.error.issues.map((error) => new errors_js_1.EnvironmentError(error.message, error.path));
return Promise.resolve(failure([...inputErrors, ...envErrors]));
const ctxErrors = ctxResult.success ? [] : ctxResult.error.issues.map((error) => new errors_js_1.ContextError(error.message, error.path));
return Promise.resolve(failure([...inputErrors, ...ctxErrors]));
}
return fn(result.data, envResult.data);
return fn(result.data, ctxResult.data);
});
});
};
}

@@ -137,0 +137,0 @@ exports.applySchema = applySchema;

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.isInputError = exports.isEnvironmentError = exports.InputError = exports.ErrorList = exports.EnvironmentError = void 0;
exports.isInputError = exports.isEnvironmentError = exports.isContextError = exports.InputError = exports.ErrorList = exports.EnvironmentError = exports.ContextError = void 0;
/**

@@ -30,3 +30,4 @@ * A custom error class for input errors.

/**
* A custom error class for environment errors.
* @deprecated Use `ContextError` instead
* A custom error class for context errors.
*

@@ -42,3 +43,3 @@ * @example

/**
* Path of environment attribute that originated the error.
* Path of context attribute that originated the error.
*/

@@ -57,2 +58,27 @@ Object.defineProperty(this, "path", {

/**
* A custom error class for context errors.
*
* @example
* const aComposable = withSchema()(() => {
* throw new ContextError('Invalid context', 'user.name')
* })
*/
class ContextError extends Error {
constructor(message, path = []) {
super(message);
/**
* Path of context attribute that originated the error.
*/
Object.defineProperty(this, "path", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
this.name = 'ContextError';
this.path = path;
}
}
exports.ContextError = ContextError;
/**
* A list of errors

@@ -87,7 +113,13 @@ *

/**
* A function to check if an `Error` or a `SerializableError` is an EnvironmentError
* @deprecated Use `isContextError` instead
* A function to check if an `Error` or a `SerializableError` is a ContextError
*/
function isEnvironmentError(e) {
return e.name === 'EnvironmentError';
const isEnvironmentError = isContextError;
exports.isEnvironmentError = isEnvironmentError;
/**
* A function to check if an `Error` or a `SerializableError` is a ContextError
*/
function isContextError(e) {
return e.name === 'EnvironmentError' || e.name === 'ContextError';
}
exports.isEnvironmentError = isEnvironmentError;
exports.isContextError = isContextError;

@@ -26,3 +26,3 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
exports.environment = exports.isInputError = exports.isEnvironmentError = exports.InputError = exports.ErrorList = exports.EnvironmentError = exports.serializeError = exports.serialize = exports.inputFromUrl = exports.inputFromSearch = exports.inputFromFormData = exports.inputFromForm = exports.trace = exports.sequence = exports.pipe = exports.mergeObjects = exports.mapParameters = exports.mapErrors = exports.map = exports.collect = exports.catchFailure = exports.branch = exports.all = exports.withSchema = exports.success = exports.fromSuccess = exports.failure = exports.composable = exports.applySchema = void 0;
exports.context = exports.environment = exports.isInputError = exports.isEnvironmentError = exports.isContextError = exports.InputError = exports.ErrorList = exports.EnvironmentError = exports.ContextError = exports.serializeError = exports.serialize = exports.inputFromUrl = exports.inputFromSearch = exports.inputFromFormData = exports.inputFromForm = exports.trace = exports.sequence = exports.pipe = exports.mergeObjects = exports.mapParameters = exports.mapErrors = exports.map = exports.collect = exports.catchFailure = exports.branch = exports.all = exports.withSchema = exports.success = exports.fromSuccess = exports.failure = exports.composable = exports.applySchema = void 0;
var constructors_js_1 = require("./constructors.js");

@@ -56,8 +56,14 @@ Object.defineProperty(exports, "applySchema", { enumerable: true, get: function () { return constructors_js_1.applySchema; } });

var errors_js_1 = require("./errors.js");
Object.defineProperty(exports, "ContextError", { enumerable: true, get: function () { return errors_js_1.ContextError; } });
Object.defineProperty(exports, "EnvironmentError", { enumerable: true, get: function () { return errors_js_1.EnvironmentError; } });
Object.defineProperty(exports, "ErrorList", { enumerable: true, get: function () { return errors_js_1.ErrorList; } });
Object.defineProperty(exports, "InputError", { enumerable: true, get: function () { return errors_js_1.InputError; } });
Object.defineProperty(exports, "isContextError", { enumerable: true, get: function () { return errors_js_1.isContextError; } });
Object.defineProperty(exports, "isEnvironmentError", { enumerable: true, get: function () { return errors_js_1.isEnvironmentError; } });
Object.defineProperty(exports, "isInputError", { enumerable: true, get: function () { return errors_js_1.isInputError; } });
// FUNCTIONS WITH ENVIRONMENT
exports.environment = __importStar(require("./environment/index.js"));
// FUNCTIONS WITH CONTEXT
/**
* @deprecated use `import { context }` instead
*/
exports.environment = __importStar(require("./context/index.js"));
exports.context = __importStar(require("./context/index.js"));

@@ -10,3 +10,3 @@ "use strict";

function serializeError(error) {
if (error instanceof errors_js_2.InputError || error instanceof errors_js_1.EnvironmentError) {
if (error instanceof errors_js_2.InputError || error instanceof errors_js_1.ContextError) {
return {

@@ -13,0 +13,0 @@ exception: error,

@@ -33,9 +33,9 @@ import type { ApplySchemaReturn, Composable, ComposableWithSchema, Failure, ParserSchema, Success } from './types.js';

/**
* Creates a composable with unknown input and environment that uses schemas to parse them into known types.
* Creates a composable with unknown input and context that uses schemas to parse them into known types.
* This allows you to code the function with arbitrary types knowinng that they will be enforced in runtime.
* Very useful when piping data coming from any external source into your composables.
* After giving the input and environment schemas, you can pass a handler function that takes type safe input and environment. That function is gonna catch any errors and always return a Result.
* After giving the input and context schemas, you can pass a handler function that takes type safe input and context. That function is gonna catch any errors and always return a Result.
* @param inputSchema the schema for the input
* @param environmentSchema the schema for the environment
* @returns a handler function that takes type safe input and environment
* @param contextSchema the schema for the context
* @returns a handler function that takes type safe input and context
* @example

@@ -52,9 +52,9 @@ * const safeFunction = withSchema(

*/
declare function withSchema<I, E>(inputSchema?: ParserSchema<I>, environmentSchema?: ParserSchema<E>): <Output>(hander: (input: I, environment: E) => Output) => ComposableWithSchema<Output>;
declare function withSchema<I, C>(inputSchema?: ParserSchema<I>, contextSchema?: ParserSchema<C>): <Output>(hander: (input: I, context: C) => Output) => ComposableWithSchema<Output>;
/**
* Takes a composable and creates a composable withSchema that will assert the input and environment types according to the given schemas.
* Takes a composable and creates a composable withSchema that will assert the input and context types according to the given schemas.
* @param fn a composable function
* @param inputSchema the schema for the input
* @param environmentSchema the schema for the environment
* @returns a composable function that will assert the input and environment types at runtime.
* @param contextSchema the schema for the context
* @returns a composable function that will assert the input and context types at runtime.
* @example

@@ -76,3 +76,3 @@ * ```ts

*/
declare function applySchema<ParsedInput, ParsedEnvironment>(inputSchema?: ParserSchema<ParsedInput>, environmentSchema?: ParserSchema<ParsedEnvironment>): <R, Input, Environment>(fn: Composable<(input?: Input | undefined, environment?: Environment | undefined) => R>) => ApplySchemaReturn<ParsedInput, ParsedEnvironment, Composable<(input?: Input | undefined, environment?: Environment | undefined) => R>>;
declare function applySchema<ParsedInput, ParsedContext>(inputSchema?: ParserSchema<ParsedInput>, contextSchema?: ParserSchema<ParsedContext>): <R, Input, Context>(fn: Composable<(input?: Input | undefined, context?: Context | undefined) => R>) => ApplySchemaReturn<ParsedInput, ParsedContext, Composable<(input?: Input | undefined, context?: Context | undefined) => R>>;
export { applySchema, composable, failure, fromSuccess, success, withSchema };

@@ -17,3 +17,4 @@ /**

/**
* A custom error class for environment errors.
* @deprecated Use `ContextError` instead
* A custom error class for context errors.
*

@@ -27,3 +28,3 @@ * @example

/**
* Path of environment attribute that originated the error.
* Path of context attribute that originated the error.
*/

@@ -34,2 +35,17 @@ path: string[];

/**
* A custom error class for context errors.
*
* @example
* const aComposable = withSchema()(() => {
* throw new ContextError('Invalid context', 'user.name')
* })
*/
declare class ContextError extends Error {
/**
* Path of context attribute that originated the error.
*/
path: string[];
constructor(message: string, path?: string[]);
}
/**
* A list of errors

@@ -54,8 +70,13 @@ *

/**
* A function to check if an `Error` or a `SerializableError` is an EnvironmentError
* @deprecated Use `isContextError` instead
* A function to check if an `Error` or a `SerializableError` is a ContextError
*/
declare function isEnvironmentError(e: {
declare const isEnvironmentError: typeof isContextError;
/**
* A function to check if an `Error` or a `SerializableError` is a ContextError
*/
declare function isContextError(e: {
name: string;
message: string;
}): boolean;
export { EnvironmentError, ErrorList, InputError, isEnvironmentError, isInputError, };
export { ContextError, EnvironmentError, ErrorList, InputError, isContextError, isEnvironmentError, isInputError, };

@@ -6,4 +6,8 @@ export { applySchema, composable, failure, fromSuccess, success, withSchema, } from './constructors.js';

export { serialize, serializeError } from './serializer.js';
export { EnvironmentError, ErrorList, InputError, isEnvironmentError, isInputError, } from './errors.js';
export { ContextError, EnvironmentError, ErrorList, InputError, isContextError, isEnvironmentError, isInputError, } from './errors.js';
export type { ApplySchemaReturn, BranchReturn, CanComposeInParallel, CanComposeInSequence, Composable, ComposableWithSchema, FailToCompose, Failure, IncompatibleArguments, MapParametersReturn, MergeObjects, ParserSchema, PipeReturn, Result, SequenceReturn, SerializableError, SerializableResult, Success, UnpackAll, UnpackData, } from './types.js';
export * as environment from './environment/index.js';
/**
* @deprecated use `import { context }` instead
*/
export * as environment from './context/index.js';
export * as context from './context/index.js';

@@ -45,3 +45,3 @@ import type { Internal } from './internal/types.js';

*/
type ComposableWithSchema<O> = Composable<(input?: unknown, environment?: unknown) => O>;
type ComposableWithSchema<O> = Composable<(input?: unknown, context?: unknown) => O>;
/**

@@ -115,3 +115,3 @@ * Extract the type of the returned data when a Composable is successful.

/**
* The object used to validate either input or environment when creating composables with a schema.
* The object used to validate either input or context when creating composables with a schema.
*/

@@ -125,6 +125,6 @@ type ParserSchema<T extends unknown = unknown> = {

error: {
issues: {
path: Array<string | number>;
issues: ReadonlyArray<{
path: PropertyKey[];
message: string;
}[];
}>;
};

@@ -151,5 +151,5 @@ };

/**
* Ensure that schemas are compatible with composable input and environment otherwise return a FailToCompose.
* Ensure that schemas are compatible with composable input and context otherwise return a FailToCompose.
*/
type ApplySchemaReturn<ParsedInput, ParsedEnvironment, Fn extends Composable> = ParsedInput extends Parameters<Fn>[0] ? ParsedEnvironment extends Parameters<Fn>[1] ? ComposableWithSchema<UnpackData<Fn>> : FailToCompose<ParsedEnvironment, Parameters<Fn>[1]> : FailToCompose<ParsedInput, Parameters<Fn>[0]>;
type ApplySchemaReturn<ParsedInput, ParsedContext, Fn extends Composable> = ParsedInput extends Parameters<Fn>[0] ? ParsedContext extends Parameters<Fn>[1] ? ComposableWithSchema<UnpackData<Fn>> : FailToCompose<ParsedContext, Parameters<Fn>[1]> : FailToCompose<ParsedInput, Parameters<Fn>[0]>;
/**

@@ -156,0 +156,0 @@ * The return type of the mapParameters function

@@ -13,3 +13,3 @@ # Handling External Input

The `applySchema` function takes a schemas for the input and environment, and a composable, applying these schemas to ensure data integrity.
The `applySchema` function takes a schemas for the input and context, and a composable, applying these schemas to ensure data integrity.

@@ -31,3 +31,3 @@ ```typescript

type Test = typeof fnWithSchema
// ^? Composable<(input?: unknown, env?: unknown) => { message: string }>
// ^? Composable<(input?: unknown, ctx?: unknown) => { message: string }>
```

@@ -44,3 +44,3 @@

const runtimeSafeAdd = withSchema(z.number(), z.number())((a, b) => a + b)
// ^? Composable<(input?: unknown, env?: unknown) => number>
// ^? Composable<(input?: unknown, ctx?: unknown) => number>
const result = await runtimeSafeAdd(1, 2)

@@ -47,0 +47,0 @@ /*

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc