npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

pothos-plugin-valibot

v0.2.0

Published

A Pothos plugin for adding argument validation

Downloads

352

Readme

Valibot Validation Plugin

@foadonis/magnify License

A plugin for adding validation for field arguments based on Valibot. This plugin does not expose valibot directly, but most of the options map closely to the validations available in valibot.

Usage

Install

To use the valibot plugin you will need to install both valibot package and the valibot plugin:

pnpm add valibot pothos-plugin-valibot

Setup

import ValibotPlugin from "pothos-plugin-valibot";
const builder = new SchemaBuilder({
  plugins: [ValibotPlugin],
  valibot: {
    // optionally customize how errors are formatted
    validationError: (valiError, args, context, info) => {
      // the default behavior is to just throw the valibot error directly
      return valiError;
    },
  },
});

builder.queryType({
  fields: (t) => ({
    simple: t.boolean({
      args: {
        // Validate individual args
        email: t.arg.string({
          validate: {
            email: true,
          },
        }),
        phone: t.arg.string(),
      },
      // Validate all args together
      validate: {
        check: (args) => !!args.phone || !!args.email,
      }
      resolve: () => true,
    }),
  }),
});

Options

validationError: (optional) A function that will be called when validation fails. The function will be passed the the valibot validation error, as well as the args, context and info objects. It can throw an error, or return an error message or custom Error instance.

Examples

With custom message

builder.queryType({
  fields: (t) => ({
    withMessage: t.boolean({
      args: {
        email: t.arg.string({
          validate: {
            email: [true, "invalid email address"],
          },
        }),
        phone: t.arg.string(),
      },
      validate: {
        check: [
          (args) => !!args.phone || !!args.email,
          "Must provide either phone number or email address",
        ],
      },
      resolve: () => true,
    }),
  }),
});

Validating List

builder.queryType({
  fields: (t) => ({
    list: t.boolean({
      args: {
        list: t.arg.stringList({
          validate: {
            items: {
              email: true,
            },
            maxLength: 3,
          },
        }),
      },
      resolve: () => true,
    }),
  }),
});

Using your own valibot schemas

If you just want to use a valibot schema defined somewhere else, rather than using the validation options you can use the schema option:

builder.queryType({
  fields: (t) => ({
    list: t.boolean({
      args: {
        max5: t.arg.int({
          validate: {
            schema: v.pipeAsync(v.number(), v.integer(), v.maxValue(5)),
          },
        }),
      },
      resolve: () => true,
    }),
  }),
});

You can also validate all arguments together using a valibot schema:

builder.queryType({
  fields: (t) => ({
    simple: t.boolean({
      args: {
        email: t.arg.string(),
        phone: t.arg.string(),
      },
      // Validate all args together using own zod schema
      validate: {
        schema: v.object({
          email: v.pipeAsync(v.string(), v.email()),
          phone: v.string(),
        }),
      },
      resolve: () => true,
    }),
  }),
});

API

On Object fields (for validating field arguments)

  • validate: ValidationOptions.

On InputObjects (for validating all fields of an input object)

  • validate: ValidationOptions.

On arguments or input object fields (for validating a specific input field or argument)

  • validate: ValidationOptions.

RefineConstraint and CheckConstraints

A RefineConstraint is a function that can be used to customize valibot schema. It receives the ValiSchema of args object, input object, or specific field the refinement is defined on. It should return an another ValiSchema.

{
  email: t.arg.string(),
  phone: t.arg.string(),
  validate: {
    refine: (schema) =>
      v.pipeAsync(
        schema,
        v.forward(
          v.check(
            (args) => args.email.toLocaleLowerCase() === args.email,
            'email should be lowercase',
          ),
          ['email'],
        ),
      )
  }
}

CheckConstraints is a function, or array of fuctions, or a tuple of function and optionally error message, or array of tuples of function and optonally error message. Check function must return a boolean of Promise<boolean>.

{
  email: t.arg.string(),
  phone: t.arg.string(),
  validate: {
    check: [
      (args) => !!args.phone || !!args.email,
      'Must provide either phone number or email address',
    ],
  }
}

ValidationOptions

The validation options available depend on the type being validated. Each property of ValidationOptions can either be a value specific to the constraint, or a tuple with the value, and the options passed to the underlying valibot method. This options can be used to set a custom error message:

{
  validate: {
    maxValue: [10, 'should not be more than 10'],
    integer: true,
  }
}

Number

  • type?: 'number'
  • refine?: RefineContraint<number>
  • check?: CheckConstraints<number>
  • minValue?: Constraint<number>
  • maxValue?: Constraint<number>
  • integer?: Constraint<boolean>
  • schema?: ValiSchema<number>

BigInt

  • type?: 'bigint'
  • refine?: RefineConstraint<bigint>
  • check?: CheckConstraints<bigint>
  • minValue?: Constraint<bigint>
  • maxValue?: Constraint<bigint>
  • schema?: ValiSchema<bigint>

