knex: what is the appropriate way to create an array from results? - javascript

I have an endpoint that joins the user and user_emails table as a one-to-many relationship (postgresql). It look as follows.
router.get('/', function (req, res, next) {
db.select('users.id', 'users.name', 'user_emails.address')
.from('users')
.leftJoin('user_emails', 'users.id', 'user_emails.user_id')
.then(users => res.status(200).json(users))
.catch(next) // go to error handler
});
However, this will return a new document for each email address. What I want is an array of documents that looks as follows:
[{
id: 1,
name: 'Steve',
emails: [
{ address: 'hello#world.org' },
{ address: 'meow#meow.org' }
]
}, {
id: 2,
name: 'Jimmy',
emails: [
{ address: 'jimmy#jimbo.org' }
]
}]
How should this be done in knex?

Assuming you're using Postgres - you need to use array_agg function to generate arrays. I would suggest using knex.raw
Please let me know if this works.
knex('users')
.innerJoin('user_emails','users.id','user_emails.user_id')
.select([
'users.id as userID',
'users.name as userName',
knex.raw('ARRAY_AGG(user_emails.adress) as email')
])
.groupBy('users.id','users.name')

Related

How to delete items out of a nested array by their _id in Mongoose

I have a Model, Users, and each user has an array of objects called habits.
{
_id: 606f1d67aa1d5734c494bf0a,
name: 'Courtney',
email: 'c#gmail.com',
password: '$2b$10$WQ22pIiwD8yDvRhdQ0olBe6JnnFqV2WOsC0cD/FkV4g7LPtUOpx1C',
__v: 35,
habits: [
{
_id: 6081d32580bfac579446eb81,
name: 'first',
type: 'good',
days: 0,
checked: false
},
{
_id: 6081d32f80bfac579446eb82,
name: 'seconds',
type: 'bad',
days: 0,
checked: false
},
]
}
From my client side, I send over a list of ids of the habits I want to delete out of the array, that looks like this..
[
'6081d32580bfac579446eb81',
'6081d32f80bfac579446eb82',
]
I am trying to find a way to delete the IDs in the habits array, deleting only the habits whose IDs are sent in the array above.
Here is what I have tried....
router.post('/delete', validate, async (req, res) =>{
const user = await User.findById(req.user._id)
const idList = req.body.ids.checkedItems
const updatedList = user['habits'].filter(habit=> {
return !idList.includes(`${habit._id}`)
})
user['habits'] = updatedList;
try {
await user.save()
res.send(updatedList)
} catch (err) {
res.status(400).send(err)
}
})
-idList is the array of ids as strings.
-user['habits'] accesses the list from the document, user.
-my filter method only returns the habits that are NOT included in the idList array. Because the Ids in the array are the ones to be deleted.
This solution is obviously just vanilla javascript, what I am looking for is if anyone knows how to achieve this using mongoose.js syntax.
I think you could do this by using deleteMany or deleteOne on the Model, but I am not sure how to achieve this.
Thank you for taking the time to help or give suggestions.
The solution that worked for me in the end is to use the Model method 'findByIdAndUpdate'.
const { itemsToDelete } = req.body
User.findByIdAndUpdate(req.user._id,
{ $pull: { habits: { _id: itemsToDelete } } },
{ new: true , useFindAndModify: false},
function (err, data) {
if (err) {
res.send(err)
} else {
res.send(data.habits)
}
}
)
You can use a combination of $pull and $in for this.
User.update({_id: userId},
{
$pull: {
habits: {
_id: {
$in: [
ObjectId("6081d32580bfac579446eb81"),
ObjectId("6081d32f80bfac579446eb82")
]
}
}
}
})

MongoDB Aggregate is not matching specific field

I'm new to Aggregation in MongoDB and I'm trying to understand the concepts of it by making examples.
I'm trying to paginate my subdocuments using aggregation but the returned document is always the overall values of all document's specific field.
I want to paginate my following field which contains an array of Object IDs.
I have this User Schema:
const UserSchema = new mongoose.Schema({
username: {
type: String,
unique: true,
required: true
},
firstname: String,
lastname: String,
following: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}],
...
}, { timestamps: true, toJSON: { virtuals: true }, toObject: { getters: true, virtuals: true } });
Without aggregation, I am able to paginate following,
I have this route which gets the user's post by their username
router.get(
'/v1/:username/following',
isAuthenticated,
async (req, res, next) => {
try {
const { username } = req.params;
const { offset: off } = req.query;
let offset = 0;
if (typeof off !== undefined && !isNaN(off)) offset = parseInt(off);
const limit = 2;
const skip = offset * limit;
const user = await User
.findOne({ username })
.populate({
path: 'following',
select: 'profilePicture username fullname',
options: {
skip,
limit,
}
})
res.status(200).send(user.following);
} catch (e) {
console.log(e);
res.status(500).send(e)
}
}
);
And my pagination version using aggregate:
const following = await User.aggregate([
{
$match: { username }
},
{
$lookup: {
'from': User.collection.name,
'let': { 'following': '$following' },
'pipeline': [
{
$project: {
'fullname': 1,
'username': 1,
'profilePicture': 1
}
}
],
'as': 'following'
},
}, {
$project: {
'_id': 0,
'following': {
$slice: ['$following', skip, limit]
}
}
}
]);
Suppose I have this documents:
[
{
_id: '5fdgffdgfdgdsfsdfsf',
username: 'gagi',
following: []
},
{
_id: '5fgjhkljvlkdsjfsldkf',
username: 'kuku',
following: []
},
{
_id: '76jghkdfhasjhfsdkf',
username: 'john',
following: ['5fdgffdgfdgdsfsdfsf', '5fgjhkljvlkdsjfsldkf']
},
]
And when I test my route for user john: /john/following, everything is fine but when I test for different user which doesn't have any following: /gagi/following, the returned result is the same as john's following which aggregate doesn't seem to match user by username.
/john/following | following: 2
/kuku/following | following: 0
Aggregate result:
[
{
_id: '5fdgffdgfdgdsfsdfsf',
username: 'kuku',
...
},
{
_id: '5fgjhkljvlkdsjfsldkf',
username: 'gagi',
...
}
]
I expect /kuku/following to return an empty array [] but the result is same as john's. Actually, all username I test return the same result.
I'm thinking that there must be wrong with my implementation since I've only started exploring aggregation.
Mongoose uses a DBRef to be able to populate the field after it has been retrieved.
DBRefs are only handled on the client side, MongoDB aggregation does not have any operators for handling those.
The reason that aggregation pipeline is returning all of the users is the lookup's pipeline does not have a match stage, so all of the documents in the collection are selected and included in the lookup.
The sample document there is showing an array of strings instead of DBRefs, which wouldn't work with populate.
Essentially, you must decide whether you want to use aggregation or populate to handle the join.
For populate, use the ref as shown in that sample schema.
For aggregate, store an array of ObjectId so you can use lookup to link with the _id field.

