Dynamic default scopes in sequelize.js - node.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?

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});

Maybe too late here but scopes can take arguments if defined as functions. From documentation Sequelize scope docs if the scope is defined as
scopes: {
accessLevel (value) {
return {
where: {
accessLevel: {
[Op.gte]: value
}
}
}
}
sequelize,
modelName: 'project'
}
you can use it like: Project.scope({ method: ['accessLevel', 19]}).findAll(); where 19 is the dynamic value the scope will use.
As per defaultScope I'm not sure it can be defined as a function

Related

How to use the class-validator conditional validation decorator (#ValidateIf) based on environment variable value

I am trying to use the class-validator #ValidateIf conditional validator decorator based on the value of a environment variable. Let me share the code for better understanding:
// .env file entry
AMOUNT_CHECK_IN_MODE=TEST
In my validator(.dto) file, i have the following code placed
import {
IsNumberString,
Max,
ValidateIf
} from 'class-validator';
export class GtTransactionDto {
otherProperty: string;
constructor() {
this.otherProperty = process.env.AMOUNT_CHECK_IN_MODE;
}
#ValidateIf(o => o.otherProperty === 'TEST')
#Max(1, {
message: 'Amount should not exceed 1',
context: {
code: GtTransactionErrorCode.validate.DestinationAmount
},
})
#ValidateIf(o => o.otherProperty === 'LIVE')
#IsNumberString(
{},
{
message: 'This is not a valid $property number',
context: {
code: GtTransactionErrorCode.validate.DestinationAmount,
},
}
)
#ValidateIf(o => o.otherProperty === 'TEST')
#IsNumberString(
{},
{
message: 'This is not a valid $property number',
context: {
code: GtTransactionErrorCode.validate.DestinationAmount,
},
}
)
destinationAmount!: string;
}
I want to make sure that in the if TEST is set as the value of AMOUNT_CHECK_IN_MODE in .env file, then the validation for max amount and isNumberString should run. However if the value is set to LIVE then only validation for isNumberString should run
Any help would be highly appreciated
You could use Validation groups and set the group based on the environment variable.
From the docs
import { validate, Min, Length } from 'class-validator';
export class User {
#Min(12, {
groups: ['registration'],
})
age: number;
#Length(2, 20, {
groups: ['registration', 'admin'],
})
name: string;
}
let user = new User();
user.age = 10;
user.name = 'Alex';
validate(user, {
groups: ['registration'],
}); // this will not pass validation
validate(user, {
groups: ['admin'],
}); // this will pass validation
validate(user, {
groups: ['registration', 'admin'],
}); // this will not pass validation
validate(user, {
groups: undefined, // the default
}); // this will not pass validation since all properties get validated regardless of their groups
validate(user, {
groups: [],
}); // this will not pass validation, (equivalent to 'groups: undefined', see above)

Sequelize upsert or create without PK

I'm unable to perform any kind of upsert or create within Sequelize (v: 6.9.0, PostGres dialect).
Using out-of-the-box id as PK, with a unique constraint on the name field. I've disabled timestamps because I don't need them, and upsert was complaining about them. I've tried manually defining the PK id, and allowing Sequelize to magically create it. Here's the current definition:
const schema = {
name: {
unique: true,
allowNull: false,
type: DataTypes.STRING,
}
};
class Pet extends Model { }
Pet.define = () => Pet.init(schema, { sequelize }, { timestamps: false });
Pet.buildCreate = (params) => new Promise((resolve, reject) => {
let options = {
defaults: params
, where: {
name: params.name
}
, returning: true
}
Pet.upsert(options)
.then((instance) => {
resolve(instance);
})
.catch(e => {
// message:'Cannot read property 'createdAt' of undefined'
console.log(`ERROR: ${e.message || e}`);
reject(e);
});
});
module.exports = Pet;
Upsert code:
// handled in separate async method, including here for clarity
sequelize.sync();
// later in code, after db sync
Pet.buildCreate({ name: 'Fido' });
In debugging, the options appear correct:
{
defaults: {
name: 'Fido'
},
returning:true,
where: {
name: 'Fido'
}
}
I've also tried findOrCreate and findCreateFind, they all return errors with variations of Cannot convert undefined or null to object.
I've tried including id: null with the params, exact same results.
The only way I've succeeded is by providing PK in the params, but that is clearly not scalable.
How can I upsert a Model instance without providing a PK id in params?
class Pet extends Model { }
//...you might have the id for the pet from other sources..call it petId
const aPet = Pet.findCreateFind({where: {id: petId}});
aPet.attribute1 = 'xyz';
aPet.attribute2 = 42;
aPet.save();

TypeScript, Cannot find name declared inside if else [duplicate]

