Operation hooks are triggered by all methods that execute a particular high-level create, read, update, or delete operation.
Page Contents

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, createsave, 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
access X X X   X X X X      
before save       X X X   X X   X
after save       X X X   X X   X
before delete             X     X  
after delete             X     X  
loaded X X X X X X     X   X
persist       X X X   X X   X

Using async/await

Operation hooks can also return a promise instead of calling the next parameter.

/common/models/MyModel.js

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

The ctx.hookState property is preserved across all hooks invoked for a single operation.

For example, both “access”, “before save” and “after save” invoked for Model.create() have the same object passed in ctx.hookState.

This way the hooks can pass state date between “before” and “after” hook.

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 instance (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.

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:

Operation before save persist after save before delete after delete
create ctx.instance ctx.currentInstance ctx.instance --- ---
findOrCreate ctx.instance ctx.currentInstance ctx.instance --- ---
updateOrCreate n/a* ctx.currentInstance ctx.instance --- ---
updateAll n/a n/a n/a --- ---
prototype.save ctx.instance ctx.currentInstance ctx.instance --- ---
prototype.updateAttributes ctx.currentInstance ctx.currentInstance ctx.instance --- ---

prototype.delete

--- --- --- ctx.where.id ctx.where.id
deleteAll --- --- --- n/a n/a

(*) The operation updateOrCreate does not provide any instance in the “before save” hook. Because we cannot tell in advance whether the operation will result in UPDATE or CREATE, we cannot tell whether there is any existing “currentInstance” 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
find
findOne 
findById  
exists
count 

access, loaded
create before save, after save, loaded, persist
upsert (aka updateOrCreate) access, before save, after save, loaded, persist
findOrCreate access, before save*, after save*, loaded, persist
deleteAll (aka destroyAll)
deleteById (aka 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

NOTE: 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.

Context properties

  • Model - the constructor of the model that will be queried
  • query - the query containing fields whereincludeorder, 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:

NOTE: 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.

The hook is triggered before model validation functions are called.

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 saved
    • instance - the model instance to be saved. The value is an instance of Model class.
  • Partial update of possibly multiple models
    • Model - the constructor of the model that will be saved
    • where - the where filter describing which instances will be affected
    • data - the (partial) data to apply during the update
    • currentInstance - the instance being affected, see Triggering with prototype.updateAttributes below.

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 operations
  • Undefined for updateOrCreateprototype.save,  prototype.updateAttributes, and updateAll 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:

  1. 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. 
  2. 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 in ctx.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 via ctx.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:

NOTE: 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 of Model class and contains updated values computed by datastore (for example, auto-generated ID).
  • 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. 

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 operations.
  • The operations updateOrCreateprototype.save, and prototype.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.

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 embedding model is created.

For example, if Customer embedsOne Address, and you define a 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:

Context properties

  • Model - the constructor of the model that will be queried
  • where - 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

The after delete hook is triggered after some models are removed from the datasource, specifically when the following methods of PersistedModel are called:

Context properties

  • Model - the constructor of the model that will be queried
  • where - 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:

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:

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:

Context properties

  • data - the data that will be sent to the connector (saved to the database)
  • currentInstance - the affected model instance
  • isNewInstance - see below.

For this hook, ctx.isNewInstance is:

  • True for all CREATE operations
  • False for all UPDATE operations
  • Undefined for updateOrCreate, prototype.save, prototype.updateAttributes, and updateAll operations.

afterInitialize 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 existscount, or bulk update REST endpoints.

Migration guide

The following table shows which new hook to use for each of the old 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