Skip to content

Instantly share code, notes, and snippets.

@bajtos
Last active April 18, 2016 09:53
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save bajtos/b8157112452eeaf984c9 to your computer and use it in GitHub Desktop.
Save bajtos/b8157112452eeaf984c9 to your computer and use it in GitHub Desktop.
LoopBack Hooks

THE STATUS IS NO LONGER TRUE, WE HAVE ADDED OPERATION HOOKS SINCE THEN.

SEE http://docs.strongloop.com/display/LB/Operation+hooks


Current status as of 2015-01-15

At the moment, LoopBack provides two kinds of hooks:

Model hooks

Model hooks execute when model methods are called. It is possible to register only one handler for each hook, handlers are inherited. It is not possible to register a handler for all methods (wildcard).

Most of these hooks are async, although there are few sync exceptions. When a hook fails, the execution of a method is aborted.

Model hooks operate at the model level only, they don't have access to any transport-specific objects like HTTP request. These hooks are executed every time a model method is invoked.

List of model hooks:

  • afterInitialize (sync)
  • beforeValidate
  • afterValidate
  • beforeSave
  • afterSave
  • beforeCreate
  • afterCreate
  • beforeUpdate
  • afterUpdate
  • beforeDestroy
  • afterDestroy

Remote hooks (out of scope)

Remote hooks execute before or after a remote method is called. It's possible to register multiple handlers for the same hook. Handlers are not inherited. One can register a single handler function for all methods using a wildcard.

Remote hooks operate on the transport level, they have access to transport-specific objects like HTTP request and response. These hooks are executed only when the method method is invoked via strong-remoting.

CRUD events (out of scope)

There are also few CRUD events used for change-tracking:

  • deletedAll (arg: where object)
  • deleted (arg: model id)
  • changed (arg: model instance)
  • set (arg: model instance)

Ideally, these events should be implemented on top of Model hooks.

Model hooks in action

Item.create({ name: 'first' }, cb)

afterInitialize
  this: model { name: 'first' }
beforeValidate
  this: model { name: 'first' }
  arg1: data { name: 'first', id: undefined }
afterValidate
  this: model { name: 'first' }
  arg1: data undefined
beforeCreate
  this: model { name: 'first' }
  arg1: model { name: 'first' }
beforeSave
  this: model { name: 'first' }
  arg1: model { name: 'first' }
afterSave
  this: model { name: 'first', id: 1 }
  arg1: data undefined
afterCreate
  this: model { name: 'first', id: 1 }
  arg1: data undefined

Item.find(cb)

afterInitialize
  this: model { name: 'first', id: 1 }
afterInitialize
  this: model { name: 'second', id: 2 }

item.save(sb)

beforeValidate
  this: model { name: 'first-updated', id: 1 }
  arg1: data undefined
afterValidate
  this: model { name: 'first-updated', id: 1 }
  arg1: data undefined
beforeSave
  this: model { name: 'first-updated', id: 1 }
  arg1: data { name: 'first-updated', id: 1 }
beforeUpdate
  this: model { name: 'first-updated', id: 1 }
  arg1: data { name: 'first-updated', id: 1 }
afterInitialize
  this: model { name: 'first-updated', id: 1 }
afterUpdate
  this: model { name: 'first-updated', id: 1 }
  arg1: data undefined
afterSave
  this: model { name: 'first-updated', id: 1 }
  arg1: data undefined

The New Hook API

See http://docs.strongloop.com/display/LB/Operation+hooks for the up-to-date documentation.

The text below is an initial draft that is no longer accurate.


The pull request loopbackio/loopback-datasource-juggler#403 introduces a new API for "intent-based" hooks. These hooks are not tied to a particular method (e.g. "find" or "update"). Instead, they are triggered from all methods that execute a particular "intent".

The consumer API is simple, there is a new method Model.observe(name, observer), where the observer is function observer(context, callback).

The contex object is specific to hooks and it does not have any relation to the context object passed to remoting hooks registered via Model.beforeRemote and Model.afterRemote. The context object is not related to the "current context" provided by loopback.getCurrentContext() either.

Observers are inherited by child models and it is possible to register multiple observers for the same hook.

access

The access hook is triggered whenever a database is queried for models. Observers may modify the query, e.g. by adding extra restrictions.

Context properties

  • Model - the constructor of the model that will be queried
  • query - the query containing fields where, 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 hook before save is triggered before a model instance is about to be modified (created, updated). The hook is triggered before the validation.

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

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();
});

after save

The hook after save is called after a model change was successfully persisted to the datasource.

Depending on which method triggered this hook, the context will have one of the following sets of properties.

Single model

  • 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 (e.g. auto-generated id).

Partial update of possibly multiple models

  • Model - the constructor of the model that will be saved
  • where - the where filter describing which instances were queried
  • data - the (partial) data applied during the update

At the moment, this second set is used exclusively by Model.updateAll.

Examples:

MyModel.observe('after save', functon(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 hook before delete is triggered before some models are going to be removed from the datasource.

Context properties

  • Model - the constructor of the model that will be queried
  • where - the where filter describing which instances were queried.

Example

MyModel.observe('before delete', function(ctx, next) {
  console.log('About to delete %s matching %j',
    ctx.Model.pluralModelName,
    ctx.where);
  next();
});

after delete

The hook after delete is triggered after some models were removed from the datasource.

Context properties

  • Model - the constructor of the model that will be queried
  • where - the where filter describing which instances were queried.

Example

MyModel.observe('after delete', function(ctx, next) {
  console.log('Deleted %s matching %j',
    ctx.Model.pluralModelName,
    ctx.where);
  next();
});

Migration guide

The following table shows which new hook to use for each of the existing (old) hooks:

Old hook name New hook to use
beforeValidate before save
beforeCreate before save
afterCreate after save
beforeSave before save
afterSave after save
beforeUpdate before save
afterUpdate after save
beforeDestroy before delete
afterDestroy after delete

The following hook doesn't have any counterpart: afterValidate. If you have a specific use case that cannot be implemented without it, then please open a github issue and describe your requirements there. We will consider adding a replacement for this hook.

List of hooks invoked by PersistedModel methods

Method name Hooks invoked
all access
find access
findOne access
findById access
findByIds access
create before save, after save
updateOrCreate access, before save, after save
findOrCreate access, before save (1), after save (1)
exists access
count access
deleteAll access, before delete, after delete
deleteById access, after delete
updateAll access, before save, after save
prototype.save before save, after save
prototype.delete before delete, after delete
prototype.updateAttributes before save, after save
  • (1) 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 won't created, as they cannot determine in advance whether the model will be created or not.
@ilianaza
Copy link

Is there a way to update other models inside a hook? For example:

Model1.afterUpdate = function(next) {
    var app = Model1.app;
    var Model2 = app.models.Model2;
    Model2.someFunction(next);  // I get "Error: Can't set headers after they are sent."
    next();
  };

What's the recommended approach here?

@yagobski
Copy link

yagobski commented Jun 2, 2015

Can you please update the doc.
I see you have added instance for before and after delete
Ref : Improve instance-level operation hooks #488

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment