Example of Sequelize Associations in FeathersJS

I've been playing around with FeathersJS and one thing that has been hard to find is a concrete example of setting up Sequelize with related tables.

Here's an example of the users table referencing a user_statuses table using the new model.associate() syntax including populating the output of the users.find() endpoint with a hook.

Model setup

The user's model here is the basic file generated by @feathersjs/authentication with just the foreign key added and the relationship defined.

# src/models/user.model.js
# file generated using @feathers/cli

const Sequelize = require('sequelize');  
const DataTypes = Sequelize.DataTypes;

module.exports = function(app){  
    const sequelizeClient = app.get('sequelizeClient');
    const users = sequelizeClient.define('users', {
        email: {
            type: DataTypes.STRING,
            allowNull: false,
            unique: true
        },
        password: {
            type: DataTypes.STRING,
            allowNull: false
        },
        statusId: {
            type: Sequelize.INTEGER,
            field: 'status_id'
        }

    }, {
        hooks: {
            beforeCount(options){
                options.raw = true;
            }
        },
    });

    users.associate = function(models){
        users.hasOne(models.userStatuses, {
            as: 'UserStatus',
            foreignKey: 'id'
        });
    };

    return users;
};

UserStatuses model - just the generated file. No changes really needed for this one way relationship.

# src/models/user-statuses.model.js
# file generated using @feathers/cli

const Sequelize = require('sequelize');  
const DataTypes = Sequelize.DataTypes;

module.exports = function(app){  
    const sequelizeClient = app.get('sequelizeClient');
    const userStatuses = sequelizeClient.define('userStatuses', {
        name: {
            type: DataTypes.STRING
        }
    }, {
        hooks: {
            beforeCount(options){
                options.raw = true;
            }
        },
    });

    userStatuses.associate = function(models){
    };

    return userStatuses;
};

Populating the Relationship

The goal is to return the related table as part of a users.find() call. As a bare minimum you want to set two Sequelize params in a before hook: include (see Sequelize documentation for more details on this option) and raw:true (if you want a nested object rather than a flat structure).

# src/services/users/users.hooks.js
# file generated by @feathers/authentication

const {authenticate} = require('@feathersjs/authentication').hooks;  
const {  
    hashPassword, protect
} = require('@feathersjs/authentication-local').hooks;

module.exports = {  
    before: {
        all: [],
        find: [
                        authenticate('jwt'),
                        //
                        //      Quick & dirty example 
                        //
                        context => {
                                const sequelize = context.params.sequelize || {};
                                sequelize.raw = true;
                                sequelize.include = [
                        {
                            model: context.app.services['user-statuses'].Model,
                            as: 'UserStatus'
                        }
                    ];
                                return context;
                },
                ],
        get: [authenticate('jwt')],
        create: [hashPassword()],
        update: [hashPassword(), authenticate('jwt')],
        patch: [hashPassword(), authenticate('jwt')],
        remove: [authenticate('jwt')]
    },

    after: {
        all: [
            // Make sure the password field is never sent to the client
            // Always must be the last hook
            protect('password')
        ],
        find: [],
        get: [],
        create: [],
        update: [],
        patch: [],
        remove: []
    },

    error: {
        all: [],
        find: [],
        get: [],
        create: [],
        update: [],
        patch: [],
        remove: []
    }
};

As various models might make use of this functionality I've actually created a generic hook addAssociations(). It takes multiple models for the include option and looks up the model object for you based on a shorthand string. (I'm still looking at a good way of handling hyphens vs camelcase so excuse the mix of names in the example below)

# src/hooks/add-associations.js
# hook generated by @feathers/cli

module.exports = function(options = {}){  
    options.models = options.models || [];

    return async context =>{
        const sequelize = context.params.sequelize || {};
        const include = sequelize.include || [];

        //  Reasign in case we created these properties
        sequelize.include = include.concat(options.models.map(model => {
            const newModel = {...model};

            newModel.model = context.app.services[model.model].Model;
            return newModel;
        }));

        //  Nested output
        sequelize.raw = false;

        context.params.sequelize = sequelize;
        return context;
    };
};

And here's the updated users.hooks.js file:

# src/services/users/users.hooks.js
# file generated by @feathers/authentication

const {authenticate} = require('@feathersjs/authentication').hooks;  
const addAssociations = require('./../../hooks/add-associations');

const {  
    hashPassword, protect
} = require('@feathersjs/authentication-local').hooks;

module.exports = {  
    before: {
        all: [],
        find: [
            authenticate('jwt'),
            addAssociations({
                models: [
                    {
                        model: 'user-statuses',
                        as: 'UserStatus'
                    }
                ]
            })
        ],
        get: [authenticate('jwt')],
        create: [hashPassword()],
        update: [hashPassword(), authenticate('jwt')],
        patch: [hashPassword(), authenticate('jwt')],
        remove: [authenticate('jwt')]
    },

    after: {
        all: [
            // Make sure the password field is never sent to the client
            // Always must be the last hook
            protect('password')
        ],
        find: [],
        get: [],
        create: [],
        update: [],
        patch: [],
        remove: []
    },

    error: {
        all: [],
        find: [],
        get: [],
        create: [],
        update: [],
        patch: [],
        remove: []
    }
};

All this results in a nice nested JSON response:

{
    "total": 1,
    "limit": 10,
    "skip": 0,
    "data": [
        {
            "id": 1,
            "email": "[email protected]",
            "statusId": 1,
            "created_at": "2017-11-08T15:25:01.000Z",
            "updated_at": "2017-11-08T15:27:29.000Z",
            "UserStatus": {
                "id": 1,
                "name": "active",
                "created_at": "2017-11-08T15:25:01.000Z",
                "updated_at": "2017-11-08T15:25:01.000Z"
            }
        }
    ]
}