update some field on mongodb not all. How?

I'm still learning about Node.js & MongoDB so my question is:
on Update functionality would like to update some of the fields not necessary all on them So How i can do that ?
Note: i used the same validateTask function for insert new data to tasks document.
router.put('/:id', (req, res) => {
const { error } = validateTask(req.body);
if(error) return res.status(400).send(error.details[0].message);
Task.findByIdAndUpdate(req.params.id, {
name: req.body.name,
employee: req.body.employee,
description: req.body.description,
section: req.body.section,
status: req.body.status,
updated_at: new Date()
}, { new: true })
.then ( data => {
res.status(201).send(data);
}).catch(err => {
res.status(422).send('The task with given ID was not found');
});
});
function validateTask(task) {
const schema = {
name: Joi.string().min(3).required(),
employee: Joi.string().min(3).required(),
description: Joi.string().min(3).required(),
section: Joi.string().min(3).required(),
status: Joi.boolean()
};
return Joi.validate(task, schema);
}
Remove the validateTask validation
The error is because in validateTask function you have made all the fields as required and these paramters are validated before findByIdAndUpdate.
I believe You need not have any required(mandatory field) validation to be done in update. This validation can be only done for documents that are newly inserted
For that, you can use Task.findByIdAndUpdate it will only update the fields you give to for example
User: {
name: Ahmed Gamal
email: ahmed#ahmedgamal.ga
country: Egypt
}
When used with findByIdAndUpdate and given {name: Ahmed} the result will be
User: {
name: Ahmed
email: ahmed#ahmedgamal.ga
country: Egypt
}

sails js mongodb populate

I would like to use .populate() with Sails js and MongoDB and so I did something like this:
I am using the latest version of Sails and MongoDB and Node.
My models look like:
// User.js
module.exports = {
attributes: {
name: {
type: 'string'
},
pets: {
collection: 'pet',
via: 'owner'
}
}
};
// Pet.js
module.exports = {
attributes: {
name: {
type: 'string'
},
owner: {
model: 'user'
}
}
};
And I used the following which is the same as the documentation:
User.find()
.populate('pets')
.exec(function(err, users) {
if(err) return res.serverError(err);
res.json(users);
});
But I get the following output:
[
{
"pets": [],
"id": "58889ce5959841098d186b5a",
"firstName": "Foo",
"lastName": "Bar"
}
]
If you just need the query for dogs. You could just as easily reverse the query.
Pet.find() //or what you want with {}
.populate('users')
.exec(err, petsWithUsers)
But if not:
User.find()
.populate('pets')
.exec(err, users) ...
This will return all users (User.find()) and only populate pets of type dog (populate('pets', {type: 'dog'})). In the case you'll have users without dogs in your results.
More information and Reference: here

Mongoose return array $size

I have a document :
{
_id: 98556a665626a95a655a,
first_name: 'Panda',
last_name: 'Panda,
notifications: [{
_id: '',
// ...
}]
}
I want to return the following response :
{
_id: 98556a665626a95a655a,
first_name: 'Panda',
last_name: 'Panda',
notifications: 2
}
The problem is about notifications count field,
I used Mongoose NodeJS package and I tried the following :
UserDBModel.findOne({_id: uid}, {notifications: {$size: '$notifications'}}, function(err, user){ });
But it seems to not work.
Someone can help me ? :)
Thanks in advance.
Use aggregate with a project pipeline operator.
UserDBModel.aggregate()
.match({_id: uid})
.project({
first_name: 1,
last_name: 1,
notifications: {$size:"$notifications"}
})
.exec(function(err, notifications) {
// notifications will be an array, you probably want notifications[0]
});
Note that you will have to explicitly specify the fields for the project operator.
Maybe, you could do something like this in your node.
UserDBModel.findOne({ '_id': uid }, 'notifications', function (err, notifications) {
if (err) return handleError(err);
console.log(notifications.length);
})
Since you are using JS anyways maybe use its power! :)
What I found to work for me is creating a mongoose virtual attribute.
Schema.virtual('notifications_count').get(function() {
if (this.notifications) {
return this.notifications.length;
}
});

Categories

Resources