Skip to content
/ ivo Public

A user story-focused event-driven schema validator for JS/TS backends

License

Notifications You must be signed in to change notification settings

kamtoeddy/ivo

Repository files navigation

Foreword

ivo is a user story focused event-driven schema validator which provides an interface for you to clearly define the behaviour of your entities at creation and during updates

Installation

$ npm i ivo

Importing

// CJS
const { Schema } = require("ivo");

// ESM
import { Schema } from "ivo";

Defining a schema

import { Schema, type MutableSummary } from "ivo";

type UserInput = {
  email: string | null;
  phoneNumber: string | null;
  username: string;
};

type User = {
  id: string;
  createdAt: Date;
  email: string | null;
  phoneNumber: string | null;
  updatedAt: Date | null;
  username: string;
  usernameLastUpdatedAt: Date | null;
};

const userSchema = new Schema<UserInput, User>(
  {
    id: { constant: true, value: generateUserID },
    email: {
      default: null,
      required: isEmailOrPhoneRequired,
      validator: [validateEmail, makeSureEmailIsUnique],
    },
    phoneNumber: {
      default: null,
      required: isEmailOrPhoneRequired,
      validator: validatePhoneNumber,
    },
    username: {
      required: true,
      validator: [validateUsername, makeSureUsernameIsUnique],
      shouldUpdate({ usernameLastUpdatedAt }) {
        if (!usernameLastUpdatedAt) return true;

        const timeDifferenceInMillisecs =
          new Date().getTime() - usernameUpdatableFrom.getTime();
        const thirtyDaysInMillisecs = 2_592_000_000;

        return timeDifferenceInMillisecs >= thirtyDaysInMillisecs;
      },
    },
    usernameLastUpdatedAt: {
      default: null,
      dependsOn: "username",
      resolver: ({ isUpdate }) => (isUpdate ? new Date() : null),
    },
  },
  { timestamps: true },
);

function isEmailOrPhoneRequired({
  context: { email, phoneNumber },
}: MutableSummary<UserInput, User>) {
  return [!email && !phoneNumber, 'Provide "email" or "phone" number'] as const;
}

async function makeSureEmailIsUnique(email: string) {
  const userWithEmail = await usersDb.findByEmail(email);

  return userWithEmail ? { valid: false, reason: "Email already taken" } : true;
}

async function makeSureUsernameIsUnique(username: string) {
  const userWithUsername = await usersDb.findByUsername(username);

  return userWithUsername
    ? { valid: false, reason: "Username already taken" }
    : true;
}

// get the model
const UserModel = userSchema.getModel();

Creating an entity

const { data, error } = await UserModel.create({
  email: "[email protected]",
  id: 5, // will be ignored because it is a constant property
  name: "John Doe", // will be ignored because it is not on schema
  username: "john_doe",
  updatedAt: new Date(), // will be ignored because it is a timestamp
  usernameLastUpdatedAt: new Date(), // will be ignored because it is a dependent property
});

if (error) return handleError(error);

console.log(data);
// {
//   createdAt: new Date(),
//   email: '[email protected]',
//   id: 101,
//   phoneNumber: null,
//   updatedAt: null,
//   username: 'john_doe',
//   usernameLastUpdatedAt: null
// }

// data is safe to dump in db
await usersDb.insertOne(data);

Updating an entity

const user = await usersDb.findByID(101);

if (!user) return handleError({ message: "User not found" });

const { data, error } = await UserModel.update(user, {
  usernameLastUpdatedAt: add(new Date(), { days: 31 }), // dependent property -> will be ignored
  id: 75, // constant property -> will be ignored
  age: 34, // not on schema -> will be ignored
  username: "johndoe",
});

if (error) return handleError(error);

console.log(data);
// {
//   updatedAt: new Date(),
//   username: 'johndoe',
//   usernameLastUpdatedAt: new Date() // value returned from resolver -> current date
// }

await usersDb.updateByID(user.id, data);
// any further attempt to update 'username' will be ignored until
// the 'shouldUpdate' rule returns true

const { error } = await UserModel.update(user, { username: "john-doe" });

console.log(error);
// {
//   message: 'NOTHING_TO_UPDATE',
//   payload: {}
// }

Docs

About

A user story-focused event-driven schema validator for JS/TS backends

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published