Sequelize use upsert function with custom where clause - node.js

I use Sequelize to communicate with my postgres database. I want to use the upsert functionality, but with custom where clause. For example I have this model for volunteers location
module.exports = function (sequelize, DataTypes) {
var volunteer_location = sequelize.define('volunteer_location', {
id: {
type: Sequelize.INTEGER,
allowNull: false,
autoIncrement: true,
primaryKey: true
},
deviceId: {
allowNull: false,
type: Sequelize.STRING
},
latitude: {
allowNull: false,
type: Sequelize.DECIMAL(18, 14)
},
longitude: {
allowNull: false,
type: Sequelize.DECIMAL(18, 14)
},
city: {
allowNull: false,
type: Sequelize.STRING(90)
},
state: {
type: Sequelize.STRING(90)
},
postalCode: {
type: Sequelize.STRING(16)
},
country: {
allowNull: false,
type: Sequelize.STRING(70)
}
}, {
instanceMethods: {
insertOrUpdate: function (requestBody, res) {
volunteer_location.upsert(requestBody).then(function () {
res.sendStatus(200);
});
}
}
});
return volunteer_location;
};
In the insertOrUpdate method I try to use the upsert by giving a json like
location = {
deviceId: req.body.deviceId,
latitude: req.body.latitude,
longitude: req.body.longitude,
city: req.body.city,
state: req.body.state,
postalCode: req.body.postalCode,
country: req.body.country,
volunteer: req.decoded.id
};
where volunteer is a foreign key from table users.
Now the executing says:
CREATE OR REPLACE FUNCTION pg_temp.sequelize_upsert() RETURNS integer
AS $func$ BEGIN INSERT INTO "volunteer_locations" some values RETURN
1; EXCEPTION WHEN unique_violation THEN UPDATE "volunteer_locations"
SET some values WHERE (("id" IS NULL AND "volunteer" = 1));
RETURN 2; END; $func$ LANGUAGE plpgsql; SELECT * FROM
pg_temp.sequelize_upsert();
id and volunteer is a set of primary keys. I want to change the WHERE clause on the UPDATE by checking only the volunteer value, because I don't know the value of the id.
Is this possible? Or I have to use
findOne(...).then(function () {
//update
});
EDIT
I set volunteer with the attribute unique: true and it replaced the where clause with WHERE (("id" IS NULL AND "volunteer" = 1) OR "volunteer" = 1);, which means that the update works. But I don't know if this is very efficient. If anyone knows a better solution please let me know.

Related

Sequelize validation with increment function

