How to name the fields of related model in filter object? - json

In my project, two models "UserProfile" and "UserAccount" are with a relation that the former "has one" the later. The .json files look like:
userprofile.json:
{
"name": "Userprofile",
"base": "PersistedModel",
//...
"properties": {
"userid": {
"type": "Number"
},
"phoneno": {
"type": "String"
}
},
//...
"relations": {
"userAccounts": {
"type": "hasOne",
"model": "UserAccount",
"foreignKey": "id",
"options": {
"validate": true,
"forceId": false
}
}
}
}
useraccount.json:
{
"name": "UserAccount",
"base": "User",
"idInjection": true,
"restrictResetPasswordTokenScope": true,
"emailVerificationRequired": true,
"properties": {},
"relations": {}
//...
}
The models have corresponding tables in a MariaDB.
Now the quest is to "GET" UserProfile with a keyword that match any one field of UserProfile.phoneno or UserAccount.email (yes, the key point is or). In SQL terms, that is:
SELECT * FROM UserProfile INNER JOIN UserAccount
ON UserProfile.userid = UserAccount.id
WHERE UserProfile.phoneno LIKE '%keyword%'
OR UserAccount.email LIKE '%keyword%'
It should be a common and simple query in SQL but seems become difficult in LookBack. My implementation is:
userprofile.js:
'use strict';
module.exports = function (Userprofile) {
Userprofile.remoteMethod('profileByEmailOrPhoneno', {
description: '...',
http: {path:'/profileByEmailOrPhoneno', verb: 'get'},
accepts: {arg: 'keyword', type: 'string', required: true},
returns: {arg: 'profile', type: 'array' }
})
Userprofile.profileByEmailOrPhoneno = function (keyword, cb) {
let filter = {
fields: {userid: true, nickname: true, phoneno: true},
include: {
relation: 'userAccounts',
scope: {
fields: {username: true, email: true}
}
},
where: {or: [
{phoneno: {like: `%${keyword}%`}},
{'userAccount.email': {like: `%${keyword}%`}}
]}
}
Userprofile.find(
filter,
function (err, records) {
if (err) console.log(err)
else cb(null, records)
}
)
}
};
I tested it on StrongLoop API Explorer and it always returned the whole records in UserProfile no matter whatever keyword. If the criterium
{'userAccount.email': {like: `%${keyword}%`}}
was removed the codes worked correctly. I think this criterium is wrong so LookBack ignores it and evaluate the where section to be true. I modified it to:
{'email': {like: `%${keyword}%`}}
and it was still wrong.
So, I wonder how to correctly name the relation model's field (eg.'email'), or, how to write the correct filter. Anybody can give some help? I'll very appreciated for it. ^^

The include statement in Loopback is a left-outer-join, so the query will always return ALL the Userprofile records. Some will have userAccounts with an array of values, other's wont. You need to further filter the Userprofile records.
Also, you need to put he userAccoutns filter in the scope statement of your filter:
Userprofile.profileByEmailOrPhoneno = function (keyword, cb) {
let filter = {
fields: {userid: true, nickname: true, phoneno: true},
include: {
relation: 'userAccounts',
scope: {
fields: {username: true, email: true},
where: {'email':{'like': `%${keyword}%`}} // userAccounts filter goes here
}
},
where: {phoneno: {like: `%${keyword}%`}}
}
Userprofile.find(filter, function (err, records) {
if (err) console.log(err)
else {
// filter the records for those that have userAccounts
var filteredResults = records.filter(record =>
record.userAccounts &&
Array.isArray(record.userAccounts()) &&
record.userAccounts().length);
cb(null, filteredResults)
}
})
}

Related

How to do where condition on include relation in Loopback

