Skip to content

Latest commit

 

History

History
335 lines (253 loc) · 13.9 KB

index.md

File metadata and controls

335 lines (253 loc) · 13.9 KB

Defining a schema

Clean schema considers a property to be properly defined if it is dependent, readonly, required, a virtual or has a default value other than undefined

N.B: Clean schema will throw an error if a property is not properly defined. The Schema constructor accepts 2 arguments:

  1. definitions (required)
  2. options (optional)

The schema constructor also takes two generic types you could use to improve on the type inference of your Input & Output data.

const userSchema = new Schema<Input, Output>(definitions, options);
import { Schema } from 'ivo';

type UserInput = {
  dob: Date | null;
  firstName: string;
  lastName: string;
};

type User = {
  dob: Date | null;
  firstName: string;
  lastName: string;
  fullName: string;
};

const userSchema = new Schema<UserInput, User>({
  dob: { required: true, validator: validateDob },
  firstName: { required: true, validator: validateName },
  lastName: { required: true, validator: validateName },
  fullName: {
    default: '',
    dependsOn: ['firstName', 'lastName'],
    resolver({ context: { firstName, lastName } }) {
      return `${firstName} ${lastName}`;
    },
  },
});

const UserModel = userSchema.getModel();

Properties of a model

These methods are async because custom validators could be async as well.

Property Type Description
create function Async method to create an instance
delete function Async method to trigger all onDelete listeners
update function Async method to update an instance

Accepted rules

Property Type Description
allow any[ ] | object used to specify the values that should be accepted for a property. See more
constant boolean use with value rule to specify a property with a forever constant value. more
default any | function the default value of a propterty. more
dependsOn string | string[ ] a property or list of property the said property depends on. more
onDelete function | function[ ] executed when the delete method of a model is invoked more
onFailure function | function[ ] executed after an unsucessful operation more
onSuccess function | function[ ] executed after a sucessful operation more
readonly boolean | 'lax' a propterty whose value should not change more
required boolean | function a property that must be set during an operation more
sanitizer function This could be used to transform a virtual property before their dependent properties get resolved. more
shouldInit false | function(): boolean A boolean or setter that tells ivo whether or not a property should be initialized.
shouldUpdate false | function(): boolean A boolean or setter that tells ivo whether or not a property should be initialized.
validator function A function (async / sync) used to validated the value of a property. more
value any | function value or setter of constant property. more
virtual boolean a helper property that can be used to provide extra context but does not appear on instances of your model more

Options

import type {DeletionContext, ImmutableContext, ImmutableSummary, ValidationErrorMessage } from 'ivo'

type Input = {}
type Output = {}

type IContext = Context<Input, Output>
type ISummary = ImmutableSummary<Input, Output>

type DeleteListener = (data: DeletionContext<Output>) => void | Promise<void>

type SuccessListener = (summary: ISummary) => void | Promise<void>

type Timestamp = {
  createdAt?: string
  updatedAt?: string
}

interface ErrorToolClass<ErrorTool, CtxOptions extends ObjectType> {
  new (message: ValidationErrorMessage, ctxOptions: CtxOptions): ErrorTool;
}

type SchemaOptions = {
  equalityDepth?: number
  errorTool?: ErrorToolClass // more on this below 👇
  errors?: 'silent' | 'throw'
  onDelete?: DeleteListener | DeleteListener[]
  onSuccess?: SuccessListener | SuccessListener[]
  postValidate?: PostValidationConfig | PostValidationConfig[]
  setMissingDefaultsOnUpdate?: boolean
  shouldUpdate?: boolean | (summary: ISummary) => boolean
  timestamps?: boolean | Timestamp
  useParentOptions?: boolean // 👈 only for extended schemas
}

const options: SchemaOptions = {}

const schema = new Schema<Input, Output, CtxOptions, ErrorToolClass>(definitions, options)

More details on the Context & Summary utiliies can be found here

equalityDepth

This is the number used to determine if the value of a property has changed during updates.

To determine if a property has changed, it's value is compared against it's default value and previous value. Because object equality is not always straightforward, the equalityDepth provided is used to determine if properties of your schema that accept objects (which may have nested objects) as values have changed during updates

The possible values allowed for this number range from 0 to +Infinity. The default value is 1, which means one level of nesting.

Here is a snippet to demonstrate how changing just the arragement of values of nested properties (without even changing their actual values) can affect the results of an update:

const user = {
  name: 'John Doe',
  bio: {
    facebook: { displayName: 'john', handle: 'john3434' },
    twitter: { displayName: 'John Doe', handle: 'john_on_twitter' },
  },
};

// depth == 0

Model.update(user, { bio: user.bio }).then(({ data, error }) => {
  console.log(data); // null
  console.log(error.message); // Nothing to update
});

// 👇 changing the positions of facebook & twitter in bio
Model.update(user, {
  bio: {
    twitter: { displayName: 'John Doe', handle: 'john_on_twitter' },
    facebook: { displayName: 'john', handle: 'john3434' },
  },
}).then(({ data, error }) => {
  console.log(data);
  // {
  //   bio: {
  //     facebook: { displayName: 'john', handle: 'john3434' },
  //     twitter: { displayName: 'John Doe', handle: 'john_on_twitter' }
  //     }
  // }

  console.log(error); // null
});

