I am building kind of multitenancy using sequelize.js. Technically I need to filter all queries by predefined column and dynamic value of the current context. General idea was to use defaultScope
to filter out other contexts, something like:
var context = () => { return "some current context id"; }
connection.define('kid', {
firstName: Sequelize.STRING,
photoUrl: Sequelize.STRING,
context: {
type: Sequelize.STRING,
defaultValue: context // this part works, it accepts function
}
}, {
defaultScope: {
where: {
context: context // this does not work, it does not accept function and values is defined only once
}
}
});
However this does not work because defaultScope
is defined on the application start.
What is the right way to do this?
Scopes are used to help you reuse code. You can define commonly used queries, specifying options such as where , include , limit , etc.
When you create a Sequelize model, you can add the default value for your model by adding the defaultValue option to the column(s) definition. The defaultValue option will be used by Sequelize to define default value(s) for your SQL column(s) when you create a table using Sequelize.
The Sequelize instance method sync() is used to synchronize your Sequelize model with your database tables. The synchronization happens at the table level. When your table doesn't exist the sync() method will generate and run a CREATE TABLE statement for you.
According to the doc : If you do not provide other arguments than the SQL, raw will be assumed to the true, and sequelize will not try to do any formatting to the results of the query.
The problem is that Sequelize scopes are defined on the model but you need to apply the scope just before the query because that's when you have context such as the user and role.
Here's a slightly modified copy of the scope merge function from Sequelize which you can use in your hooks such as beforeFind()
// Feel free to write a more fp version; mutations stink.
const {assign, assignWith} = require('lodash')
const applyScope = ({scope, options}) => {
if (!scope) {
throw new Error('Invalid scope.')
}
if (!options) {
throw new Error('Invalid options.')
}
assignWith(options, scope, (objectValue, sourceValue, key) => {
if (key === 'where') {
if (Array.isArray(sourceValue)) {
return sourceValue
}
return assign(objectValue || {}, sourceValue)
}
else if (['attributes', 'include'].indexOf(key) >= 0
&& Array.isArray(objectValue)
&& Array.isArray(sourceValue)
) {
return objectValue.concat(sourceValue)
}
return objectValue ? objectValue : sourceValue
})
}
In your model:
{
hooks: {
beforeFind(options) {
// Mutates options...
applyScope({
scope: this.options.scopes.user(options.user)
, options
})
return options
}
}
, scopes: {
user(user) {
// Set the scope based on user/role.
return {
where: {
id: user.id
}
}
}
}
}
Finally in your query, set an option with the context that you need.
const user = {id: 12, role: 'admin'}
YourModel.findOne({
attributes: [
'id'
]
, where: {
status: 'enabled'
}
, user
})
I'm not sure it will help, but you can override a model default scope anytime.
let defaultScope = {
where: {
context: ""
}
};
defaultScope.where.context = context();
model.addScope('defaultScope',defaultScope,{override: true});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With