Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamic default scopes in sequelize.js

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?

like image 459
Mike Chaliy Avatar asked Feb 14 '16 13:02

Mike Chaliy


People also ask

What are scopes in Sequelize?

Scopes are used to help you reuse code. You can define commonly used queries, specifying options such as where , include , limit , etc.

How do I set default value in Sequelize?

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.

What is Sequelize sync ()?

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.

What is raw true in Sequelize?

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.


2 Answers

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
})
like image 150
Richard Ayotte Avatar answered Oct 23 '22 03:10

Richard Ayotte


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});
like image 37
Gergo Avatar answered Oct 23 '22 04:10

Gergo