Skip to content

Add support for hooks#27

Merged
jimlambie merged 99 commits intomasterfrom
feature/hooks
Mar 24, 2016
Merged

Add support for hooks#27
jimlambie merged 99 commits intomasterfrom
feature/hooks

Conversation

@eduardoboucas
Copy link
Contributor

This PR proposes the introduction of hooks — pieces of logic that are executed at specific points when documents are created, updated or deleted.

Overview

In its essence, a hook is simply a function that intercepts a document/query before it's executed, having the option to modify it before returning it back to the model.

A hook is stored as an individual file on a hooks directory (defaulting to /workspace/hooks) and can be used by being attached to a create, update or delete operation in the settings section of the schema definition.

collections.user.json:

"settings": {
  "hooks": {
    "create": ["myhook1", "myhook2"]
  }
}

This means that whenever a new user is created, the document that is about to be inserted will be passed to myhook1, its return value will then be passed on to myhook2 and so on. After all the hooks finish executing, the final document will be returned to the model to be inserted in the database.

The order in which hooks are executed is defined by the order of the items in the array.

Anatomy of a hook

A simple hook can be defined as following:

module.exports = function (doc, type, data) {
    doc.name = 'Modified by the hook';

    return doc;
};

This particular hook will receive a document, change a property (name) and return it back. So if attached to the create event, it will make all the created documents have name set to Modified by the hook.

However, this logic ties the hook to the schema — what happens if we want to modify a property other than name? Hooks are supposed to be able to add functionality to a document, and should be approached as interchangeable building blocks rather than pieces of functionality tightly coupled with a schema.

For that reason, developers might have the need to pass extra information to the hook — e.g. inform the hook the name of the properties that should be modified. As such, in addition to the syntax shown above for declaring a hook (an array of strings), an alternative one allows data to be passed through a options object.

"settings": {
  "hooks": {
    "create": [{
        "hook": "slugify",
        "options": {
          "from": "title",
          "to": "slug"
        }
    }]
  }
}

In this example we implement a hook that populates a field (slug) with a URL-friendly version of another field (title). The hook is created in such a way that the properties it reads from and writes to are dynamic, passed through as from and to inside options. The slugify hook could then be written as follows:

// Example hook: Creates a URL-friendly version (slug) of a field
function slugify(text) {
    return text.toString().toLowerCase()
            .replace(/\s+/g, '-')
            .replace(/[^\w\-]+/g, '')
            .replace(/\-\-+/g, '-')
            .replace(/^-+/, '')
            .replace(/-+$/, '');
}

module.exports = function (obj, type, data) {
    // We use the options object to know what field to use as the source
    // and what field to populate with the slug
    obj[data.options.to] = slugify(obj[data.options.from]);

    return obj;
};

A hook always receives 3 arguments:

  1. obj: The main object, which the hook is able to modify. It must always return it back. It can be a document (on create) or a query (on update or delete);
  2. type: The type of hook. Can be 0 (create), 1 (update) or 2 (delete);
  3. data: Object that passed additional data, such as the options object that may be declared with the hook, or other objects depending on the type of hook (e.g. updatedDocs will be passed if it's a update hook).

Use cases

  • Creating variations of a field, such as creating a slug (example above);
  • Validating fields with complex conditions, when a regular expression might not be enough (depends on 1. in Further considerations below);
  • Converting different types of data to a unique format, such as Unix timestamp;
  • Triggering an action, notification or external command when a record is modified.

Further considerations

  1. Should hooks be allowed to cancel an operation completely? If so, what would the response look like?
  2. Should all actions of a hook be logged? This could help debugging when it's not clear what and who affected a document. This is easily implemented in the Hook prototype.
  3. At the moment, all hooks (create, update and delete) are fired before operations take place, giving developers power to change their course. But it also means that they happen at a point where there's no guarantee the operation will completely, especially because they are fired before validation. Would there be a need for more granular events like beforeCreate and afterCreate, where the former could be used to change the behaviour of an operation and the latter would be used to trigger something when the operation has definitely finished? Is this going too far?

Testing

I have some tests but they still need some love, so I haven't included them in this PR. If you think this feature is worth implementing, I'll polish them up and add to the PR.

@jimlambie @josephdenne what do you think?

@jimlambie jimlambie self-assigned this Mar 2, 2016
jimlambie and others added 12 commits March 21, 2016 19:52
Create connections at database level, not collection level
The default behaviour is to require authentication if there is no model.settings.authenticate block.

To allow unauthenticated requests, use the following in your endpoint:

```
module.exports.model = {
  "settings": {
    "authenticate": false
  }
}
```
This commit also ensures the hook is called for an array of documents passed to model.create()
@jimlambie
Copy link
Contributor

@eduardoboucas I believe we can close this PR, as everything here is covered by #42. What do you think?

@eduardoboucas
Copy link
Contributor Author

@jimlambie #42 is actually trying to merge the new hooks into this branch (feature/hooks) and not master, so if it's alright with you I'll merge it and we'll carry on from here.

@eduardoboucas
Copy link
Contributor Author

@jimlambie This commit fixes the bug with Model.create() that makes calls with multiple documents not being stored/indexed properly. It now makes sure obj is always an array (even if it contains only one document). It also refactors the call to the afterCreate hook accordingly.

Let me know what you think.

@eduardoboucas
Copy link
Contributor Author

  • Added fail-safe to hooks so it doesn't fail if no hooks property exists in the config
  • Added missing dev dependency (mkdirp)

Eduardo Boucas and others added 2 commits March 23, 2016 17:23
@jimlambie jimlambie merged commit 0cf1db9 into master Mar 24, 2016
@eduardoboucas
Copy link
Contributor Author

🎉

@jimlambie jimlambie deleted the feature/hooks branch December 22, 2016 13:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants