Sequelize.js - Asynchronous validators - node.js

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!')
}
})
}
}
})

Related

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

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.

Mongoose Subdocument will not update

I am having trouble figuring out if I designed the schema correctly because I am receiving a 500 error when attempting to PATCH changes of the roles property from a profile. (Note: The 500 error just responds with an empty {}, so it isn't really informative)
Below is the profile schema:
var ProfileSchema = new Schema({
name: {
type: String,
required: true
},
roles: [{
application: {
type: Schema.Types.ObjectId,
required: true,
ref: 'Application'
},
role: {
type: String,
required: true,
enum: [ 'admin', 'author', 'publisher' ]
}
}]
});
Each profile has a role for an application, and when I send the request to the controller action 'update', it fails:
profile update controller:
// Updates an existing Profile in the DB
export function update(req, res) {
try {
if (req.body._id) {
delete req.body._id;
}
console.log('ENDPOINT HIT...');
console.log(`REQUEST PARAM ID: ${req.params.id}`);
console.log('REQUEST BODY:');
console.log(req.body);
console.log('ENTIRE REQUEST: ');
return Profile.findByIdAsync(req.params.id)
.then(handleEntityNotFound(res))
.then(saveUpdates(req.body))
.then(respondWithResult(res))
.catch(handleError(res));
} catch(ex) {
console.error('FAILED TO UPDATE PROFILE');
return handleError(res);
}
}
I made sure that the id and body was being sent properly, and I am hitting the end point.
This is an example of the request body JSON:
{
_id: 57e58ad2781fd340563e29ff,
__updated: Thu Oct 27 2016 15:41:12 GMT-0400 (EDT),
__created: Fri Sep 23 2016 16:04:34 GMT-0400 (EDT),
name: 'test',
__v: 11,
roles:[
{ application: 57b70937c4b9fe460a235375,
role: 'admin',
_id: 58125858a36bd76d8111ba16 },
{ application: 581b299f0145b48adf8f57bd,
role: 'publisher',
_id: 581b481898eefb19ed8a73ee }
]
}
When I try to find the Profile by Id, the promise chain goes straight to the catch(handleError(res)); part of the code and shows an empty object in my console.
My handle error function:
function handleError(res, statusCode) {
console.error('HANDLE PROFILE ERROR: ', statusCode);
statusCode = statusCode || 500;
return function(err) {
console.error('PROFILE ERROR:');
console.error(JSON.stringify(err, null, 2));
res.status(statusCode).send(err);
};
}
UPDATE
I am realizing the code is breaking when it hits my saveUpdates function (Note: I am using lodash):
function saveUpdates(updates) {
/// the code is fine here ///
return function(entity) {
/// once it enters in here, is where it begins to break ///
var updated = _.merge(entity, updates);
if(updated.roles.length != updates.roles.length) {
updated.roles = updates.roles;
}
for(var i in updates.roles) {
updated.roles.set(i, updates.roles[i]);
}
return updated.saveAsync()
.then(updated => {
return updated;
});
};
}
Lesson learned: Read Documentation properly.
Since I am using bluebird promises for this application, I forgot to use .spread() within my saveUpdates() callback function.
Solution:
function saveUpdates(updates) {
return function(entity) {
var updated = _.merge(entity, updates);
if(updated.roles.length != updates.roles.length) {
updated.roles = updates.roles;
}
for(var i in updates.roles) {
updated.roles.set(i, updates.roles[i]);
}
return updated.saveAsync()
// use `.spread()` and not `.then()` //
.spread(updated => {
return updated;
});
};
}
I want to thank the following SOA that led to this conclusion: https://stackoverflow.com/a/25799076/5994486
Also, here is the link to the bluebird documentation in case anyone was curious on .spread(): http://bluebirdjs.com/docs/api/spread.html

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?
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

How can I write sails function on to use in Controller?

I have a question on sails js:
How can I write sails function on model To use in Controler? like:
beforeValidation / fn(values, cb)
beforeCreate / fn(values, cb)
afterCreate / fn(newlyInsertedRecord, cb)
If you are actually trying to use one of the lifecycle callbacks, the syntax would look something like this:
var uuid = require('uuid');
// api/models/MyUsers.js
module.exports = {
attributes: {
id: {
type: 'string',
primaryKey: true
}
},
beforeCreate: function(values, callback) {
// 'this' keyword points to the 'MyUsers' collection
// you can modify values that are saved to the database here
values.id = uuid.v4();
callback();
}
}
Otherwise, there are two types of methods you can create on a model:
instance methods
collection methods
Methods placed inside the attributes object will be "instance methods" (available on an instance of the model). i.e.:
// api/models/MyUsers.js
module.exports = {
attributes: {
id: {
type: 'string',
primaryKey: true
},
myInstanceMethod: function (callback) {
// 'this' keyword points to the instance of the model
callback();
}
}
}
this would be used as such:
MyUsers.findOneById(someId).exec(function (err, myUser) {
if (err) {
// handle error
return;
}
myUser.myInstanceMethod(function (err, result) {
if (err) {
// handle error
return;
}
// do something with `result`
});
}
Methods placed outside the attributes object but inside the model definition are "collection methods", i.e.:
// api/models/MyUsers.js
module.exports = {
attributes: {
id: {
type: 'string',
primaryKey: true
}
},
myCollectionMethod: function (callback) {
// 'this' keyword points to the 'MyUsers' collection
callback();
}
}
the collection method would be used like this:
MyUsers.myCollectionMethod(function (err, result) {
if (err) {
// handle error
return;
}
// do something with `result`
});
P.S. the comments about what the 'this' keyword will be are assuming that you use the methods in a normal way, i.e. calling them in the way that I described in my examples. If you call them in a different way (i.e. saving a reference to the method and calling the method via the reference), those comments may not be accurate.

Resources