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": "name@example.com",
            "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"
            }
        }
    ]
}