Overview
Operation hooks are not tied to a particular method, but rather are triggered from all methods that execute a particular high-level create, read, update, or delete operation.
These are all methods of PersistedModel that application models inherit.
Using operation hooks enables you to intercept actions that modify data independent of the specific method that invokes them (for example, create
, save
, or updateOrCreate
).
The API is simple: the method Model.observe(name, observer)
, where name
is the string name of the operation hook, for example “before save”,
and observer
is function observer(context, callback)
. Child models inherit observers, and you can register multiple observers for a hook.
The following table summarizes the operation hooks invoked by PersistedModel create, retrieve, update, and delete methods.
Method → Operation hook ↓ |
find findOne findById |
exists | count | create | upsert | findOrCreate | deleteAll deleteById |
updateAll | prototype .save |
prototype .delete |
prototype .updateAttributes |
prototype .replaceAttributes |
replaceById | replaceOrCreate | upsertWithWhere |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
access | X | X | X | X | X | X | X | X | X | ||||||
before save | X | X | X | X | X | X | X | X | X | X | |||||
after save | X | X | X | X | X | X | X | X | X | X | |||||
before delete | X | X | |||||||||||||
after delete | X | X | |||||||||||||
loaded | X | X | X | X | X | X | X | X | X | X | X | X | |||
persist | X | X | X | X | X | X | X | X | X | X |
Using async/await
Operation hooks can also return a promise instead of calling the next parameter.
MyModel.observe('before save', async function(ctx) {
//...
return;
});
Operation hook context object
The context
object is specific to operation hooks and does not have any relation to the context object passed to remote hooks registered via
Model.beforeRemote
and Model.afterRemote
. See Remote hooks for more information.
Note that the context object is not related to the “current context” provided by loopback.getCurrentContext()
either.
Properties common for all hooks and operations
Target model
The property context.Model
is set to the constructor of the model that is the target of the operation. For example Product.find()
sets context.Model = Product
.
Operation options
The context object has an options
property that enables hooks to access any options provided by the caller of the specific model method (operation).
For example:
var FILTERED_PROPERTIES = ['immutable', 'birthday'];
MyModel.observe('before save', function filterProperties(ctx, next) {
if (ctx.options && ctx.options.skipPropertyFilter) return next();
if (ctx.instance) {
FILTERED_PROPERTIES.forEach(function(p) {
ctx.instance.unsetAttribute(p);
});
} else {
FILTERED_PROPERTIES.forEach(function(p) {
delete ctx.data[p];
});
}
next();
});
// immutable is not updated
MyModel.updateOrCreate({
id: 1,
immutable: 'new value'
}, cb);
// immutable is changed
MyModel.updateOrCreate({
id: 2,
immutable: 'new value'
}, {
skipPropertyFilter: true
}, cb);
Shared hookState property
Use the ctx.hookState
property to share data between hooks (for example, “before save” and “after save”). The value of the ctx.hookState
property is preserved across all hooks invoked for a single operation.
For example, “access”, “before save” and “after save” hooks that are invoked for MyModel.create()
have the same object passed in ctx.hookState
.
In contrast, ctx.options
is set using options argument provided to PersistedModel methods like MyModel.find()
or MyModel.create()
. If no options argument was provided, then ctx.options
is set to an empty object, so that hooks don’t have to check whether ctx.options
is set.
Hook and operation specific properties
Besides the common properties listed above, each hook provides additional properties identifying the model instance(s) affected by the operation and the changes applied.
The general rule is that the context provides either an instance
property or a pair of data
and where
properties.
instance
This property is provided when the operation affects a single instance and performs a full update/create/delete of all model properties,
for example PersistedModel.create()
.
where + data
When the operation affects multiple instances (e.g. PersistedModel.updateAll()
) or performs a partial update of a subset of model properties
(e.g. PersistedModel.prototype.updateAttributes()
), the context provides a where filter
used to find the affected records and plain data
object containing the changes to be made.
isNewInstance
Some operations provide a flag to distinguish between a CREATE operation and an UPDATE operation. See the documentation of individual hooks for more information.
Important:
Only certain connectors support ctx.isNewInstance
. With other connectors it is undefined.
See Checking for support of ctx.isNewInstance.
currentInstance
This property is provided by hooks that perform a partial change of a single instance. It contains the affected model instance, you should treat the value as read only (immutable).
Checking for support of ctx.isNewInstance
The initial implementation of ctx.isNewInstance
included only support for memory, MongoDB, and MySQL connectors.
You can check whether your connector supports this feature by testing the value returned in “after save” hook.
For example:
MyModel.observe('after save', function(ctx, next) {
console.log('supports isNewInstance?', ctx.isNewInstance !== undefined);
next();
});
// It's important to provide a value for the id property
// Include also values for any required properties
MyModel.updateOrCreate({
id: 123
}, console.log);
Please report a GitHub issue in the connector project if the feature is not supported.
Accessing the affected instance
Operations affecting a single instance only (all create, retrieve, update,
and delete operations except PersistedModel.deleteAll
and PersistedModel.updateAll
) usually provide the affected instance in the context object.
However, depending on the operation, this instance is provided either as modifiable ctx.instance
or as read-only ctx.currentInstance:
Method | before save | persist | after save | before delete | after delete |
---|---|---|---|---|---|
create |
ctx.instance |
ctx |
ctx.instance |
--- | --- |
findOrCreate |
ctx.instance |
ctx |
ctx.instance |
--- | --- |
updateOrCreate |
n/a* | ctx |
ctx.instance |
--- | --- |
upsertWithWhere |
n/a* | ctx |
ctx.instance |
--- | --- |
updateAll |
n/a | n/a | n/a | --- | --- |
prototype.save |
ctx.instance |
ctx |
ctx.instance |
--- | --- |
prototype |
ctx |
ctx |
ctx.instance |
--- | --- |
prototype.delete |
--- | --- | --- | ctx.where.id |
ctx.where.id |
deleteAll |
--- | --- | --- | n/a | n/a |
replaceOrCreate |
ctx.instance |
ctx |
ctx.instance |
--- | --- |
prototype
|
ctx.instance |
ctx |
ctx.instance |
--- | --- |
(*) The operations updateOrCreate
and upsertWithWhere
do not provide an instance in the “before save” hook. Since it’s impossible tell in advance whether the operation will result in UPDATE or CREATE, there is no way to know whether an existing “currentInstance” is affected by the operation.
See the following sections for more details.
Hooks
LoopBack provides the following operation hooks:
The following table lists hooks that PersistedModel methods invoke.
Method name | Hooks invoked |
---|---|
all |
access, loaded |
create | before save, after save, loaded, persist |
upsert (aka updateOrCreate) | access, before save, after save, loaded, persist |
upsertWithWhere | access, before save, after save, loaded, persist |
findOrCreate | access, before save*, after save*, loaded, persist |
deleteAll (destroyAll) deleteById (destroyById) |
access, before delete, after delete |
updateAll | access, before save, after save, persist |
prototype.save | before save, after save, persist, loaded |
prototype.delete | before delete, after delete |
prototype. updateAttributes |
before save, after save, loaded, persist |
replaceOrCreate | access, before save, after save, loaded, persist |
prototype. replaceAttributes replaceById |
before save, after save, loaded, persist |
(*) When findOrCreate
finds an existing model, the save hooks are not triggered.
However, connectors providing atomic implementation may trigger before save
hook even when the model is not created,
since they cannot determine in advance whether the model will be created or not.
access
The access
hook is triggered whenever a database is queried for models, that is when any create, retrieve, update,
and delete method of PersistedModel is called.
Observers may modify the query, for example by adding extra restrictions.
Note:
Prototype methods don’t trigger the access
hook because the hook was already triggered by the method that loaded the model instance from the database.
For example, when you call a prototype method via the REST API, two model calls are made: static findById()
(that triggers the “access” hook) and then the prototype method as requested.
Context properties
Model
- the constructor of the model that will be queriedquery
- the query containing fieldswhere
,include
,order
, etc.
Examples:
MyModel.observe('access', function logQuery(ctx, next) {
console.log('Accessing %s matching %s', ctx.Model.modelName, ctx.query.where);
next();
});
MyModel.observe('access', function limitToTenant(ctx, next) {
ctx.query.where.tenantId = loopback.getCurrentContext().tenantId;
next();
});
before save
The before save
hook is triggered before a model instance is modified (created, updated), specifically when the following methods of PersistedModel are called:
- create()
- upsert()
- upsertWithWhere()
- findOrCreate() - When
findOrCreate
finds an existing model, it does not trigger save hooks. However, connectors providing atomic implementation may triggerbefore save
hook even when the model is not created, since they cannot determine in advance whether the model will be created or not. - updateAll()
- prototype.save()
- prototype.updateAttributes()
- replaceOrCreate()
- prototype.replaceById() / replaceAttributes()
The hook is triggered before model validation functions are called.
Tip:
Since the before save
hook is triggered before validators are called, you can use it to ensure that empty or missing values are filled with default values.
Depending on which method triggered this hook, the context will have one of the following sets of properties:
- Full save of a single model
Model
- the constructor of the model that will be savedinstance
- the model instance to be saved. The value is an instance ofModel
class.
- Partial update of possibly multiple models
Model
- the constructor of the model that will be savedwhere
- the where filter describing which instances will be affecteddata
- the (partial) data to apply during the updatecurrentInstance
- the affected instance.
ctx.isNewInstance
The before save hook provides the ctx.isNewInstance
property when ctx.instance
is set, with the following values:
- True for all CREATE operations.
- False for all UPDATE and REPLACE operations.
- Undefined for
updateOrCreate
,upsertWithWhere
,replaceOrCreate
,prototype.save
,prototype.updateAttributes
, andupdateAll
operations.
Embedded relations
You can define a before save
hook for a model that is embedded in another model.
Then, updating or creating an instance of the containing model will trigger the operation hook on the embedded model.
When this occurs, ctx.isNewInstance
is false, because only a new instance of the container model is created.
For example, if Customer embedsOne Address
, and you define a before save
hook on the Address model, creating a new Customer instance will trigger the operation hook.
Manipulating model data in “before save” hook
As explained above, the context provides either an instance
property or a pair of data
and where
properties.
Exposing a full model instance in ctx.instance
allows hooks to call custom model instance methods
(for example , the hook can call order.recalculateShippingAndTaxes()
whenever order data like address was changed).
That’s why LoopBack create, retrieve, update, and delete operations provide the instance if possible.
There are two notable exception when it is not feasible to provide the instance object:
PersistedModel.updateAll
updates multiple instances matching the provided query. LoopBack does not even load their data from the database, it’s up to the database to find these instances and apply necessary changes.PersistedModel.updateAttributes
performs a partial update, only a subset of model properties is modified. While LoopBack has a model instance available, it also needs to know which of model properties should be changed in the database. Passing the operation payload inctx.data
- a plain object containing only those properties which should be modified - makes it easy for hook implementations to add/remove the properties to modify. You can still access the model instance to be modified viactx.currentInstance
as long as you treat it as immutable (read-only).
Examples
MyModel.observe('before save', function updateTimestamp(ctx, next) {
if (ctx.instance) {
ctx.instance.updated = new Date();
} else {
ctx.data.updated = new Date();
}
next();
});
MyModel.observe('before save', function computePercentage(ctx, next) {
if (ctx.instance) {
ctx.instance.percentage = 100 * ctx.instance.part / ctx.instance.total;
} else if (ctx.data.part && ctx.data.total) {
ctx.data.percentage = 100 * ctx.data.part / ctx.data.total;
} else if (ctx.data.part || ctx.data.total) {
// either report an error or fetch the missing properties from DB
}
next();
});
Removing unneeded properties
To remove (unset) a property in a model instance, it is not enough the set its value to undefined and/or delete the property.
One has to call unsetAttribute(name)
instead. However, don’t forget to handle the case where the context has a data property instead!
Since the data object is a plain object, you can remove properties the usual way via delete operator.
Example:
MyModel.observe('before save', function removeUnwantedField(ctx, next) {
if (ctx.instance) {
ctx.instance.unsetAttribute('unwantedField');
} else {
delete ctx.data.unwantedField;
}
next();
});
This completely removes the field and prevents inserting spurious data into the database.
after save
The after save
hook is called after a model change was successfully persisted to the datasource, specifically when the following methods of PersistedModel are called:
- create()
- upsert()
- upsertWithWhere()
- findOrCreate()*
- updateAll()
- prototype.save()
- prototype.updateAttributes()
- prototye.replaceAttributes() / replaceById()
- replaceOrCreate()
(*) When findOrCreate
finds an existing model, the save hooks are not triggered.
However, connectors providing atomic implementation may trigger before save
hook even when the model is not created,
since they cannot determine in advance whether the model will be created or not.
Depending on which method triggered this hook, the context will have one of the following sets of properties:
- A single model was updated:
Model
- the constructor of the model that will be saved.-
instance
- the model instance that was saved. The value is an instance ofModel
class and contains updated values computed by datastore (for example, auto-generated ID).Note:
The after save hook returns the changes made to
ctx.instance
to the caller (REST client), but does not persist them to the database!
</div>
- Partial update of more model instances via
Model.updateAll
:Model
- the constructor of the model that will be saved.where
- the where filter describing which instances were queried. See caveat below.-
data
- the (partial) data applied during the update.NOTE: You cannot reliably use the "where" query in an after save hook to find which models were affected. Consider the following call:
MyModel.updateAll({ color: 'yellow' }, { color: 'red' }, cb);
At the time the "after save" hook is run, no records will match the query
{ color: 'yellow' }
.
The after save
hook provides the ctx.isNewInstance
property whenever ctx.instance
is set, with the following values:
- True after all CREATE operations.
- False after all UPDATE/REPLACE operations.
- The operations
updateOrCreate
,prototype.save
, andprototype.updateAttributes
require connectors to report whether a new instance was created or an existing instance was updated. When the connector provides this information,ctx.isNewInstance
is True or False. When the connector does not support this feature yet (see below), the value is undefined.
Important:
Only certain connectors support ctx.isNewInstance
. With other connectors it is undefined.
See Checking for support of ctx.isNewInstance.
Embedded relations
You can define an after save
hook for a model that is embedded in another model.
Then, updating or creating an instance of the containing model will trigger the operation hook on the embedded model.
When this occurs, ctx.isNewInstance
is false, because only a new instance of the container model is created.
For example, if Customer embedsOne Address
, and you define an after save
hook on the Address model,
creating a new Customer instance will trigger the operation hook.
Examples
MyModel.observe('after save', function(ctx, next) {
if (ctx.instance) {
console.log('Saved %s#%s', ctx.Model.modelName, ctx.instance.id);
} else {
console.log('Updated %s matching %j',
ctx.Model.pluralModelName,
ctx.where);
}
next();
});
before delete
The before delete
hook is triggered before a model is removed from a datasource, specifically when the following methods of PersistedModel are called:
destroyAll()
(same asdeleteAll()
)destroyById()
(same asdeleteById()
)prototype.destroy()
(same asprototype.delete()
)
Important:
The before delete
operation hook does not receive a list of deleted model instance IDs, because backend data stores such as relational or NoSQL databases don’t provide this information.
However, when deleting a single model instance hook receives ctx.where
that contains the id
of the instance being deleted.
Context properties
Model
- the constructor of the model that will be queriedwhere
- the where filter describing which instances will be deleted.
Example:
MyModel.observe('before delete', function(ctx, next) {
console.log('Going to delete %s matching %j',
ctx.Model.pluralModelName,
ctx.where);
next();
});
To reject the deletion of a model based on some condition, call next()
with an error to abort the delete operation.
For example:
if (subscriptions.length > 0) {
//Stop the deletion of this Client
var err = new Error("Client has an active subscription, cannot delete");
err.statusCode = 400;
console.log(err.toString());
next(err);
} else {
next();
}
after delete
Important:
The after delete
operation hooks do not receive a list of deleted model instance IDs, because backend data stores such as relational or NoSQL databases don’t provide this information.
However, when deleting a single model instance hook receives ctx.where
that contains the id
of the instance being deleted.
The after delete
hook is triggered after some models are removed from the datasource, specifically when the following methods of PersistedModel are called:
destroyAll()
(same asdeleteAll()
)destroyById()
(same asdeleteById()
)prototype.destroy()
(same asprototype.delete()
)
Context properties
Model
- the constructor of the model that will be queriedwhere
- the where filter describing which instances were deleted.
Example:
MyModel.observe('after delete', function(ctx, next) {
console.log('Deleted %s matching %j',
ctx.Model.pluralModelName,
ctx.where);
next();
});
loaded
This hook is triggered by the following methods of PersistedModel:
find()
findOne()
findById()
exists()
count()
create()
upsert()
(same asupdateOrCreate()
)upsertWithWhere()
findOrCreate()
*prototype.save()
prototype.updateAttributes()
replaceOrCreate()
prototype.replaceAttributes()
/replaceById()
Important:
By default, create
and updateAttributes
do not apply database updates to the model instance returned to the callback,
therefore any changes made by “loaded” hooks are discarded. To change this behavior, set a per-model option updateOnLoad: true
.
LoopBack invokes this hook after the connector fetches data, but before creating a model instance from that data. This enables hooks to decrypt data (for example). NOTE: This hook is called with the raw database data, not a full model instance.
Context properties
data
- the data returned by the connector (loaded from the database)
persist
This hook is triggered by operations that persist data to the datasource, specifically, the following methods of PersistedModel:
create()
upsert()
(same asupdateOrCreate()
)upsertWithWhere()
findOrCreate()
*prototype.save()
prototype.updateAttributes()
updateAll()
replaceOrCreate()
prototype.replaceAttributes()
/replaceById()
Don’t confuse this hook with the “before save” hook:
- before save – Use this hook to observe (and operate on) model instances that are about to be saved (for example, when the country code is set and the country name not, fill in the country name).
- persist – Use this hook to observe (and operate on) data just before it is going to be persisted into a data source (for example, encrypt the values in the database).
During create
the updates applied through persist
hook are reflected into the database,
but the same updates are NOT reflected in the instance
object obtained in callback of create
.
Secondly, for connectors implementing atomic findOrCreate
, a new instance of the object is created every time,
even if an existing record is later found in the database. So:
- Both
ctx.data.id
andctx.currentInstance.id
are set to new ID. ctx.isNewInstance
istrue
Context properties
data
- the data that will be sent to the connector (saved to the database)currentInstance
- the affected model instanceisNewInstance
- see below.
For this hook, ctx.isNewInstance
is:
- True for all CREATE operations
- False for all UPDATE operations
- Undefined for updateOrCreate, , upsertWithWhere, replaceOrCreate, prototype.save, prototype.updateAttributes, and updateAll operations.
afterInitialize hook
Important:
afterInitialize
is not strictly an operation hook. It is actually the only model hook that is not deprecated.
It is a synchronous method and does not take a callback function: You do not need to call next()
after performing your logic in the hook.
This hook is called after a model is initialized.
Example
/common/models/coffee-shop.js
...
CoffeeShop.afterInitialize = function() {
//your logic goes here
};
...
Most operations require initializing a model before actually performing an action, but there are a few cases where the initialize event is not triggered,
such as HTTP requests to the exists
, count
, or bulk update REST endpoints.
Migrating from model hooks
The following table shows which new hook to use for each of the deprecated model hooks:
Model hook | Operation hook to use instead |
---|---|
beforeValidate | before save |
afterValidate | persist |
beforeCreate | before save |
afterCreate | after save |
beforeSave | before save |
afterSave | after save |
beforeUpdate | before save |
afterUpdate | after save |
beforeDestroy | before delete |
afterDestroy | after delete |