sequelize filtering for nested model - node.js

I've been working with sequelize in a couple of weeks, still trying to learn..
My Models are defined in models folder.
/models/endpoint.js
import Sequelize from 'sequelize';
import { sequelize } from '../data';
import Permission from './permission';
const { Model } = Sequelize;
class Endpoint extends Model { }
Endpoint.init({
endpoint: {
type: Sequelize.VIRTUAL(Sequelize.STRING, [ 'method', 'path' ]),
get() {
return `${this.get('method')} ${this.get('path')}`;
}
},
method: {
type: Sequelize.ENUM,
values: [ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH' ],
allowNull: false
},
path: {
type: Sequelize.STRING,
allowNull: false
},
description: {
type: Sequelize.TEXT
}
}, {
sequelize,
modelName: 'endpoint'
});
Endpoint.Permissions = Endpoint.hasMany(Permission);
Permission.Endpoint = Permission.belongsTo(Endpoint, { as: 'permission' });
export default Endpoint;
/models/permission.js
import Sequelize from 'sequelize';
import { sequelize } from '../data';
const { Model } = Sequelize;
class Permission extends Model { }
Permission.init({
entity: {
type: Sequelize.ENUM,
values: [ '*', 'user', 'extension', 'queue', 'group' ],
defaultValue: '*'
},
role: {
type: Sequelize.STRING,
defaultValue: '*'
}
}, {
sequelize,
modelName: 'permission'
});
export default Permission;
found something in docs: Top level where with eagerly loaded models
User.findAll({
where: {
'$Instruments.name$': { [Op.iLike]: '%ooth%' }
},
include: [{
model: Tool,
as: 'Instruments'
}]
})
as following that, I made this
Endpoint.findAndCountAll({
where: {
path: { [Symbol(iLike)]: '%user%' },
'$permissions.role$': { [Symbol(eq)]: 'admin' }
},
include: [
{ model: permission, as: 'permissions' }
],
order: [],
offset: 0,
limit: 20
})
my code throws this error:
SequelizeDatabaseError: missing FROM-clause entry for table
"permissions"
and generated SQL query:
SELECT "endpoint".*,
"permissions"."id" AS "permissions.id",
"permissions"."entity" AS "permissions.entity",
"permissions"."role" AS "permissions.role",
"permissions"."createdAt" AS "permissions.createdAt",
"permissions"."updatedAt" AS "permissions.updatedAt",
"permissions"."endpointId" AS "permissions.endpointId",
"permissions"."permissionId" AS "permissions.permissionId"
FROM (
SELECT "endpoint"."id",
"endpoint"."method",
"endpoint"."path",
"endpoint"."description",
"endpoint"."createdAt",
"endpoint"."updatedAt"
FROM "endpoints" AS "endpoint"
WHERE "endpoint"."path" ilike '%user%'
AND "permissions"."role" = 'admin' limit 20 offset 0) AS "endpoint"
LEFT OUTER JOIN "permissions" AS "permissions"
ON "endpoint"."id" = "permissions"."endpointId";

Don't try to use limit and offset with hasMany associations and with findAndCountAll.
If you have 2 permissions in each endpoint (for instance you have 10 endpoints and set LIMIT to 10) then you'll get first 5 endpoints because in SQL query endpoints will be multiplied with permissions.
If you still want to filter parent records (endpoints) by some conditions on child records then I suppose you need to use sequelize.literal for that purpose.
If you want to filter parent and child records separately (and still use LIMIT and OFFSET) just move the permissions condition clause to association's where clause and also set separate:true in an association include to make LIMIT and OFFSET work correctly (this will make sequelize to execute separate SQL query to get permissions for each endpoint. This latter case will look like this:
Endpoint.findAndCountAll({
where: {
path: { [Symbol(iLike)]: '%user%' }
},
include: [
{
model: permission,
as: 'permissions',
separate: true,
where: {
role: { [Symbol(eq)]: 'admin' }
}
}
],
order: [],
offset: 0,
limit: 20
})

Related

Query limited relations using typeorm(OneToMany) relations?

I use OneToMany relations for adding messages under conversations. Here, I want to query conversation with last messages only.
Here is an example-
const conversation = await this.conversationRepository.find({
where: [
{ user: { id: reqUser.id }, hide_from_user: false },
{ participant: { id: reqUser.id }, hide_from_participant: false }
],
order: {
updated_at: "DESC"
},
relations: {
user: true,
participant: true,
message: true
}
});
It returns all messages under particular conversation-
But I need only last message. Then how I have to query it? Please help me, here?
TypeOrm doesn't support that directly, to do that you have to make another query
import { InjectRepository } from "typeorm";
const conversation = await this.conversationRepository.find({
where: [
{ user: { id: reqUser.id }, hide_from_user: false },
{ participant: { id: reqUser.id }, hide_from_participant: false }
],
order: {
updated_at: "DESC"
},
relations: {
user: true,
participant: true,
}
});
conversation.messages = await InjectRepository(Message)
.createQueryBuilder('message')
.where('message.conversation = :id' , {conversation.id});
.limit(1)
.orderBy('message.id', 'Desc')
.getMany()

Sequelize - trying to make models dynamic

I've been trying to automate the creation of my sequelize models by creating them with a generic model that I can pass definitions into, rather than creating a model file specifically for each one.
I have an array of model definitions which looks something like this:
const modelDefinitions = [
{
name: "User",
fieldDefinitions: [
{
name: "first_name",
label: "First Name",
column_type: Sequelize.DataTypes.STRING,
},
{
name: "last_name",
label: "Last Name",
column_type: Sequelize.DataTypes.STRING,
},
{
name: "email",
label: "Email",
column_type: Sequelize.DataTypes.STRING,
},
{
name: "password",
label: "Password",
restricted: true,
column_type: Sequelize.DataTypes.STRING,
},
],
},
{
name: "Audit",
fieldDefinitions: [
{
name: "ref",
column_type: Sequelize.DataTypes.STRING,
label: "Audit Ref",
},
{
name: "result",
column_type: Sequelize.DataTypes.STRING,
label: "Result",
},
{
name: "auditor_id",
column_type: Sequelize.DataTypes.INTEGER,
label: "Auditor",
},
],
},
];
When my array of models contains just one model it works perfectly fine, but when I have multiple, the GenericModel of the previously defined models is then "changed" to ne the last one in the list that was initialised.
I'm new to node so I think I'm either missing something or there's some sort of model caching happening, meaning that all instances of GenericModel become what it is initialised as last.
Please see my example below (the commented out code is what I used to use to define the models and the reduce is my new way of defining these)
// {
// User: User.init(sequelize, modelDef),
// Article: Article.init(sequelize, modelDef),
// Audit: Audit.init(sequelize, modelDef),
// Form: Form.init(sequelize, modelDef),
// };
const models = modelDefinitions.reduce((acc, modelDef) => {
return { ...acc, [modelDef.name]: GenericModel.init(sequelize, modelDef) };
}, {});
console.log({ models });
My console.log() returns the following - notice both are Group :
{
models: {
User: Group,
Group: Group
}
}
As you can see, what ever the last model is defined as, the previous ones inherit that instead of keeping what I defined them as originally.
But what I actually want is :
{
models: {
User: User,
Group: Group
}
}
If my list only had User in it, it works fine.
I managed to get this working in the end.
I think my issue was that my GenericModel was treated as a Singleton, so to get around this I changed GenericModel from extending the Sequelize.Model and instead made a new class with a contructor to consume my arguments and then created a method on the new class to return the sequelize model.
The main change there was instead of defining the models with GenericModel.init(), I defined them by calling sequelize.define(modelName, attributes, options)
so my map now looks like this :
const models = modelDefinitions.reduce((acc, modelDef) => {
return { ...acc, [modelDef.name]: new GenericModel(sequelize, modelDef).getDBModel() };
}, {});
and my class:
class TestModel {
constructor(sequelize, modelDef) {
this.sequelize = sequelize;
this.modelDef = modelDef;
this.modelName = modelDef?.name;
this.definitions = modelDef?.fieldDefinitions;
this.restrictedFieldList = this.definitions.filter((field) => field?.restricted).map((definition) => definition.name);
}
getDBModel() {
const model = this.sequelize.define(
this.modelName,
this.definitions.reduce((acc, definition) => {
return { ...acc, [definition.name]: definition.column_type };
}, {}),
{
defaultScope: {
attributes: {
exclude: this.restrictedFieldList,
},
},
sequelize: this.sequelize,
modelName: this.modelName,
}
);
return model;
}
}```

Sequelize how to make a join request?

I'm trying to make joined queries with Sequelize.
That's my db :
What I need is to select all of my relations and get this kind of result:
[
{
id: 1,
State: true,
FK_User: {
id: 2,
Name: "my name"
},
FK_Team: {
id: 3,
Name: "team name"
}
},
...
]
But today I've got this result:
[
{
id: 1,
State: true,
FK_User: 2,
FK_Team: 3
},
...
]
For each of my relations, I've go to do another request to get datas ...
So I putted a look in this Stack and in the doc.
Then I made this code :
let User = this.instance.define("User", {
Name: {
type: this.libraries.orm.STRING,
allowNull: false
}
});
let Team = this.instance.define("Team", {
Name: {
type: this.libraries.orm.STRING,
allowNull: false
}
});
let Relation = this.instance.define("Relation", {
State: {
type: this.libraries.orm.BOOLEAN,
allowNull: false,
defaultValue: 0
}
});
Relation.hasOne(User, {as: "FK_User", foreignKey: "id"});
Relation.hasOne(Team, {as: "FK_Team", foreignKey: "id"});
With this code, I haven't got any relation between tables... So I added theses two lines. I don't understand why I need to make a two direction relation, because I don't need to access Relation From User and Team ...
User.belongsTo(Relation, {foreignKey: 'FK_User_id'});
Team.belongsTo(Relation, {foreignKey: 'FK_Team_id'});
When I do that, I've a FK_User_id in the User table and a FK_Team_id in the Team table ... I don't know how to make this simple relation and get all I need with my futur request and the include: [User, Team]} line.
User.hasOne(Relation);
Team.hasOne(Relation);
Relation.belongsTo(Team);
Relation.belongsTo(User);
This code seems to work.
I don't know why ...
Here your associations are setup correctly you can join it with include :
Relation.findAll({
where : {
state : true
}
include:[
{
model : User
},
{
model : Team
}
]
})

Sequelize throws Error "Unable to find a valid association for model x" When Ordering By Associated Model

I have a problem with sequelize, when I want to ordering my query result by associated model, sequelize throw this error:
UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Unable to find a valid association for model, 'productLanguage'
These are my files:
**Context.js **
const Sequelize = require('sequelize');
const sequelize = new Sequelize("postgres://postgres:123456#localhost:5432/sampleDB");
module.exports = {
Sequelize: Sequelize,
sequelize: sequelize
}
User.js
const context = require('../context');
module.exports = context.sequelize.define('user', {
name: context.Sequelize.STRING,
},{
freezeTableName: true
});
Product.js
const context = require('../context');
module.exports = context.sequelize.define('product', {
slug: context.Sequelize.STRING,
price: context.Sequelize.DECIMAL(10,2),
},{
freezeTableName: true
});
ProductLanguage.js
const context = require('../context');
module.exports = context.sequelize.define('productLanguage', {
name: context.Sequelize.STRING,
},{
freezeTableName: true,
timestamps: false
});
Language.js
const context = require('../context');
module.exports = context.sequelize.define('language', {
name: context.Sequelize.STRING,
slug: context.Sequelize.STRING,
},{
freezeTableName: true
});
db.js
var context = require('./context');
var User = require('./models/User'),
Product = require('./models/Product'),
ProductLanguage = require('./models/ProductLanguage'),
Language = require('./models/Language');
// ===================== ASSOCIATIONS =====================
// user 1:m Product
Product.belongsTo(User); // product owner
User.hasMany(Product);
// Product 1:m ProductLanguage m:1 Language
ProductLanguage.belongsTo(Product);
Product.hasMany(ProductLanguage);
ProductLanguage.belongsTo(Language);
Language.hasMany(ProductLanguage);
module.exports = {
Sequelize: context.Sequelize,
sequelize: context.sequelize,
models: {
Product: Product,
User: User,
ProductLanguage: ProductLanguage,
Language: Language
}
}
and finally this is my query
app.get('/', async (req, res, next)=>{
var result = await db.models.User.findAll({
include:[
{
model: db.models.Product,
attributes: ['price'],
include: [
{
model: db.models.ProductLanguage,
attributes: ['name'],
include: [
{
model: db.models.Language,
attributes: ['name'],
}
]
}
]
}
],
order:[
[db.models.ProductLanguage, 'name', 'desc']
],
attributes: ['name']
});
res.send(result);
});
The query work fine without "order" part, so I think the problem should be one on these :
Something is wrong on this part: [db.models.ProductLanguage, 'name', 'desc']
Something is wrong on association definitions
Note: I've searched on youtube and stackoverflow and sequelize documentation over 4 days but nothing found.
I use these dependencies:
"express": "^4.16.2",
"pg": "^6.4.2",
"pg-hstore": "^2.3.2",
"sequelize": "^4.32.2"
I've found the solution.
I must put all associated model into order, so the correct query is:
order:[
[db.models.Product, db.sequelize.models.ProductLanguage, 'name', 'desc']
],
The full query must be:
var result = await db.models.User.findAll({
include:[
{
model: db.models.Product,
attributes: ['price'],
include: [
{
model: db.models.ProductLanguage,
attributes: ['name'],
include: [
{
model: db.models.Language,
attributes: ['name'],
}
]
}
]
}
],
order:[
[db.models.Product, db.sequelize.models.ProductLanguage, 'name', 'desc']
],
attributes: ['name']
});
I hope this will be helpful for others.
Those who still won't get the result, try this syntax -
order:[[{ model: db.models.ProductLanguage, as: 'language_of_product' } , 'name', 'desc']]
In addition to Moradof's answer, it's important to note that if you specify an alias for your included model, then you must also specify the alias in the order statement.
Building on the previous example, we get:
var result = await db.models.User.findAll({
include:[
{
model: db.models.Product,
as: 'include1',
attributes: ['price'],
include: [
{
model: db.models.ProductLanguage,
as: 'include2',
attributes: ['name'],
include: [
{
model: db.models.Language,
attributes: ['name'],
}
]
}
]
}
],
order:[
[{ model: db.models.Product, as: 'include1' },
{ model: db.sequelize.models.ProductLanguage, as: 'include2' },
'name',
'desc']
],
attributes: ['name']
});
Note that because I named the Product as include1 in the include statement, I also had to name it as include1 in the order statement.
order:[
[{ model: db.models.Product, as: 'include1' },
{ model: db.sequelize.models.ProductLanguage, as: 'include2' },
'name',
'desc']
After using this sequlizer is creating this in query. ORDER BY ``.name DESC LIMIT 10;
So how can I pass the table alias before name.

Sequelize - scope with include

I'm using Sequelize in my Node.js/Express app with the notion of scopes (http://docs.sequelizejs.com/en/latest/docs/scopes/).
I would like to have two scopes on a Model. Each of these scope are based on a join of another table (see the two scopes inLocation & withExpertises):
const Agency = sequelize.define("agency", {
id: { type: DataTypes.INTEGER, primaryKey: true },
name: DataTypes.STRING,
address: DataTypes.STRING,
size: DataTypes.INTEGER,
description: DataTypes.TEXT,
logoFileName: { type: DataTypes.STRING, field: "logo_file_name" },
externalScore: { type: DataTypes.FLOAT, field: "external_score" }
}, {
classMethods: {
associate(models) {
Agency.belongsToMany(models.location, {
through: "locationsAgency",
foreignKey: "location_id"
});
Agency.hasOne(models.locationsAgency);
Agency.hasMany(models.service);
}
},
scopes: {
inLocation(locationId) {
return {
include: [{
model: locationsAgency, where: { locationId: locationId }
}]
};
},
withExpertises(expertiseIds) {
return {
include: [{
model: service, where: { expertiseId: expertiseIds }
}]
};
},
orderByRelevance: {
order: '"externalScore" DESC'
}
}
});
When I'm doing the following call:
models.agency.scope({ method: ["inLocation", 9260] },
{ method: ["withExpertises", 79] },
"orderByRelevance")
.findAll({ limit: 10 })
I have this SQL generated:
SELECT "agency".*,
"services"."id" AS "services.id",
"services"."expertise_id" AS "services.expertiseId"
FROM (
SELECT "agency".*,
"locationsAgency"."id" AS "locationsAgency.id",
"locationsAgency"."location_id" AS "locationsAgency.locationId",
"locationsAgency"."agency_id" AS "locationsAgency.agencyId"
FROM "agencies" AS "agency"
INNER JOIN "locations_agencies" AS "locationsAgency" ON "agency"."id" = "locationsAgency"."agency_id"
AND "locationsAgency"."location_id" = 9260
WHERE (
SELECT "agency_id"
FROM "services" AS "service"
WHERE ("service"."agency_id" = "agency"."id" AND "service"."expertise_id" = 79)
LIMIT 1
)
IS NOT NULL LIMIT 10
) AS "agency"
INNER JOIN "services" AS "services" ON "agency"."id" = "services"."agency_id"
AND "services"."expertise_id" = 79
ORDER BY "externalScore" DESC
As you can see, the scope are nested in the FROM-clause, which give me a awful SQL and then the limit statement is not a the good place.
I expected to have this following SQL query:
SELECT "agency".*,
"services"."id" AS "services.id",
"services"."expertise_id" AS "services.expertiseId",
"services"."created_at" AS "services.created_at",
"services"."updated_at" AS "services.updated_at",
"locationsAgency"."id" AS "locationsAgency.id",
"locationsAgency"."location_id" AS "locationsAgency.locationId",
"locationsAgency"."agency_id" AS "locationsAgency.agencyId"
FROM "agencies" AS "agency"
INNER JOIN "locations_agencies" AS "locationsAgency" ON "agency"."id" = "locationsAgency"."agency_id"
INNER JOIN "services" AS "services" ON "agency"."id" = "services"."agency_id"
AND "locationsAgency"."location_id" = 9260
AND "services"."expertise_id" = 79
ORDER BY "external_score" DESC
LIMIT 10
Any ideas how to have this sql with the use of scopes?
Thanks!

Resources