Boolean

  • type?: 'boolean'
  • refine?: RefineConstraint<boolean>
  • check?: CheckConstraints<boolean>
  • schema?: ValiSchema<boolean>

Date

  • type?: 'boolean'
  • refine?: RefineConstraint<Date>
  • check?: CheckConstraints<Date>
  • minValue?: Constraint<Date>
  • maxValue?: Constraint<Date>
  • schema?: ValiSchema<Date>

File

  • type?: 'boolean'
  • refine?: RefineConstraint<File>
  • check?: CheckConstraints<File>
  • minSize?: Constraint<number>
  • maxSize?: Constraint<number>
  • mimeType?: Constraint<string[]>
  • schema?: ValiSchema<File>

String

  • type?: 'string';
  • refine?: RefineConstraint<string>
  • check?: CheckConstraints<string>
  • trim?: boolean
  • nonEmpty?: Constraint<boolean>
  • minLength?: Constraint<number>
  • maxLength?: Constraint<number>
  • length?: Constraint<number>
  • url?: Constraint<boolean>
  • uuid?: Constraint<boolean>
  • email?: Constraint<boolean>
  • regex?: Constraint<RegExp>
  • schema?: ValiSchema<string>

Object

  • type?: 'object';
  • refine?: RefineConstraint<T>
  • check?: CheckConstraints<T>
  • schema?: ValiSchema<T>

Array

  • type?: 'array';
  • refine?: RefineConstraint<T[]>
  • check?: CheckConstraints<T[]>
  • nonEmpty?: Constraint<boolean>
  • minLength?: Constraint<number>
  • maxLength?: Constraint<number>
  • length?: Constraint<number>
  • items?: ValidationOptions<T>
  • schema?: ValiSchema<T[]>

How it works

Each arg on an object field, and each field on an input type with validation will build its own valibot validator. These validators will be a union of all potential types that can apply the validations defined for that field. For example, if you define an optional field with a maxLength validator, it will create a valibot schema that looks something like:

v.union([
  v.null(),
  v.undefined(),
  v.pipe(v.array(), v.maxLength(5)),
  v.pipe(v.string(), v.maxLength(5)),
]);

If you set and email validation instead the schema might look like:

v.union([v.null(), v.undefined(), v.pipe(v.string(), v.email())]);

At runtime, we don't know anything about the types being used by your schema, we can't infer the expected js type from the type definition, so the best we can do is limit the valid types based on what validations they support. The type validation allows explicitly validating the type of a field to be one of the base types supported by valibot:

// field
{
validate: {
  type: 'string',
  maxLength: 5
}
// generated
v.union([v.null(), v.undefined(), v.pipe(v.string(), v.maxLength(5))]);

There are a few exceptions the above:

  1. args and input fields that are InputObjects always use v.object() rather than creating a union of potential types.

  2. args and input fields that are list types always use v.array().

  3. If you only include a refine or check validation we will just use vs unknown validator instead:

// field
{
  validate: {
    check: (val) => isValid(val)
  },
}
// generated
v.union([v.null(), v.undefined(), v.pipe(v.unknown(), v.check((val) => isValid(val)))]);

If the validation options include a schema that schema will be used as an intersection wit the generated validator:

// field
{
  validate: {
    integer: true,
    schema: v.pipeAsync(v.number(), v.maxValue(10)),
  }
}
// generated
v.union([
    v.null(),
    v.undefined(),
    v.intersect([
        v.pipe(v.number(), v.maxValue(10)),
        v.pipe(v.number(), v.integer())
    ])
]);

Sharing schemas with client code

The easiest way to share validators is the use the to define schemas for your fields in an external file using the normal valibot APIs, and then attaching those to your fields using the schema option.

// shared
import { ValidationOptions } from '@pothos/plugin-zod';

const numberValidation = v.pipe(v.number(), v.maxValue(5));

// server
builder.queryType({
  fields: (t) => ({
    example: t.boolean({
      args: {
        num: t.arg.int({
          validate: {
            schema: numberValidation,
          }
        }),
      },
      resolve: () => true,
    }),
  });
});

// client
v.parse(numberValidator, 3) // pass
v.parse(numberValidator, '3') // fail

You can also use the createValibotSchema helper from the plugin directly to create valibot Schemas from an options object:

// shared
import { ValidationOptions } from 'pothos-plugin-valibot';

const numberValidation: ValidationOptions<number> = {
  max: 5,
};

// server
builder.queryType({
  fields: (t) => ({
    example: t.boolean({
      args: {
        num: t.arg.int({
          validate: numberValidation,
        }),
      },
      resolve: () => true,
    }),
  });
});

// client
import { createValibotSchema } from 'pothos-plugin-valibot';

const validator = createValibotSchema(numberValidator);

v.parseAsync(validator, 3) // pass
v.parseAsync(validator, '3') // fail