// depth == 1

Model.update(user, { bio: user.bio }).then(({ data, error }) => {
  console.log(data); // null
  console.log(error.message); // Nothing to update
});

// 👇 changing the positions of facebook & twitter in bio
Model.update(user, {
  bio: {
    twitter: { displayName: 'John Doe', handle: 'john_on_twitter' },
    facebook: { displayName: 'john', handle: 'john3434' },
  },
}).then(({ data, error }) => {
  console.log(data); // null
  console.log(error.message); // Nothing to update
});

// 👇 changing the positions of facebook & twitter in bio and the positions of displayName & handle
Model.update(user, {
  bio: {
    twitter: { handle: 'john_on_twitter', displayName: 'John Doe' },
    facebook: { displayName: 'john', handle: 'john3434' },
  },
}).then(({ data, error }) => {
  console.log(data);
  // {
  //   bio: {
  //     facebook: { displayName: 'john', handle: 'john3434' },
  //     twitter: { handle: 'john_on_twitter', displayName: 'John Doe' }
  //     }
  // }

  console.log(error); // null
});

errorTool

This is a class which will be used to manage your validation errors, hence giving you the power to have custom validation errors. See example here

import type { ValidationErrorMessage, IErrorTool } from 'ivo';

// the class should have this signature 👇
interface ErrorToolClass<ErrorTool, CtxOptions extends ObjectType> {
  new (message: ValidationErrorMessage, ctxOptions: CtxOptions): ErrorTool;
}

// the instances of your ErrorTool class should have this signature 👇
interface IErrorTool<ExtraData extends ObjectType = {}> {
  /** return what your validation error should look like from this method */
  get data(): IValidationError<ExtraData>;

  /** return a custom error that will be thrown when validation fails & Schema.option.errors == 'throws' */
  get error(): Error;

  /** array of fields that have failed validation */
  get fields(): string[];

  /** determines if validation has failed */
  get isLoaded(): boolean;

  /** used to append a field to your final validation error */
  add(field: FieldKey, error: FieldError, value?: any): this;

  /** method to set the value of the validation error message */
  setMessage(message: ValidationErrorMessage): this;
}

type IValidationError<ExtraData extends ObjectType = {}> = ({
  message: ValidationErrorMessage;
} & ExtraData) & {};

errors

This option is to specify the way the errors should be treated. If set to silent, the errors will be returned in the operation's resolved results but if set to throw, it will simply throw the error(you may want to use in a try-catch block). The default value is 'silent'

This is the structure of the error returned or thrown

type SchemaErrorMessage =
  | 'INVALID_SCHEMA'
  | 'NOTHING_TO_UPDATE'
  | 'VALIDATION_ERROR';

type SchemaError = {
  message: SchemaErrorMessage;
  payload: {
    [key: string]: {
      reasons: string[]; // e.g. name: ["Invalid name", "too long"]
      metadata: any;
    };
  };
};

onDelete

This could be a function or an array of functions with the DeleteListener signature above. These functions would be triggered together with the onDelete listeners of individual properties when the Model.delete method is invoked. See more here

onSuccess

This could be a function or an array of functions with the SuccessListener signature above. These functions would be triggered together with the onSuccess listeners of individual properties when the handleSuccess method is invoked at creation & during updates of any property. See more here

postValidate

To validate integrity of more than one field after initial validation. More on this here

setMissingDefaultsOnUpdate

A boolean. If set to true, it'll check all defaultable properties of the existing data passed to the model's update method Model.update(existingData, updates), for all the properties with value undefined it'll generate their default values, add these them to the operation's context before validating the updates provided.

If the update operation is successful, the newly generated default values will also be added to the updated values returned if not already present on the updated values. Default false

shouldUpdate

A boolean or a function that expects the operation's summary and returns a boolean value. This value is read/computed before the values provided during updates have been validated.

If it's value or computed value if true, validations for updates will proceed else, the operation will fail with error message Nothing to update

new Schema(
  {
    id: { constant: true, value: generateId },
  },
  {
    shouldUpdate: () => (condition ? true : false),
  },
);

timestamps

If timestamps is set to true, you'll automatically have the createdAt and updatedAt properties attached to instances of your model at creation & during update. But you can overwrite the options and use your own properties like in the example below. Default false

Overwrite one

let transactionSchema = new Schema(definitions, {
  timestamps: { createdAt: 'created_at' },
});

Or both

let transactionSchema = new Schema(definitions, {
  timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' },
});

To use one timestamp alone, pass false for the timestamp key to eliminate

let transactionSchema = new Schema(definitions, {
  timestamps: { createdAt: 'created_at', updatedAt: false },
});

// or
let transactionSchema = new Schema(definitions, {
  timestamps: { updatedAt: false },
});

useParentOptions

When extending schemas, extended schemas automatically inherit all options(except life cycle methods) of base schema. Setting useParentOptions: false in extended schema option will prevent this behaviour. Default is true