Sequelize - Query with Many-to-Many relationship using where and limit - javascript

I have this relationship:
Clients -> ProgramsClients <- Programs
What i'm trying to do is basically:
SELECT * FROM Programs p JOIN ProgramsClients pc on p.id = pc.programId WHERE pc.clientId = 1 LIMIT 0, 100;
I have managed to reach something like this with the following code:
query = {
include: [{
model: models.Clients,
attributes: [],
require: true,
}],
where: { '$Clients.id$': 1 }
}
models.Programs.findAll(query) // This works
Which generates:
SELECT [...]
FROM `programs` AS `Programs` LEFT OUTER JOIN (
`ProgramsClients` AS `Clients->ProgramsClients`
INNER JOIN `clients` AS `Clients`
ON `Clients`.`id` = `Clients->ProgramsClients`.`ClientId`)
ON `Programs`.`id` = `Clients->ProgramsClients`.`ProgramId`
WHERE `Clients`.`id` = 1;
This works, but when i try limitting it, i get an error.
The code:
query = {
include: [{
model: models.Clients,
attributes: [],
require: true,
}],
limit: 0,
offset: 10,
where: { '$Clients.id$': 1 }
}
models.Programs.findAll(query) // This fails
Which generates:
SELECT [...]
FROM (SELECT `Programs`.`id`, `Programs`.`name`, `Programs`.`description`, `Programs`.`createdAt`, `Programs`.`updatedAt`
FROM `programs` AS `Programs` WHERE `Clients`.`id` = 1 LIMIT 0, 10) AS `Programs`
LEFT OUTER JOIN ( `ProgramsClients` AS `Clients->ProgramsClients`
INNER JOIN `clients` AS `Clients`
ON `Clients`.`id` = `Clients->ProgramsClients`.`ClientId`)
ON `Programs`.`id` = `Clients->ProgramsClients`.`ProgramId`;
Error:
DatabaseError [SequelizeDatabaseError]: Unknown column 'Clients.id' in 'where clause'
NOTE: I'm using a MySQL database.
Is there any easier way to solve this and generate the desired (or similar) result for SQL?
Thanks in advance

