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"
}
}
]
}