This question already has answers here:
Javascript set const variable inside of a try block
(8 answers)
Closed 3 years ago.
if (!challengeType) {
const { brawl, fight }: any = await userModel
.findOneAndUpdate(
{ _id: req.userData._id },
{ $inc: { 'fight.remaining': -1 } },
{ 'new': true }
)
} else {
const { brawl, fight }: any = await userModel
.findOneAndUpdate(
{ _id: req.userData._id },
{ $inc: { 'brawl.remaining': -1 } },
{ 'new': true }
)
}
// typescript error here
// "Cannot find name brawl", and "Cannot find name fight"
console.log(brawl, fight)
not sure why typescript cannot find name brawl and fight,
it could be a problem with typescript error handling
in the case of if else statements,
but if script is running, no problem has occurred.
const and let are block scoped so they are not available in a higher scope than the block in which they are declared in. You are trying to access them outside the block they are declared in. You can declare them with let in the higher scope.
let result: any;
if (!challengeType) {
result = await userModel.findOneAndUpdate(
{ _id: req.userData._id },
{ $inc: { 'fight.remaining': -1 } },
{ 'new': true });
} else {
result = await userModel.findOneAndUpdate(
{ _id: req.userData._id },
{ $inc: { 'brawl.remaining': -1 } },
{ 'new': true });
}
// typescript error here
// "Cannot find name brawl", and "Cannot find name fight"
const { brawl, fight } = result;
console.log(brawl, fight);
You may have to fix some TypeScript syntax as I don't really know TypeScript, but you should get the general idea here.
Actually, you can DRY this up a bit and remove a bunch of repeated code.
const queryType: string = challengeType ? 'brawl' : 'fight';
const { brawl, fight }: any = await userModel.findOneAndUpdate(
{ _id: req.userData._id },
{ $inc: { [`${queryType}.remaining`]: -1 } },
{ 'new': true });
console.log(brawl, fight);
not sure why typescript cannot find name brawl and fight
Because you're trying to access those variables outside the scope in which they are declared. The body of an if or else is a separate block scope and both let and const are only available inside that scope (note var is function scoped, not block scoped).
it could be a problem with typescript error handling in the case of if else statements,
No, it's not a typescript problem. It's how the language is designed. It is a feature of the language to limit the scope that variables can be accessed in. Javascript is the same in this regard.

Sequelize 'afterValidate' hook does not change values

I've got a problem with the sequelize hooks. The following code does its job very well when i create a new instance. But when it comes to updating an existent database entry the values of rec.working_time and rec.break_duration are not wirtten back to the database
I can see that the hook is called by adding some console logs and the rec values change as well. But somhow there is no update on theese fields.
If i do the same with an 'beforeUpdate' everything works fine with the same code.
TimeRecording.hook('afterValidate', rec => {
var moment = require('moment');
var models = require('../models');
let workingTime = moment(rec.time_to).diff(rec.time_from, 'minutes', null);
let workingHours = workingTime / 60;
return models.BreakControl.max('break_duration',
{
where: { working_time: { lt: workingHours } }
})
.then(breakControl => {
breakControl = breakControl ? breakControl : 0;
rec.working_time = workingTime - breakControl;
rec.break_duration = breakControl;
})
.catch(err => console.error(err));
});
Here also a quck look on the important attributes in the model:
time_from: {
type: DataTypes.DATE,
allowNull: false,
},
time_to: {
type: DataTypes.DATE,
validate: {
isAfter: function (value, next) {
let self = this;
if (self.time_from > value) {
return next('time_to must be after time_from!');
}
return next();
}
}
},
required_time: {
type: DataTypes.DECIMAL(4, 0)
},
working_time: {
type: DataTypes.DECIMAL(4, 0),
validate: {
min: 0
}
},
break_duration: {
type: DataTypes.DECIMAL(4, 0)
}
So am i missing something, or did i fail to understand some critical concept here?
Thanks for any information on this.

Sequelize.js - Asynchronous validators

Is it possible to have an asynchronous validator using Sequelize.js? I want to check for the existence of an association before saving a model. Something like this:
User = db.define("user", {
name: Sequelize.STRING
},
{
validate:
hasDevice: ->
#getDevices().success (devices) ->
throw Exception if (devices.length < 1)
return
})
# .... Device is just another model
User.hasMany(Device)
Or is there a way to force that check to run synchronously? (not ideal)
you can use asynchronous validations in v2.0.0.
It works like this:
var Model = sequelize.define('Model', {
attr: Sequelize.STRING
}, {
validate: {
hasAssociation: function(next) {
functionThatChecksTheAssociation(function(ok) {
if (ok) {
next()
} else {
next('Ooops. Something is wrong!')
}
})
}
}
})

Resources