There's a sequelize function that allows to increment a value by a number, that number can be negative to decrement, I would like to make sure that that column is always positive or 0, so I added a validator to my model:
module.exports = function (sequelize, DataTypes) {
var ProductVariant = sequelize.define('ProductVariant', {
id: {
type: DataTypes.INTEGER(11),
allowNull: false,
primaryKey: true,
autoIncrement: true
},
ProductId: {
type: DataTypes.STRING(45),
field: 'product_id',
allowNull: false,
primaryKey: true,
references: {
model: 'product',
key: 'id'
}
},
image: {
type: DataTypes.INTEGER(11),
allowNull: true
},
price: {
type: DataTypes.DECIMAL(6, 2),
allowNull: false
},
qty_stock: {
type: DataTypes.INTEGER(11),
allowNull: false,
defaultValue: 0
},
createdAt: {
type: 'TIMESTAMP',
allowNull: true,
field: 'creation_date'
},
updatedAt: {
type: 'TIMESTAMP',
field: 'last_updated',
allowNull: true
},
},
{
tableName: 'variants',
hooks: {
beforeValidate: function(variant, options) {
if (variant.qty_assigned < 0) {
throw new Error('Not valid');
}
}
}
});
ProductVariant.associate = function (models) {
ProductVariant.belongsTo(models.Product)
},
{
indexes:
[{
unique: true,
fields: ['product_id']
}]
}
return ProductVariant;
};
But when I do:
await ProductVariant.increment({ qty_assigned: -100 }, {
where: { id: { [Op.eq]: variantId } },
transaction: t
});
I works and updates the value without checking validation. I also tried with:
qty_stock: {
type: DataTypes.INTEGER(11),
allowNull: false,
defaultValue: 0,
validate: {
min: 0
}
}
Is it possible that is has to do with the fact that increment is done in database? can I do something to add a validation?
Validation is done against an instance (in your app) and not against the DB.
The increment API is special as it updates the value directly in the database and not on the instance
From https://sequelize.org/master/class/lib/model.js~Model.html#static-method-increment (emphasis mine)
Increment the value of one or more columns. This is done in the database, which means it does not use the values currently stored on the Instance. The increment is done using a SET column = column + X WHERE foo = 'bar' query. To get the correct value after an increment into the Instance you should do a reload.
If you want to increment and get validation done, you can:
use a transaction (or optimistic locking) and update manually increment the value of the instance (use sequelize validation)
add a check in the DB (mysql8+: https://dev.mysql.com/doc/refman/8.0/en/create-table-check-constraints.html).

Sequelize error enum already exists after db reset

Hope you're well !
I have a question. How can I prevent the following error after a db reset (sync force true) please ?
SequelizeDatabaseError: type "enum_coverLists_supportingDocument"
already exists
Here is what my model look like :
export const SUPPORTING_DOCUMENT = {
MEDIATION_FEES: 'MEDIATION_FEES',
LEGAL_COUNSEL_FEES: 'LEGAL_COUNSEL_FEES',
FILING_COMPLAINT: 'FILING_COMPLAINT'
};
export const TYPE = {
BUDGET: 'BUDGET',
QUANTITY: 'QUANTITY',
UNLIMITED: 'UNLIMITED'
};
const coverList = function (sequelize, Sequelize) {
const coverList = sequelize.define('coverList',
{
id: {
primaryKey: true,
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4
},
title: {
type: Sequelize.STRING,
allowNull: false
},
description: {
type: Sequelize.STRING,
allowNull: true
},
supportingDocument: {
type: Sequelize.ARRAY(Sequelize.ENUM({
values: [...Object.values(SUPPORTING_DOCUMENT)]
})),
validate: {
isIn: [...Object.values(SUPPORTING_DOCUMENT)],
},
allowNull: true
},
type: {
type: Sequelize.ENUM,
values: Object.values(TYPE),
validate: {
isIn: [Object.values(TYPE)],
},
allowNull: false
}
});
return coverList;
};
export default coverList;
Stack :
Node.js
PostgreSQL
Sequelize
Thanks in advance for your help.
So, Whenever you create an "enum" it will store in the database. if you are using pgAdmin it stores all enums in the "Types" folder and if you use dbeaver there it is stored in "dataTypes" folder. so from there, you can delete it...
you can see here...

How to access the data from mapping table in graphql using sequelize

i am new to sequelize, i have a user table , address table and address type table as given below.
A user can have 2 a different address , permanent and current address, and the type of address (permanent or current ) is specified in the table address type.
I have tried to access the data from mapping table (address_type) in the resolver based on schema and set hasMany relation from user -> address table , but graphql shows association not found error.
How can we get the relation properly in order to get the mapping address type name.
type User{
id:Int
name:String
}
type Address {
id: ID!
user_id:Int
city: String
addr_type:AddressType
}
type AddressType{
id : Int
name:String (permanent|current)
}
table definition
module.exports = function(sequelize, DataTypes) {
return sequelize.define('user', {
id: {
type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true
},
name: {
type: DataTypes.INTEGER, allowNull: false,
},
}, {
tableName: 'user',
timestamps: false
});
};
module.exports = function(sequelize, DataTypes) {
return sequelize.define('address', {
id: {
type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true
},
user_id: {
type: DataTypes.INTEGER, allowNull: false, field:"addr_type"
},
addr_type: {
type: DataTypes.INTEGER, allowNull: false, field:"addr_type"
},
city: {
type: DataTypes.STRING, allowNull: false,
},
}, {
tableName: 'address',
timestamps: false
});
};
module.exports = function(sequelize, DataTypes) {
return sequelize.define('address_types', {
id: {
type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true
},
name: {
type: DataTypes.STRING, allowNull: false,
},
}, {
tableName: 'address_type',
timestamps: false
});
};
relationship
db.user.hasMany(db.address,{foreignKey: 'user_id'});
db.address.belongsTo(db.user,{foreignKey: 'user_id'});
db.address.belongsTo(db.address_types,{foreignKey: 'addr_type'});
resolver code
userts: async (obj, args, context, info ) => User.findAll( {
where: { user_status: 1 },
,
raw: true,
nest: true,
} ).then(userts => {
const response = userts.map(usert => {
return{
// i have 15 fields for a user, if i can access the schema of the corresponsing resolver i can dynamically build the response out put
id: usert.id,
firstName: usert.firstName,
lastName: usert.lastName,
middleName: usert.middleName,
}
})
return response;
}),
You should turn off the option raw in order to get associated objects and use the include option to indicate what associated models you wish to load.
User.findAll( {
where: { user_status: 1 },
include: [{
model: Address,
include: AddressType
}],
raw: false,
nest: true,
}

Sequelize: 'findAll' in instance method getParticipants()?

Specification for my conference instance method:
getParticipants() : Promise -> Participant array
Conference model:
return sequelize.define('conference', {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true
},
name: {
type: Sequelize.STRING,
allowNull: false,
unique: true
},
maxParticipants: {
type: Sequelize.INTEGER,
allowNull: false
},
fileShareSession: {
type: Sequelize.STRING,
defaultValue: null,
allowNull: true
},
startDate: {
type: Sequelize.DATE,
defaultValue: null,
allowNull: true
},
endDate: {
type: Sequelize.DATE,
defaultValue: null,
allowNull: true
},
state: {
type: Sequelize.ENUM(
ConferenceState.new,
ConferenceState.starting,
..
),
defaultValue: ConferenceState.new,
required: true,
allowNull: false
}
Participant model:
return sequelize.define('participant', {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true
},
displayName: {
type: Sequelize.STRING,
defaultValue: null,
allowNull: true
},
mediaResourceId: {
type: Sequelize.STRING,
defaultValue: null,
allowNull: true
},
screenSharingId: {
type: Sequelize.STRING,
defaultValue: null,
allowNull: true
},
mediaType: {
type: Sequelize.ENUM(
MediaType.AUDIO_VIDEO),
defaultValue: MediaType.AUDIO_VIDEO,
allowNull: false
},
state: {
type: Sequelize.ENUM(
ParticipantState.new,
ParticipantState.joining,
..
),
defaultValue: ParticipantState.new,
required: true,
allowNull: false
}
Question:
So can I do a participant.findAll in my conferencing instance model or not? When yes, do I get an Array back with a findAll?
I would have done it like that:
// getParticipants() : Promise -> Participant array
getParticipants() {
return new Promise((resolve, reject) => {
var Participant = sequelize.models.participant;
Participant.findAll({
where: {
id: id
}
}).then(function(participant) {
if (_.isObject(participant)) {
resolve(participant);
} else {
throw new ResourceNotFound(conference.name, {id: id});
}
}).catch(function(err) {
reject(err);
});
});
},
LAZY loading is implemented by sequelize when you make relationships between tables. You could make a relationship as follows:
var Conference = sequelize.define('conference', { ... });
var Participant = sequelize.define('participant', { ... });
Conference.belongsToMany(Participant, { through: 'ConferenceParticipants'});
Participant.belongsToMany(Conference, { through: 'ConferenceParticipants'});
Then you can implement EAGER loading when you query your database like:
// Obtain the participant list included in the original object (EAGER)
var conference =
Conference.findOne({
attributes: ['field1', 'field2', ...],
where: {title: 'Conference A'},
includes: [{
attributes: ['field1', 'field2', ...],
model: Participant,
through: { model: 'ConferenceParticipants'} // You have to name the join table
}]
})
.then(function(conference) {
// Here you will have the conference with the list of participants
});
If you want to use LAZY loading, sequelize implement it for you, you just need to call below methods:
// Obtain the participant LAZY
conference.getParticipants().then(function(participants) {
// participants is an array of participant
})
// You can also pass filters to the getter method.
// They are equal to the options you can pass to a usual finder method.
conference.getParticipants({ where: 'id > 10' }).then(function(participants) {
// participants with an id greater than 10 :)
})
// You can also only retrieve certain fields of a associated object.
conference.getParticipants({attributes: ['title']}).then(function(participants) {
// retrieve participants with the attributes "title" and "id"
})
You can get a reference to sequelize relationship implementation in next document.

Sequelize create through association

I'm working on a create method for an association between two classes. The sequelize documentation indicates that this can be done in one step using includes
IntramuralAthlete.create(intramuralAthlete,{
include: [Person]
}).then((data,err)=>{
if(data)res.json(data);
else res.status(422).json(err);
}).catch(function(error) {
res.status(422).json({message: "failed to create athlete", error: error.message});
});
My model association looks like this
var Person = require('../models').person;
var IntramuralAthlete = require('../models').intramuralAthlete;
IntramuralAthlete.belongsTo(Person);
And the value of intramural athlete when I log it is
{
person:
{ firstName: 'Test',
lastName: 'User',
email: 'test#user.com'
},
grade: '12th',
organizationId: 1
}
But I get the error notNull Violation: personId cannot be null. This error makes it sound like something is wrong with the way I'm indicating to Sequelize that I'm intending to create the personId in that same call.
Is there something wrong in the way I indicate to the create statement what associated tables to create with the IntramuralAthlete?
Thanks!
EDIT:
I have also tried with the following structure with the same result
{
Person: {
firstName: 'Test',
lastName: 'User',
email: 'test#user.com'
},
grade: '12th',
organizationId: 1
}
My model is as follows:
module.exports = function(sequelize, DataTypes) {
return sequelize.define('intramuralAthlete', {
id: {
type: DataTypes.INTEGER(11),
allowNull: false,
primaryKey: true,
autoIncrement: true
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: sequelize.literal('CURRENT_TIMESTAMP')
},
grade: {
type: DataTypes.STRING,
allowNull: true
},
age: {
type: DataTypes.INTEGER(11),
allowNull: true
},
school: {
type: DataTypes.STRING,
allowNull: true
},
notes: {
type: DataTypes.STRING,
allowNull: true
},
guardianId: {
type: DataTypes.INTEGER(11),
allowNull: true,
references: {
model: 'contact',
key: 'id'
}
},
personId: {
type: DataTypes.INTEGER(11),
allowNull: false,
references: {
model: 'person',
key: 'id'
}
},
mobileAthleteId: {
type: DataTypes.INTEGER(11),
allowNull: true,
references: {
model: 'mobileAthlete',
key: 'id'
}
},
organizationId: {
type: DataTypes.INTEGER(11),
allowNull: false,
references: {
model: 'organization',
key: 'id'
}
}
}, {
tableName: 'intramuralAthlete'
});
};
I suppose that your models are named Person and IntramuralAthlete (first arguments of sequelize.define method). In this case, when you create an association like yours, and do not define the as attribute, your create data object should look as follows
{
Person: {
firstName: 'Test',
lastName: 'User',
email: 'test#user.com'
},
grade: '12th',
organizationId: 1
}
If you want to use person instead (just as in your code), you should define the association a little bit differently
IntramuralAthlete.belongsTo(Person, { as: 'person' });
Then, you would have to perform some changes in the create query in the include attribute of the options like this
IntramuralAthlete.create(data, {
include: [
{ model: Person, as: 'person' }
]
}).then((result) => {
// both instances should be created now
});
EDIT: Trick the save() method with empty value of personId
You can maintain the allowNull: false if you do something like that
{
person: {
// person data
},
personId: '', // whatever value - empty string, empty object etc.
grade: '12th',
organizationId: 1
}
EDIT 2: Disable validation when creating.
This case assumes that the validation is turned off. It seems like a bad idea to omit model validation, however there still maintains the database table level validation - defined in migrations, where it can still check if personId value was set
IntramuralAthlete.create(data, {
include: [
{ model: Person, as: 'person' }
],
validate: false
}).then((result) => {
// both instances should be created now
});
In this case the data object can be as in your example - without the personId attribute. We omit the model level validation which allows to pass null value, however if during the save() method it would still be null value - database level validation would throw an error.
first of all, when you associatea a model with belongsTo, sequelize will add automatically the target model primary key as a foreign key in the source model. in most of cases you don't need to define it by yourself, so in your case when you define IntramuralAthlete.belongsTo(Person) sequelize adds PersonId as a foreign key in IntramuralAthlete. your IntramuralAthlete model should looks like:
module.exports = function(sequelize, DataTypes) {
return sequelize.define('intramuralAthlete', {
grade: {
type: DataTypes.STRING,
allowNull: true
},
age: {
type: DataTypes.INTEGER(11),
allowNull: true
},
school: {
type: DataTypes.STRING,
allowNull: true
},
notes: {
type: DataTypes.STRING,
allowNull: true
}
});
};
now you can create an intramuralAthlete like your code above. for example:
let data = {
Person: {
firstName: 'Test',
lastName: 'User',
email: 'test#user.com'
},
grade: '12th',
notes: 'test notes'
}
IntramuralAthlete.create(data, {include: [Person]}).then((result) => {
// both instances should be created now
});
be carefull with the model name.
second I suppose that your IntramuralAthlete model has more than one belongsTo association. just you need to define them as the previous one association and sequelize will add their primary keys as foreign keys in the IntramuralAthlete model.
third, when you define a model, sequelize adds automatically an id datafield as a primary key and autoincrement and also adds createdAt and updatedAt datafields with a default CURRENT_TIMESTAMP value, so you don't need to define them in your model

Resources