/all-ok

Validate anything simply and type-safely.

Primary LanguageTypeScriptMIT LicenseMIT

all-ok

Validate anything simply and type-safely.

Installation

$ npm install all-ok

Introduction

It's important to parse input data and narrow down its types more strictly using schema validation packages such as Zod, Valibot, and others.

A typical server-side implementation first parses external input into appropriate types before passing it to domain logic, then additional validations are executed in the domain logic.

When implementing validations in domain logic, you might have the following needs:

  • Function-based validation rather than additional schema validation
  • Easy validation with server-generated objects (a transaction object and other non-external objects)
  • Sharing domain validation logic with frontend schema validation in full-stack TypeScript applications

This package addresses all these needs. While designed to work with schema validation packages, it can also be used independently.

Usage

Here are some basic usage examples. The API Document is 👉 here.

import * as aok from 'all-ok';
import * as v from 'valibot';

import { db } from '~/db';

type User = {
  name: string;
};

const checkUser = aok.checkSync(
  (user: User) => user.name.length > 3,
  "name",
  "The name should be more than 3 characters."
);

const checkNameLength = aok.checkSync(
  (name: string) => name.length < 16,
  "name",
  "The name should be less than 16 characters."
);

const checkNameUniq = aok.checkAsync(
  async (name: string) => {
    const u = await db.findUser(name);
    return !u;
  },
  "name",
  "The name should be unique."
);

const userValidation = [
  checkUser,
  aok.mapAsync(
    (user: User) => user.name,
    [
      checkNameLength,
      checkNameUniq
    ]
  ),
];

const result = await aok.runAsync(userValidation, { name: "foo" });

if (!result.ok) {
  console.log(result.errors);
  // =>
  // [
  //   { label: "name", message: "The name should be more than 3 characters." },
  //   { label: "name", message: "The name should be unique." }
  // ]
}

// With valibot

const UserSchema = v.pipe(
  v.object({
    name: v.pipe(
      v.string(),
      v.check(checkNameLength.fn, checkNameLength.error.message),
    ),
  }),
  v.forward(
    v.check(checkUser.fn, checkUser.error.message),
    [ checkUser.error.label ]
  ),
);

Run validation with transaction. You can pass any object as context to the runner.

import * as aok from 'all-ok';

import { type Database, db } from '~/db';

type Seat = {
  userId: number;
  eventId: number;
};

const checkSeatReserved = aok.checkAsync(
  async (seat: Seat, tx: Database) => {
    const s = await tx.findSeat(seat.userId, seat.eventId);
    return !s;
  },
  "event",
  "You have reserved this event already."
);

const checkSeatAvailable = aok.checkAsync(
  async (seat: Seat, tx: Database) => {
    const e = await tx.findEventWithLock(seat.eventId);
    return e.isAvailable;
  },
  "event",
  "There are no seats available for this event."
);

const seatValidation = [
  checkSeatReserved,
  checkSeatAvailable,
];

await db.transaction(async (tx) => {
  const result = await aok.runAsyncWithContext(
    seatValidation,
    { userId: 1, eventId: 1 },
    tx
  );

  if (!result.ok) {
    console.log(result.errors);
  }
});