Life Cycles
The Operation Contextβ
This is an object comprized of a mix of input and output values of the instance during a life cycle operation ( creation or update ) plus any virtual properties (if present during the operation) defined in your schema.
import { type Context } from "ivo";
type Ctx = Context<Input, Output>;
Context Optionsβ
This is a way of providing extra information (related or not related to your schema) to operations like creation, updates and deletion. Some good usecases would be dependency injection (DI) and internationalization (i18n)
How to use:
type UserInput = {
email: string;
name: string;
};
type User = {
email: string;
id: string;
name: string;
};
interface UserRepo {
findByEmail: (email: User["email"]) => Promise<User | null>;
// ... other methods
}
type CtxOptions = {
lang: "en" | "de" | "fr"; // lang for i18n
userRepo: UserRepo; // userRepo for DI
};
// 1) define your schema
const Model = new Schema<UserInput, User, CtxOptions>({
id: { constant: true, value: generateUserId },
email: {
required: true,
async validator(value, { options: { userRepo } }) {
if (!isEmail(value))
return { valid: false, reason: "Invalid email provided" };
const isEmailTaken = await userRepo.findByEmail(value);
return isEmailTaken
? { valid: false, reason: "email already taken" }
: true;
},
},
name: { required: true, validator: validateName },
}).getModel();
// 2) pass it to related operations
import { userRepo } from "data-access/users";
// creating an entity π
Model.create(input, { lang: "en", userRepo });
// updating an entity π
Model.update(entity, changes, { lang: "en", userRepo });
// deleting an entity π
Model.delete(entity, { lang: "en", userRepo });
// 3) access the context options as below
// in a validator
function validateName(value, summary: IvoSummary<UserInput, User, CtxOptions>) {
const { options, updateOptions } = summary;
const { lang } = options;
// ... further processing
// update options
updateOptions({ lang: "de" });
return true;
}
The Operation Summaryβ
import type { Context, IvoSummary, ReadonlyIvoSummary } from "ivo";
type Input = {};
type Output = {};
type IContext = Context<Input, Output>;
type Summary = IvoSummary<Input, Output, CtxOptions>;
// π S represents is what `Summary` looks like
type S =
| Readonly<{
changes: null;
context: IContext;
inputValues: Partial<Input>;
isUpdate: false;
previousValues: null;
values: Readonly<Output>;
options: Readonly<CtxOptions>;
updateOptions: (updates: Partial<CtxOptions>) => void;
}>
| Readonly<{
changes: Partial<Readonly<Output>>;
context: IContext;
inputValues: Partial<Input>;
isUpdate: true;
previousValues: Readonly<Output>;
values: Readonly<Output>;
options: Readonly<CtxOptions>;
updateOptions: (updates: Partial<CtxOptions>) => void;
}>;
type ReadonlySummary = ReadonlyIvoSummary<Input, Output, CtxOptions>;
// π Rs represents is what `ReadonlySummary` looks like
type Rs =
| Readonly<{
changes: null;
context: IContext;
inputValues: Partial<Input>;
isUpdate: false;
previousValues: null;
values: Readonly<Output>;
options: Readonly<CtxOptions>; // π notice that the `updateOptions` method is missing
}>
| Readonly<{
changes: Partial<Readonly<Output>>;
context: IContext;
inputValues: Partial<Input>;
isUpdate: true;
previousValues: Readonly<Output>;
values: Readonly<Output>;
options: Readonly<CtxOptions>; // π notice that the `updateOptions` method is missing
}>;
const Model = new Schema<Input, Output>(definitions).getModel();
type FailureHandler = (
ctx: IContext,
options: CtxOptions,
) => void | Promise<void>;
type HandlerWithSummary = (summary: ReadonlySummary) => void | Promise<void>;
Life Cycle handlersβ
These are functions that are invoked during a life cycle operation (creation, failure or update)
onDeleteβ
A void function or array of void functions(async / sync) you want to execute every time an instance of your model gets deleted. That is; every time the model.delete method is invoked. These listeners have access to a context without virtauls even if passed to the delete method of the model. Default [ ]. They are expected to respect the signature below
// signature
function onDelete(data: Output, options: CtxOptions) {
const { id, name } = data;
const { lang } = options; // { lang: "en" }
}
// how to trigger after deleting an entity
Model.delete(entity, { lang: "en" });
onFailureβ
A function or array of functions(async / sync) you want to execute every time the create & update operations are unsuccessful. Default [ ].
N.B: They are only allowed on properties that support and have validators.
These handlers have to be triggered manually by invoking the handleFailure method of the operation's results object returned by the create & update methods of your models.
If the operation is successful,
errorandhandleFailurewill benull
// signature
function onFailure(ctx: IContext, options: CtxOptions) {
const { id, name } = ctx;
const { lang } = options; // { lang: "en" }
}
const { error, handleFailure } = await UserModel.create(userData);
// how to trigger after a validation error
if (error) await handleFailure();
onSuccessβ
A function, config object or array of config objects or functions(async / sync) you want to execute every time the create & update operations are successful. Handlers for this event should expect the operation's summary as only parameter. Default [ ]. Handlers are expected to respect the type HandlerWithSummary as shown above.
These handlers have to be triggered manually by invoking the handleSuccess method of the operation's results object returned by the create & update methods of your models.
N.B: If the operation is unsuccessful,
dataandhandleSuccesswill benull
// signature
function onSuccess(summary: Summary) {
const { ctx, options } = summary;
const { id, name } = ctx;
const { lang } = options; // { lang: "en" }
}
const { data, error, handleSuccess } = await UserModel.create(userData);
// how to trigger after successful validation
if (data) await handleSuccess();
Config objectsβ
These were introduced in version 1.4.1 to allow for more simplicity and flexibility when dealing with success handlers related to more than one property. A success config object should have the following shape:
type ConfigObject = {
properties: ArrayOfMinSizeTwo<keyof (Input & Output)>;
handler: HandlerWithSummary | HandlerWithSummary[];
};
Example:
const Model = new Schema<Input, Output>(definitions, {
onSuccess: handler, // the handler will be executed during all success operations
});
// or
const Model = new Schema<Input, Output>(definitions, {
onSuccess: [handler1, handler2], // the handlers will be executed during all success operations
});
// or
const Model = new Schema<Input, Output>(definitions, {
onSuccess: {
properties: ["email", "name"],
handler, // always executed at creation during updates with either email or name
},
});
// or
const Model = new Schema<Input, Output>(definitions, {
onSuccess: {
properties: ["email", "name"],
handler: [handler1, handler2], // always executed at creation during updates with either email or name
},
});
// or
const Model = new Schema<Input, Output>(definitions, {
onSuccess: [
handler1, // executed during all success operations
{ properties: ["id", "email"], handler: handler2 },
{ properties: ["firstName", "lastName"], handler: [handler3, handler4] },
],
});
// β
as from v1.5.1 you can provide subsets of other configs
const Model = new Schema<Input, Output>(definitions, {
onSuccess: [
{ properties: ["id", "email", "firstName"], handler: handler2 },
{ properties: ["email", "firstName"], handler: [handler3, handler4] },
],
});
// β this is not allowed
const Model = new Schema<Input, Output>(definitions, {
onSuccess: [
{ properties: ["id", "email"], handler: [handler1, handler2] },
{ properties: ["email", "id"], handler: handler3 },
],
});