I want to get the result with include relation with where condition on include model.
return this.htcApi.find({
include: [
{
relation: 'nmos',
scope: {
include: 'countries',
},
},
],
where: { name:'Welcome', "nmos.name":'agile'}
});
This where is condition work for name of htc model not for noms module.
I want query like
Select htc.*, nmos.* FROM htc LEFT JOIN nmos ON nmos.id = htc.n_id where htc.name = 'abc' and nmos.name = 'abc';
How can add where condition on the "relation" table?
Simply you need to add where clause in 'scope' object which lies inside the 'include' object. So the code would be like :
return this.htcApi.find({
include: [
{
relation: 'nmos',
scope: {
include: 'countries',
where:{name:'agile'}
},
},
],
where: { name:'Welcome'}
});
In your query, you just need to add the property where within the scope property, like this:
return this.htcApi.find({
include: [
{
relation: 'nmos',
scope: {
include: 'countries',
where: {
and [
{ id: htc.n_id },
{ name: 'abc' },
],
},
},
},
],
where: { name: 'abc' }
});
This should return the htcApi objects named 'abc' with the related nmos objects that have the name 'abc' and the id 'n_id'.

Sequilize query is returning only one row while using include

Context : I am having this problem were I am doing a query using sequilize an it only return's me an array with one position even though I have more than one field that correspond to the query.
This are my two involved models
This is my group.js model
module.exports = (sequelize, DataTypes) => {
const Group = sequelize.define('Group', {
name: DataTypes.STRING,
limit: DataTypes.STRING,
user_id: DataTypes.INTEGER
});
Group.associate = models => {
Group.belongsTo(models.User, { foreignKey: 'user_id' });
};
Group.associate = models => {
Group.hasMany(models.Movement, { foreignKey: 'group_id' });
};
return Group;
}
This is my movement.js model
module.exports = (sequelize, DataTypes) => {
const Mov = sequelize.define('Movement', {
description: DataTypes.STRING,
value: DataTypes.INTEGER,
group_id: DataTypes.INTEGER
});
Mov.associate = models => {
Mov.hasOne(models.Group, { foreignKey: 'group_id' });
};
return Mov;
}
This is my query (where you will see that I am doing an INNER JOIN to SUM the fields of the Movement table)
router.get('/', verify, async (req, res) => {
try {
const group = await Group.findAll({
attributes: [
'id',
'name',
'limit',
[sequelize.fn('SUM', sequelize.col('Movements.value')), 'total_spent'],
],
include: [{
attributes: [], // this is empty because I want to hide the Movement object in this query (if I want to show the object just remove this)
model: Movement,
required: true
}],
where: {
user_id: req.userId
}
});
if (group.length === 0) return res.status(400).json({ error: "This user has no groups" })
res.status(200).json({ groups: group }) //TODO see why this is onyl return one row
} catch (error) {
console.log(error)
res.status(400).json({ Error: "Error while fetching the groups" });
}
});
Problem is that it only return's one position of the expected array :
{
"groups": [
{
"id": 9,
"name": "rgrgrg",
"limit": 3454354,
"total_spent": "2533"
}
]
}
It should return 2 positions
{
"groups": [
{
"id": 9,
"name": "rgrgrg",
"limit": 3454354,
"total_spent": "2533"
},
{
"id": 9,
"name": "rgrgrg",
"limit": 3454354,
"total_spent": "2533"
}
]
}
This is the query sequilize is giving me:
SELECT `Group`.`id`, `Group`.`name`, `Group`.`limit`, SUM(`Movements`.`value`) AS `total_spent` FROM `Groups` AS `Group` INNER JOIN `Movements` AS `Movements` ON `Group`.`id` = `Movements`.`group_id` WHERE `Group`.`user_id` = 1;
I guess you need to add an appropriate group by clause as follows -
const group = await Group.findAll({
attributes: [
'id',
'name',
'limit',
[sequelize.fn('SUM', sequelize.col('Movements.value')), 'total_spent'],
],
include: [{
attributes: [], // this is empty because I want to hide the Movement object in this query (if I want to show the object just remove this)
model: Movement,
required: true
}],
where: {
user_id: req.userId
},
group: '`Movements`.`group_id`'
});
Many-to-many "through" table with multiple rows of identical foreign key pairs only returns one result?
I just ran into this bug and added this options to the main query:
{
raw: true,
plain: false,
nest: true
}
Then you just merge the query.
It's a workaround, but might help someone.

Passing associated table attributes as main table attributes in sequelize

I have a query which is similar to the following.
const TODAY = new Date().setHours(0, 0, 0, 0);
const studentAttendances = await STUDENT_ATTENDANCES.findAll({
where: {
punch_in: { [Op.gt]: TODAY },
},
attributes: ['id', 'student_id', 'arrived_time'],
include: [
{
model: STUDENTS,
attributes: ['name'],
},
],
raw: true,
nest: true,
});
The current output given is an array of objects which look like the following.
{
"id": 1041,
"student_id": 16,
"arrived_time": "2019-05-29T08:29:41.000Z",
"student": {
"name": "Tom"
}
},
Instead of having a nested object as above how do i make the student name itself be an attribute of the main object ? Example as follows.
{
"id": 1041,
"student_id": 16,
"arrived_time": "2019-05-29T08:29:41.000Z",
"student": "Tom"
},
I hope to do this through sequelize without using any JS loops
Something like this should work, assuming your singular model name is "Student":
const studentAttendances = await STUDENT_ATTENDANCES.findAll({
where: {
punch_in: { [Op.gt]: TODAY },
},
attributes: [
[sequelize.col('Student.name'), 'studentName'], // will give you name as 'studentName'
'id', 'student_id', 'arrived_time'
],
include: [
{
model: STUDENTS,
attributes: [], // empty this out
},
]
});
I think you can handle it with pure javascript :
studentAttendances = studentAttendances.get({plain: true})
for(student in studentAttendances){
studentAttendances[student].student = studentAttendances[student].student.name
}

Counting associated entries with Sequelize

