trilogy
Version:
TypeScript SQLite layer with support for both native C++ & pure JavaScript drivers.
186 lines (185 loc) • 7.11 kB
JavaScript
import { invariant } from './util';
import { normalizeCriteria } from './helpers';
export var Hook;
(function (Hook) {
Hook["OnQuery"] = "ON_QUERY";
Hook["BeforeCreate"] = "BEFORE_CREATE";
Hook["AfterCreate"] = "AFTER_CREATE";
Hook["BeforeUpdate"] = "BEFORE_UPDATE";
Hook["AfterUpdate"] = "AFTER_UPDATE";
Hook["BeforeRemove"] = "BEFORE_REMOVE";
Hook["AfterRemove"] = "AFTER_REMOVE";
})(Hook || (Hook = {}));
export const EventCancellation = Symbol('trilogy.EventCancellation');
/**
* Base implementation of lifecycle hooks inherited by all model instances.
*/
export class Hooks {
constructor() {
this._onQuery = new Set();
this._onQueryAll = new Set();
this._beforeCreate = new Set();
this._afterCreate = new Set();
this._beforeUpdate = new Set();
this._afterUpdate = new Set();
this._beforeRemove = new Set();
this._afterRemove = new Set();
}
/**
* The `onQuery` hook is called each time a query is run on the database,
* and receives the query in string form.
*
* @param fn Function called when the hook is triggered
* @param [options]
*
* @returns Unsubscribe function that removes the subscriber when called
*/
onQuery(fn, options = {}) {
invariant(typeof fn === 'function', 'hook callbacks must be of type function');
if (options.includeInternal) {
this._onQuery.add(fn);
this._onQueryAll.add(fn);
return () => this._onQueryAll.delete(fn) && this._onQuery.delete(fn);
}
else {
this._onQuery.add(fn);
return () => this._onQuery.delete(fn);
}
}
/**
* Before an object is created, the beforeCreate hook is called with the
* object.
*
* @remarks
* This hook occurs before casting, so if a subscriber to this hook
* modifies the incoming object those changes will be subject to casting.
* It's also possible to prevent the object from being created entirely
* by returning the EventCancellation symbol from a subscriber callback.
*
* @param fn Function called when the hook is triggered
*
* @returns Unsubscribe function that removes the subscriber when called
*/
beforeCreate(fn) {
invariant(typeof fn === 'function', 'hook callbacks must be of type function');
this._beforeCreate.add(fn);
return () => this._beforeCreate.delete(fn);
}
/**
* When an object is created, that object is returned to you and the
* `afterCreate` hook is called with it.
*
* @param fn Function called when the hook is triggered
*
* @returns Unsubscribe function that removes the subscriber when called
*/
afterCreate(fn) {
invariant(typeof fn === 'function', 'hook callbacks must be of type function');
this._afterCreate.add(fn);
return () => this._afterCreate.delete(fn);
}
/**
* Prior to an object being updated the `beforeUpdate` hook is called with the
* update delta, or the incoming changes to be made, as well as the criteria.
*
* @remarks
* Casting occurs after this hook. A subscriber could choose to cancel the
* update by returning the EventCancellation symbol or alter the selection
* criteria.
*
* @param fn Function called when the hook is triggered
*
* @returns Unsubscribe function that removes the subscriber when called
*/
beforeUpdate(fn) {
invariant(typeof fn === 'function', 'hook callbacks must be of type function');
this._beforeUpdate.add(fn);
return () => this._beforeUpdate.delete(fn);
}
/**
* Subscribers to the `afterUpdate` hook receive modified objects after they
* are updated.
*
* @param fn Function called when the hook is triggered
*
* @returns Unsubscribe function that removes the subscriber when called
*/
afterUpdate(fn) {
invariant(typeof fn === 'function', 'hook callbacks must be of type function');
this._afterUpdate.add(fn);
return () => this._afterUpdate.delete(fn);
}
/**
* Before object removal, the criteria for selecting those objects is passed
* to the `beforeRemove` hook.
*
* @remarks
* Casting occurs after this hook. Subscribers can modify the selection
* criteria or prevent the removal entirely by returning the `EventCancellation`
* symbol.
*
* @param fn Function called when the hook is triggered
*
* @returns Unsubscribe function that removes the subscriber when called
*/
beforeRemove(fn) {
invariant(typeof fn === 'function', 'hook callbacks must be of type function');
this._beforeRemove.add(fn);
return () => this._beforeRemove.delete(fn);
}
/**
* A list of any removed objects is passed to the `afterRemove` hook.
*
* @param fn Function called when the hook is triggered
*
* @returns Unsubscribe function that removes the subscriber when called
*/
afterRemove(fn) {
invariant(typeof fn === 'function', 'hook callbacks must be of type function');
this._afterRemove.add(fn);
return () => this._afterRemove.delete(fn);
}
async _callHook(hook, arg, options) {
const result = {
prevented: false
};
if (hook === Hook.OnQuery) {
const [query, internal] = arg;
const fns = internal ? this._onQueryAll : this._onQuery;
for (const fn of fns) {
if (await fn(query) === EventCancellation) {
result.prevented = true;
}
}
return result;
}
const fns = {
[Hook.BeforeCreate]: this._beforeCreate,
[Hook.AfterCreate]: this._afterCreate,
[Hook.BeforeUpdate]: this._beforeUpdate,
[Hook.AfterUpdate]: this._afterUpdate,
[Hook.BeforeRemove]: this._beforeRemove,
[Hook.AfterRemove]: this._afterRemove
}[hook];
for (const fn of fns) {
let thisResult;
if (hook === Hook.BeforeUpdate) {
const [data, criteria] = arg;
thisResult = await fn(data, normalizeCriteria(criteria), options || {});
}
else if (hook === Hook.BeforeRemove) {
thisResult = await fn(normalizeCriteria(arg), options || {});
}
else if (hook === Hook.BeforeCreate || hook === Hook.AfterCreate) {
thisResult = await fn(arg, options || {});
}
else {
thisResult = await fn(arg, options || {});
}
if (thisResult === EventCancellation) {
result.prevented = true;
}
}
return result;
}
}