I took a pause. And when i returned, i managed to solve it.
Basically, i had misread the super many-to-many section from docs.
You can simply define an One-to-many relationship (even if you're using many-to-many relationships) with the association's table (In this case, ProgramsClients) and then include ProgramsClients and do whatever you want. (You must declare an id column for ProgramsClients for this).
query = {
include: [{
model: models.ProgramsClients,
as: 'programsclient'
attributes: [],
require: true,
where: { clientId: 1 }
}],
limit: 0,
offset: 10,
}

Related

JOIN subquery in suiteCRM Error 40, ("name":"Access Denied","number":40,"description":"You do not have access")

Using a suiteCRM query that connects with sugarCRM i need to retrieve all the opportunities that have an specific user and client id, being the client id (account_id) not queryable (at least directly).
So this means that i cannot use a syntax like this one:
session: await CRMAuth.getSession(),
modules: CRM_MODULES.OPPORTUNITY,
query: `assigned_user_id = '${uid}' AND account_id = '${client_id}'`,
order_by: 'date_modified DESC',
offset: 0,
select_fields: [
'id',
'name',
],
link_name_to_fields_array: [],
max_results: 100,
deleted: false,
but instead i should have something like this as the query:
session: await CRMAuth.getSession(),
modules: CRM_MODULES.OPPORTUNITY,
query: `opportunities.assigned_user_id = '${uid}' AND opportunities.id IN (
SELECT opportunity_id FROM accounts_opportunities r
JOIN accounts a ON (r.account_id = a.id)
WHERE a.id = '${account_id}'
AND r.deleted=0 and a.deleted=0)`,
order_by: 'date_modified DESC',
offset: 0,
select_fields: [
'id',
'name',
],
link_name_to_fields_array: [],
max_results: 100,
deleted: false,
i tried differents variations of this like uppercases in modules/tables names, and adding AS before "r", "a" (and even i replaced them with the full name). I also tried simpler queries but i still got the same error.
Also looking at similar problems i can assure that is not a session problem, but a syntax one.
My 2 mistakes were:
Relating the modules upside-down
Using the get_entry_list method instead of get_relationships (this method ask for different parameters)
get_relationships
So i ended up with this structure
let response = await CRM(CRM_METHODS.GET_RELATIONSHIPS, {
session: await CRMAuth.getSession(),
module_name: CRM_MODULES.ACCOUNTS,
module_id: '${client_id}',
link_field_name: `opportunities`,
related_module_query:`opportunities.assigned_user_id = '${uid}'`,
related_fields: [*the fields i want*],
related_module_link_name_to_fields_array:[],
deleted: 0,
});
BE CAREFULL: there is not an id for the module (Opportunities, Accounts, etc), the "module_id" field is the id of the RECORD that you are working with, in my case the id of my client.
This bring me all the opportunities created by a specific user for a specific client

i want to send unique records for every user who logs in from a mysql database

i want to send every user who logs in a list of unique records i.e not same records from the database,
for every user i want to skip the records that have already been sent to other signed users.bare with me ,am a beginner,how can i implement such?
here is the code that fetches the records from the database
phrases.findAll({
where: {
userId: user.id,
phraseStatus: 1
},
limit: 10,
offset,
10
})
.then((data) => {
userObj.phrases.push(...data);
return res.status(200).json(userObj);
});
You will need to keep track of what has been sent to other users. I suggest you keep a table like phrases_sent to record what has been sent so far. Add the phrase_id to this new table so you have a relationship.
Take a look at associations in the docs.
You can then query the phrases table by outer joining to your phrases_sent table to return phrases that have not been sent so far.
Something like:
const phrases = await sequelize.models.phrase.findAll({
where: {
'$phrasesSents.phrase_id$': {[Op.is]: null} // query the joined table
},
attributes: ['phrase.phrase'], // the unused phrases
include: [
{
attributes: [],
model: sequelize.models.phrasesSent,
required: false, // specify this is a left outer join
}
],
raw: true,
nest: true
});
Would yield:
SELECT "phrase"."phrase"
FROM "phrases" AS "phrase"
LEFT OUTER JOIN "phrases_sent" AS "phrasesSents" ON "phrase"."id" = "phrasesSents"."phrase_id"
WHERE "phrasesSents"."phrase_id" IS NULL;
I believe doing a LEFT OUTER JOIN has performance benefits over a NOT IN (SELECT ...) style query.

Sequelize wrap column with " in where condition

I have a JSONB column in DB.
I'd like to have request to DB where I can check if some value in this JSON it true or false:
SELECT *
FROM table
WHERE ("json_column"->'data'->>'data2')::boolean = true AND id = '00000000-1111-2222-3333-456789abcdef'
LIMIT 1
So, my sequelize request:
const someVariableWithColumnName = 'data2';
Model.findOne({
where: {
[`$("json_column"->'data'->>'${someVariableWithColumnName}')::boolean$`]: true,
id: someIdVariable,
},
order: [/* some order, doesn't matter */],
})
And sequelize generate bad result like:
SELECT *
FROM table
WHERE "(json_column"."->'data'->>'data2')::boolean" = true AND id = '00000000-1111-2222-3333-456789abcdef'
LIMIT 1
Split my column by . and add " to every element.
Any idea how to get rid of adding " to the column in where condition?
Edit:
Here is my query with sequelize.literal():
const someVariableWithColumnName = 'data2';
Model.findOne({
where: {
[sequelize.literal(`$("json_column"->'data'->>'${someVariableWithColumnName}')::boolean$`)]: true,
id: someIdVariable,
},
order: [/* some order, doesn't matter */],
})
You can use Sequelize.literal() to avoid spurious quotes. IMHO, wrapping the json handling in a db function might also be helpful.
I just came across a similar use case.
I believe you can use the static sequelize.where method in combination with sequelize.literal.
Here is the corresponding documentation in sequelize API reference: https://sequelize.org/master/class/lib/sequelize.js~Sequelize.html#static-method-where
And here is an example (although I will admit hard to find) in the regular documentation:
https://sequelize.org/master/manual/model-querying-basics.html#advanced-queries-with-functions--not-just-columns-
In the end for your specific sit try something like this:
const someVariableWithColumnName = 'data2';
Model.findOne({
where: {
[Op.and]: [
// We provide the virtual column sql as the first argument of sequelize.where with sequelize.literal.
// We provide the matching condition as the second argument of sequelize.where, with the usual sequelize syntax.
sequelize.where(sequelize.literal(`$("json_column"->'data'->>'${someVariableWithColumnName}')::boolean$`), { [Op.eq]: true }),
{ id: someIdVariable }
]
})

SequelizeJS - How to Map Nested (eager loading) Models for Raw Queries?

Below is an example of a raw query.
const query = `SELECT
links.name, links.type, links.code, links.originalUrl,
domains.id as 'domain.id', domains.host as 'domain.host',
count(
CASE WHEN hits.datetime > "${past}" AND hits.datetime <= "${now}" = true then 1 END
) as hitCount
FROM links
LEFT JOIN hits ON links.id = hits.linkId
LEFT JOIN domains ON links.domainId = domains.id
WHERE links.userId = ${req.user.id}
GROUP BY links.id, hits.linkId
ORDER BY hitCount DESC
LIMIT 5`;
const links = await sequelize.query(query.trim(), {
type: sequelize.QueryTypes.SELECT,
model: Link,
mapToModel: true
});
I am mapping the query result into the model Link with mapToModel: true. It works well but when I try to get some data from a joined table and map that into an object in model it doesn't convert into array.
For example I am trying to get domains like domains.id as 'domain.id', domains.host as 'domain.host', This is how I saw sequlize does the query for eager loaded data.
But when I get the result object the I don't get a nested domain object property.
// desired link object
{
id: 3,
name: 'My test link',
domain: {
id: 23,
host: 'example.com'
}
}
instead what I get is
// current link object
{
id: 3,
name: 'My test link',
'domain.id': 23,
'domain.host': 'example.com'
}
So nested objects aren't mapping correctly.
UPDATE
I have found nest options in query() documentation but setting nest: true doesn't seems to have any effect.
Changed Query
const links = await sequelize.query(query.trim(), {
type: sequelize.QueryTypes.SELECT,
model: Link,
mapToModel: true,
nest: true, // doesn't have any effect
});
For me nest worked only in conjunction with raw:
const links = await sequelize.query(query.trim(), {
type: sequelize.QueryTypes.SELECT,
model: Link,
mapToModel: true,
nest: true,
raw: true // Without this `nest` hasn't effect, IDK why
});
I have run into the same problem. My solution was:
return sequelize.query(/* query */, {
nest: true,
type: sequelize.QueryTypes.SELECT
});
As you see, you should not set mapToModel prop, just nest and type.
You can install dottie yourself, which is what nest: true is supposed to be doing (https://github.com/mickhansen/dottie.js):
import dottie from 'dottie';
// ...
const formattedLinks = links.map(l => dottie.transform(l.toJSON()))
It seems that raw queries with nest:true doesn't works with mapToModel. So the only way I found is to include nest:true, map with Link.build(link) and { include: Domain} option:
const query = `SELECT
links.name, links.type, links.code, links.originalUrl,
domains.id as 'domain.id', domains.host as 'domain.host',
count(
CASE WHEN hits.datetime > "${past}" AND hits.datetime <= "${now}" = true then 1 END
) as hitCount
FROM links
LEFT JOIN hits ON links.id = hits.linkId
LEFT JOIN domains ON links.domainId = domains.id
WHERE links.userId = ${req.user.id}
GROUP BY links.id, hits.linkId
ORDER BY hitCount DESC
LIMIT 5`;
const links = (await sequelize.query(query.trim(), {
nest: true,
type: sequelize.QueryTypes.SELECT,
}));
links.map((link) => Link.build(link, {include: Domain}))
As far as I can see the Link model has one domain with association so you will need to pass
include: <domain model> to map to domain model.
have a look at this : https://github.com/sequelize/sequelize/issues/1830

Error: Tried to select attributes using Sequelize.cast or Sequelize.fn without specifying an alias for the result, during eager loading

Data: an Office has many OfficeLocations, each of them has many Ratings. I need to write a query, that fetches only Offices, that have at lest one Rating. My query:
let condition = {
include: [{
model: OfficeLocation.unscoped(),
attributes: [
'"Office"."id" as "Office.id"',
'"OfficeLocations"."id" AS "OfficeLocation.id"'
],
include: [
{
model: Rating.unscoped(),
attributes: [
'*',
sequelize.fn('COUNT', sequelize.col('"OfficeLocations->Ratings"."id"'))
]
}
],
group: '"Office.id", "OfficeLocation.id"',
having: sequelize.where(
sequelize.fn('COUNT', sequelize.col('"OfficeLocations->Ratings"."id"')),
'>',
0
)
}]
}
Office.findAll(condition).then(data => {
res.send(data);
}).catch(e => {
console.log(e);
});
But I have an error in console:
Error: Tried to select attributes using Sequelize.cast or Sequelize.fn without specifying an alias for the result, during eager loading. This means the attribute will not be added to the returned instance
at include.attributes.map.attr (/Users/.../node_modules/sequelize/lib/dialects/abstract/query-generator.js:1307:17)
at Array.map (<anonymous>)
at Object.generateInclude (/Users/.../node_modules/sequelize/lib/dialects/abstract/query-generator.js:1287:52)
at Object.generateInclude (/Users/.../node_modules/sequelize/lib/dialects/abstract/query-generator.js:1355:39)
I've also tried
where: sequelize.literal('COUNT(DISTINCT(`OfficeLocations`.`Ratings`.`id`)) > 0'),
instead of attributes/group/having, but it doesn't work too.
Thanks.
Update
This SQL query works as I need:
sequelize.query(`
SELECT
"Office"."id" as "Office.id",
"Office"."name",
"Office"."website",
"OfficeLocations"."id" AS "OfficeLocations.id",
COUNT("OfficeLocations->Ratings"."id") as "RatingsCount"
FROM "Companies" AS "Office"
LEFT OUTER JOIN ( "OfficeLocations" AS "OfficeLocations"
INNER JOIN "Ratings" AS "OfficeLocations->Ratings"
ON "OfficeLocations"."id" = "OfficeLocations->Ratings"."OfficeLocationId"
)
ON "Office"."id" = "OfficeLocations"."OfficeId"
GROUP BY "Office.id", "OfficeLocations.id"
HAVING COUNT("OfficeLocations->Ratings"."id") > 0
`)
Except I want to fetch all data.
Please, see Model.findAll syntax for options argument.
group and having are properties of options object.
For alias of selected columns/expr (attributes option) use array: [expr, alias].
You can pass attributes.include and/or attributes.exclude arrays.
You can pass include[].attributes option for reference on attributes of the included Model.
Also you can use include[].required option for select between INNER AND OUTER JOIN.
Your case:
let options = {
include: [
{
model: OfficeLocation,
required: false, //false for OUTER JOIN, but I think that you can use INNER JOIN
attributes: [
"id", //this is OfficeLocation.id, see 4th item above.
[Sequelize.fn("COUNT", Sequelize.col('`OfficeLocations->Ratings`.`id`')), "RatingsCount"]
],
include: [
{
model: UserRating,
attributes: [],
required: true
}
]
}
],
group: [
`Office.id`,
`OfficeLocations.id`
],
having: Sequelize.where(Sequelize.fn("COUNT", Sequelize.col('`OfficeLocations->Ratings`.`id`')), ">", 0)
};
Note that aliases generated by Sequelize may changes, so you should update it for Sequelize.col.

Categories