I have two tables, locations and sensors. Each entry in sensors has a foreign key pointing to locations. Using Sequelize, how do I get all entries from locations and total count of entries in sensors that are associated with each entry in locations?
Raw SQL:
SELECT
`locations`.*,
COUNT(`sensors`.`id`) AS `sensorCount`
FROM `locations`
JOIN `sensors` ON `sensors`.`location`=`locations`.`id`;
GROUP BY `locations`.`id`;
Models:
module.exports = function(sequelize, DataTypes) {
var Location = sequelize.define("Location", {
id: {
type: DataTypes.INTEGER.UNSIGNED,
primaryKey: true
},
name: DataTypes.STRING(255)
}, {
classMethods: {
associate: function(models) {
Location.hasMany(models.Sensor, {
foreignKey: "location"
});
}
}
});
return Location;
};
module.exports = function(sequelize, DataTypes) {
var Sensor = sequelize.define("Sensor", {
id: {
type: DataTypes.INTEGER.UNSIGNED,
primaryKey: true
},
name: DataTypes.STRING(255),
type: {
type: DataTypes.INTEGER.UNSIGNED,
references: {
model: "sensor_types",
key: "id"
}
},
location: {
type: DataTypes.INTEGER.UNSIGNED,
references: {
model: "locations",
key: "id"
}
}
}, {
classMethods: {
associate: function(models) {
Sensor.belongsTo(models.Location, {
foreignKey: "location"
});
Sensor.belongsTo(models.SensorType, {
foreignKey: "type"
});
}
}
});
return Sensor;
};
Use findAll() with include() and sequelize.fn() for the COUNT:
Location.findAll({
attributes: {
include: [[Sequelize.fn("COUNT", Sequelize.col("sensors.id")), "sensorCount"]]
},
include: [{
model: Sensor, attributes: []
}]
});
Or, you may need to add a group as well:
Location.findAll({
attributes: {
include: [[Sequelize.fn("COUNT", Sequelize.col("sensors.id")), "sensorCount"]]
},
include: [{
model: Sensor, attributes: []
}],
group: ['Location.id']
})
For Counting associated entries with Sequelize
Location.findAll({
attributes: {
include: [[Sequelize.fn('COUNT', Sequelize.col('sensors.location')), 'sensorCounts']]
}, // Sequelize.col() should contain a attribute which is referenced with parent table and whose rows needs to be counted
include: [{
model: Sensor, attributes: []
}],
group: ['sensors.location'] // groupBy is necessary else it will generate only 1 record with all rows count
})
Note :
Some how, this query generates a error like sensors.location is not exists in field list. This occur because of subQuery which is formed by above sequelize query.
So solution for this is to provide subQuery: false like example
Location.findAll({
subQuery: false,
attributes: {
include: [[Sequelize.fn('COUNT', Sequelize.col('sensors.location')), 'sensorCounts']]
},
include: [{
model: Sensor, attributes: []
}],
group: ['sensors.location']
})
Note:
**Sometime this could also generate a error bcz of mysql configuration which by default contains only-full-group-by in sqlMode, which needs to be removed for proper working.
The error will look like this..**
Error : Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'db.table.id' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by
So to resolve this error follow this answer
SELECT list is not in GROUP BY clause and contains nonaggregated column .... incompatible with sql_mode=only_full_group_by
Now this will successfully generate all associated counts
Hope this will help you or somebody else!
Location.findAll({
attributes: {
include: [[Sequelize.fn("COUNT", Sequelize.col("sensors.id")), "sensorCount"]]
},
include: [{
model: Sensor, attributes: []
}]
});
and it works. but when i add "limit", i got error: sensors undefined
Example of HAVING, ORDER BY, INNER vs OUTER JOIN + several bugs/unintuitive behavior
I went into more detail at: Sequelize query with count in inner join but here's a quick summary list of points:
you must use row.get('count'), row.count does not work
you must parseInt on PostgreSQL
this code fails on PostgreSQL with column X must appear in the GROUP BY clause or be used in an aggregate function due to a sequelize bug
OUTER JOIN example which includes 0 counts by using required: false:
sqlite.js
const assert = require('assert');
const { DataTypes, Op, Sequelize } = require('sequelize');
const sequelize = new Sequelize('tmp', undefined, undefined, Object.assign({
dialect: 'sqlite',
storage: 'tmp.sqlite'
}));
;(async () => {
const User = sequelize.define('User', {
name: { type: DataTypes.STRING },
}, {});
const Post = sequelize.define('Post', {
body: { type: DataTypes.STRING },
}, {});
User.belongsToMany(Post, {through: 'UserLikesPost'});
Post.belongsToMany(User, {through: 'UserLikesPost'});
await sequelize.sync({force: true});
const user0 = await User.create({name: 'user0'})
const user1 = await User.create({name: 'user1'})
const user2 = await User.create({name: 'user2'})
const post0 = await Post.create({body: 'post0'})
const post1 = await Post.create({body: 'post1'})
const post2 = await Post.create({body: 'post2'})
// Set likes for each user.
await user0.addPosts([post0, post1])
await user1.addPosts([post0, post2])
let rows = await User.findAll({
attributes: [
'name',
[sequelize.fn('COUNT', sequelize.col('Posts.id')), 'count'],
],
include: [
{
model: Post,
attributes: [],
required: false,
through: {attributes: []},
where: { id: { [Op.ne]: post2.id }},
},
],
group: ['User.name'],
order: [[sequelize.col('count'), 'DESC']],
having: sequelize.where(sequelize.fn('COUNT', sequelize.col('Posts.id')), Op.lte, 1)
})
assert.strictEqual(rows[0].name, 'user1')
assert.strictEqual(parseInt(rows[0].get('count'), 10), 1)
assert.strictEqual(rows[1].name, 'user2')
assert.strictEqual(parseInt(rows[1].get('count'), 10), 0)
assert.strictEqual(rows.length, 2)
})().finally(() => { return sequelize.close() });
with:
package.json
{
"name": "tmp",
"private": true,
"version": "1.0.0",
"dependencies": {
"pg": "8.5.1",
"pg-hstore": "2.3.3",
"sequelize": "6.5.1",
"sqlite3": "5.0.2"
}
}
and Node v14.17.0.
INNER JOIN version excluding 0 counts:
let rows = await User.findAll({
attributes: [
'name',
[sequelize.fn('COUNT', '*'), 'count'],
],
include: [
{
model: Post,
attributes: [],
through: {attributes: []},
where: { id: { [Op.ne]: post2.id }},
},
],
group: ['User.name'],
order: [[sequelize.col('count'), 'DESC']],
having: sequelize.where(sequelize.fn('COUNT', '*'), Op.lte, 1)
})
assert.strictEqual(rows[0].name, 'user1')
assert.strictEqual(parseInt(rows[0].get('count'), 10), 1)
assert.strictEqual(rows.length, 1)
How about defining a database view for it and then a model for that view? You can just get the relationship to the view included in your query whenever you need the number of sensors. The code may look cleaner this way, but I'm not aware if there will be performance costs. Somebody else may answer that...
CREATE OR REPLACE VIEW view_location_sensors_count AS
select "locations".id as "locationId", count("sensors".id) as "locationSensorsCount"
from locations
left outer join sensors on sensors."locationId" = location.id
group by location.id
When defining the model for the view you remove the id attribute and set the locationId as the primary key.
Your model could look like this:
const { Model, DataTypes } = require('sequelize')
const attributes = {
locationID: {
type: DataTypes.UUIDV4, // Or whatever data type is your location ID
primaryKey: true,
unique: true
},
locationSensorsCount: DataTypes.INTEGER
}
const options = {
paranoid: false,
modelName: 'ViewLocationSensorsCount',
tableName: 'view_location_sensors_count',
timestamps: false
}
/**
* This is only a database view. It is not an actual table, so
* DO NOT ATTEMPT insert, update or delete statements on this model
*/
class ViewLocationSensorsCount extends Model {
static associate(models) {
ViewLocationSensorsCount.removeAttribute('id')
ViewLocationSensorsCount.belongsTo(models.Location, { as:'location', foreignKey: 'locationID' })
}
static init(sequelize) {
this.sequelize = sequelize
return super.init(attributes, {...options, sequelize})
}
}
module.exports = ViewLocationSensorsCount
In the end, in your Location model you set a hasOne relationship to the Sensor model.

