Skip to content

Latest commit

 

History

History
121 lines (94 loc) · 5.07 KB

File metadata and controls

121 lines (94 loc) · 5.07 KB

Validation

Guards, verdicts, and fail-closed rejection.

Table of Contents

Quick Start

A guard is the optional second argument to Define. It runs before the contract and decides if the input may pass.

import { Define } from '@neabyte/typebox'

const signup = Define(
  (props: { name: string; age: number }) => ({ id: 1, name: props.name }),
  (props) => (props.age >= 18 ? true : 'age must be at least 18')
)

signup({ name: 'neo', age: 30 }) // { id: 1, name: 'neo' }

try {
  signup({ name: 'kid', age: 12 })
} catch (error) {
  if (error instanceof Error && Array.isArray(error.cause)) {
    console.log(error.cause) // ['age must be at least 18']
  }
}

Verdicts

A guard returns a GuardVerdict. Validation is fail-closed, so a guard can never let bad input pass by mistake.

Verdict Meaning
true Pass
a non-empty string Reject with that one reason
a string[] of reasons Reject with all reasons
an empty array [] Reject with a generic reason 'validation failed'
anything else Invalid verdict, rejected with 'validation failed'

Only true, a string, or an array whose every member is a string is a valid verdict. Anything else, such as false, 0, NaN, a number, a plain object, or a mixed array, is treated as invalid and rejected.

// collect every failure in one pass
const create = Define(
  (props: { username: string; password: string }) => props,
  (props) => {
    const reasons: string[] = []
    if (props.username.length < 3) {
      reasons.push('username must be at least 3 characters')
    }
    if (props.password.length < 8) {
      reasons.push('password must be at least 8 characters')
    }
    return reasons.length === 0 ? true : reasons
  }
)

A guard may be one function or a list. Every guard runs in order, and the first one that returns reasons rejects.

const create = Define(
  (props: { name: string }) => props,
  [
    (props) => (props.name.length > 0 ? true : 'name required'),
    (props) => (props.name.length <= 32 ? true : 'name too long')
  ]
)

Reading Rejections

On failure the reasons are thrown as a TypeError. The message joins the reasons with the separator "; ", and the full reason list is on cause. Read cause instead of parsing the message. Only a guard rejection carries a cause; an error thrown from inside a contract or a step is not wrapped and propagates as is, keeping its original type and full stack trace.

try {
  create({ name: '' })
} catch (error) {
  if (error instanceof Error && Array.isArray(error.cause)) {
    const reasons = error.cause as readonly string[]
    for (const reason of reasons) {
      console.log('-', reason)
    }
  }
}

API Reference

GuardFn<ContractType>

A synchronous guard for a contract's input. It receives the same input as the contract and returns a GuardVerdict. See GuardFn for the full generic form.

type GuardFn = (input: ContractInput) => GuardVerdict

GuardVerdict

type GuardVerdict = true | string | readonly string[]

Behavior Notes

  • When the input is an object, each guard receives the original input after it is deep-frozen in place, so a guard can read but cannot mutate it. The same reference is frozen and passed to the guard, so the caller's own value is frozen as well. The prototype and methods of the input are preserved. When the input is not an object, the guard receives it unchanged.
  • Guards must be synchronous. Returning a thenable throws TypeError('async guard unsupported').
  • A string input longer than 10000 characters is rejected before any guard runs, with the length reason on cause.
  • When an object input is frozen for the guard, any nested string value longer than 10000 characters is rejected with a TypeError. The message is input exceeds 10000 characters and the same reason is on cause.
  • When an object input is frozen for the guard, nesting that reaches 256 levels deep is rejected with a TypeError. The message is input nesting exceeds 256 levels and the same reason is on cause.
  • An invalid verdict throws TypeError('guard returned invalid verdict') with cause: ['validation failed'].