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

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

Related

add ascending or descending to url in React

I am trying to create a url based on the data I get from the user.if we consider this to be my url:
let url = new URL('http://localhost:8080/api/movies/search/search');
I add the search fields like this:
for (let item in data) {
url.searchParams.set(item,data[item]);
}
but at the end of my url I want to add the sort type for it to look something like this:
const url = `http://localhost:8080/api/movies/search/search?title=something&minRate=10&genre=action&sort=title,asc`;
so how should I add the last part with the comma:
,asc
to the url?
Based on comments on the question above...
If data is this:
{
title: 'the',
minRate: 2,
genre: 'action',
sortType: 'title',
type: 'asc'
}
And the result you want is this:
http://localhost:8080/api/movies/search/search?title=the&minRate=2&genre=action&sort=title,asc
Then data doesn't match what you're looking for. It has two properties called sortType and type, and you want one combined property called sort.
Project the object into the shape you want, then use that new object to build your params:
let data = {
title: 'the',
minRate: 2,
genre: 'action',
sortType: 'title',
type: 'asc'
};
let url = new URL('http://localhost:8080/api/movies/search/search');
// create the object you want:
let urlData = {
title: data.title,
minRate: data.minRate,
genre: data.genre,
sort: `${data.sortType},${data.type}`
};
// then add params from *that* object:
for (let item in urlData) {
url.searchParams.set(item, urlData[item]);
}
console.log(url);
(Note that the , character is URL-encoded by default.)
Basically, don't change the logic to work around the data structure. Keep the logic simple and change the data structure to what you need it to be.

Sequelize "object" does not have increment function

I am using generate sequelize tables from Django generated models with sequelize-auto in my project. so far so good. I wrote the code to update the rank if I see a url again.
const findLink = async (link) =>
Link.findOne({
where: {
url: link,
},
raw: true,
});
// eslint-disable-next-line no-use-before-define
const insertEvent = async (link, tweetText) => {
// Sync Database table before reading
await sequelize.sync();
findLink(link).then((url) => {
if (url) {
url.increment("rank", {by: 1}).then(()=>{
Event.create({
url_id: url.id,
tweet_text: tweetText,
created_at: new Date(),
updated_at: new Date(),
})
});
} else {
Link.create({
url: link,
rank: 0,
created_at: new Date(),
updated_at: new Date(),
}).then((newLink) => {
Event.create({
url_id: newLink.id,
tweet_text: tweetText,
created_at: new Date(),
updated_at: new Date(),
});
});
}
});
};
But the problem is that when It execute url.increment("rank", {by: 1}) it says that url does not have increment function.
But according to documentation it is clearly stated here. Please let me know if I am doing something wrong? I have searched the internet but I could not find any thing relative. I can update the value with duplicate look up but I am looking for a way If I could update the already found object instead of searching it again.
You are using raw queries
const findLink = async (link) =>
Link.findOne({
where: {
url: link,
},
raw: true,
});
It doesn't return an instance of the Model
See https://sequelize.org/master/manual/raw-queries.html
Edit:
By default the function will return two arguments - a results array, and an object containing metadata (such as amount of affected rows, etc).
A second option is the model. If you pass a model the returned data
will be instances of that model.
// Callee is the model definition. This allows you to easily map a query to a predefined model
const projects = await sequelize.query('SELECT * FROM projects', {
model: Projects,
mapToModel: true // pass true here if you have any mapped fields
});
// Each element of `projects` is now an instance of Project
So the mapToModel option might also work

Retrive some columns of relations in typeorm

I need to retrieve just some columns of relations in typeorm query.
I have an entity Environment that has an relation with Document, I want select environment with just url of document, how to do this in typeorm findOne/findAndCount methods?
To do that you have to use a querybuilder, here's an example:
return this.createQueryBuilder('environment') // use this if the query used inside of your entity's repository or getRepository(Environment)...
.select(["environment.id","environment.xx","environment.xx","document.url"])
.leftJoin("environment.document", "document")
.where("environment.id = :id ", { id: id })
.getOne();
Sorry I can't add comment to post above. If you by not parsed data mean something like "environment.id" instead of "id"
try this:
return this.createQueryBuilder("environment")
.getRepository(Environment)
.select([
"environment.id AS id",
"environment.xx AS xx",
"document.url AS url",
])
.leftJoin("environment.document", "document")
.where("environment.id = :id ", { id: id })
.getRawOne();
Here is the code that works for me, and it doesn't require using the QueryBuilder. I'm using the EntityManager approach, so assuming you have one of those from an existing DataSource, try this:
const environment = await this.entityManager.findOne(Environment, {
select: {
document: {
url: true,
}
},
relations: {
document: true
},
where: {
id: environmentId
},
});
Even though the Environment attributes are not specified in the select clause, my experience is that they are all returned in the results, along with document.url.
In one of the applications that I'm working on, I have the need to bring back attributes from doubled-nested relationships, and I've gotten that to work in a similar way, shown below.
Assuming an object model where an Episode has many CareTeamMembers, and each CareTeamMember has a User, something like the code below will fetch all episodes (all attributes) along with the first and last name of the associated Users:
const episodes = await this.entityManager.find(Episode, {
select: {
careTeamMembers: {
id: true, // Required for this to work
user: {
id: true,
firstName: true,
lastName: true,
},
}
},
relations: {
careTeamMembers: {
user: true,
}
},
where: {
deleted: false,
},
});
For some reason, I have to include at least one attribute from the CareTeamMembers entity itself (I'm using the id) for this approach to work.

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

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,
}

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

Categories