Derived attribute absent in JSON response in Sails.js

I'm writing an API for users in an example app. The api/models/User-file looks as follows:
module.exports = {
attributes: {
firstName: {
type: 'string',
required: true
},
lastName: {
type: 'string',
required: true
},
fullName: function () {
return this.firstName + ' ' + this.lastName;
}
}
};
However, when I find all my users, the derived attribute is nowhere to be found in the response:
[
{
"firstName": "Marlon",
"lastName": "Brando",
"createdAt": "2015-09-13T10:05:15.129Z",
"updatedAt": "2015-09-13T10:05:15.129Z",
"id": 8
},
{
"firstName": "Bjoern",
"lastName": "Gustavsson",
"createdAt": "2015-09-13T10:05:36.221Z",
"updatedAt": "2015-09-13T10:05:36.221Z",
"id": 10
},
{
"firstName": "Charlie",
"lastName": "Sheen",
"createdAt": "2015-09-13T10:06:59.999Z",
"updatedAt": "2015-09-13T10:06:59.999Z",
"id": 11
}
]
Am I missing something, or is it simply not possible to derive attributes like this?
When you are set attributes in Model with function it doesn't mean that it will be executed in resulting attribute. It means that you can call this function in your code. For instance, I have exactly your User model. I can make in my code smth like this:
// api/controllers/UserController.js
module.exports = {
index: function(req, res) {
User
.create({firstName: req.param('firstName'), lastName: req.param('lastName')})
.then(function(user) {
console.log(user.fullName());
return user;
})
.then(res.ok)
.catch(res.negotiate);
}
};
If you want to make it like a dynamic attribute, then you should take a look at toJSON method in your model. You can override it and implement your own logic. I think it will looks like this in your case:
// api/models/User.js
module.exports = {
attributes: {
firstName: {
type: 'string'
},
lastName: {
type: 'string'
},
fullName: function() {
return [this.firstName, this.lastName].join(' ');
},
toJSON: function() {
var obj = this.toObject();
obj.fullName = this.fullName();
return obj;
}
}
};
I didn't check this code but think that should work. You can play around with toJSON method and see what you got. Ping me in comments if code doesn't work.