I have this structure in my Accounts Model:
{
cards: {
starter: [],
intermediate: [],
advanced: [ {Object}, {Object}, {Object} ]
},
firstName: 'Sepideh',
email: 'sepideh#example.com',
}
The Objects inside cards.advanced array above are like:
{
cards: [
{ // this is a single card object
cardTitle: 'this card is for you',
performance: [],
cardID: 32,
}
],
unit: 3 // this is the unit we want to push our new card object into
}
Assuming I can access the document above (or account in other words) with the given email:
const account = await db.Account.findOne({ email: 'sepideh#example.com' });
How can we push a new card object into the nested cards array with unit: 3 (yes so far this is the only unit) property and save Sepideh account like this:
{
cards: {
starter: [],
intermediate: [],
advanced: [
{
cards: [
{ // this is a single card object
cardTitle: 'this card is for you',
performance: [],
cardID: 32,
}
{ // this is new pushed card object
cardTitle: 'this card is for me',
performance: [],
cardID: 33,
}
],
unit: 3 // this is the unit we want to push our new card object into
},
{Object},
{Object}
] // end of advanced array
},
firstName: 'Sepideh',
email: 'sepideh#example.com',
}
I have tried to select the unit 3 of the document using these guys and none of them worked:
const userUnit = await account.findOne({'cards.advanced.unit': unit});
const userUnit = await account.findOne({'cards.advanced.unit': unit});
const userUnit = await account.find({}, {'cards.advanced.unit': {$elemMatch: {unit: unit}}});
const userUnit = await db.Account.findOne({ email: email, 'cards.advanced.unit': unit});
Then I would push my new card object to the userUnit and call await account.save();
The only thing which is working is pure JavaScript code like this to select:
const userUnit = account.cards.advanced.find(e => e.unit == unit);
This time I cannot save the changes to the database... (I don't know how to save it..)
How would you do this?
You can use the $push operator directly along with the $ positional operator which allows you to specify which sub-element should be updated:
await db.Account.update({ email: "sepideh#example.com", "cards.advanced.unit": 3 },
{ $push: { "cards.advanced.$.cards": { cardTitle: 'this card is for me', performance: [], cardID: 33 } } })
If you want your path to be evaluated dynamically you can use computed property names:
let path = "advanced";
await db.Account.update(
{ email: "sepideh#example.com", ["cards." + path + ".unit"]: 3 },
{ $push: { ["cards." + path + ".$.cards"]: { cardTitle: 'this card is for me', performance: [], cardID: 33 } } })
Mongo Playground
Related
Just a bit confused as to why this magic method is returning null. It's probably very simple, but I'm using methods I wouldn't normally (bulkingCreating) and can't currently see it.
Association: Country.hasOne(Capital, { foreignKey: 'countryId' });
Populating dummy data:
const countries = await Country.bulkCreate([
{ name: 'England' },
{ name: 'Spain' },
{ name: 'France' },
{ name: 'Canada' }
]);
const capitals = await Capital.bulkCreate([
{ name: 'London' },
{ name: 'Madrid'},
{ name: 'Paris' },
{ name: 'Ottawa' }
]);
countries.forEach((country, index) => {
country.setCapital(capitals[index]);
});
const country = await Country.findOne({where: {name: 'Spain'}});
console.log(country.name, Object.keys(country.__proto__)); // Spain / magic methods
const capital = await country.getCapital();
console.log(capital); // null
The table:
Am I wrong in thinking country.getCapital() should return the relevant entry?
As you might guess setCapital should be an async function because it makes changes in DB so you need to use for instead of forEach method that does not support async callbacks:
let index = 0;
for (const country of countries) {
await country.setCapital(capitals[index]);
index += 1;
}
It would be better to create countries one by one and create capitals for them not relying on the same indexes of both collections (DB might return created records in a different order).
If you are using Sequelize 5.14+, you can do this in 1 bulkCreate using include option.
const countries = await Country.bulkCreate([
{
name: 'England',
Capital: { // This keyname should be matching with model name.
name: 'London'
}
},
{
name: 'Spain',
Capital: {
name: 'Madrid'
}
},
...
],
{
include: Capital,
returning: true // If Postgres, add this if you want the created object to be returned.
}
);
Server Started at Port 3000...
{
_id: new ObjectId("61c707e9f4ff040a47d27c3f"),
username: 'adityaaryam',
password: '1234',
nameOfUser: 'Aditya Aryam',
emailOfUser: 'adityaaryam#gmail.com',
userAllLists: [
{
name: 'Hello',
items: [],
_id: new ObjectId("61c70d915448262d1dca1a69")
},
{
name: 'Work',
items: [],
_id: new ObjectId("61c70d965448262d1dca1a70")
},
{
name: 'Home Work',
items: [],
_id: new ObjectId("61c70d9b5448262d1dca1a79")
},
{
name: 'Hello',
items: [],
_id: new ObjectId("61c70e7f5448262d1dca1a84")
},
{
name: 'Play',
items: [],
_id: new ObjectId("61c7126a5448262d1dca1a9b")
},
{
name: 'Eat',
items: [],
_id: new ObjectId("61c71325b0219e6ce4f57990")
},
{
name: 'Walla',
items: [],
_id: new ObjectId("61c7197de9564390d506cbe9")
}
],
__v: 7
}
This is how my database looks like. I want to push new elements to "items" array which is nested inside the "userAllLists" array using mongoose. How do I implement this?
I have been trying findOneAndUpdate using $push but I am not able to achieve my desriable results.
My Schemas are as follows:
const itemSchema = {
name: String
};
const customListSchema ={
name:String,
items:[itemSchema]
};
const userSchema={
username: String,
password: String,
nameOfUser: String,
emailOfUser: String,
userAllLists: [customListSchema],
};
Thanks in Advance!
I think $push is the right way to push new elements to nested arrays, you didn't show the code you tried to see if it works or not, at all here is an example based on your schema
User.update({_id: "61c707e9f4ff040a47d27c3f", }, {
'$push': {
"userAllLists.$[].items": {name: "test item name"}
}
});
Note: $[] expressions will push the specified object inside all items arrays that exist in userAllLists
To push the item for only specific userAllLists object you can use the following syntax
User.update({_id: "61c707e9f4ff040a47d27c3f", "usersAllLists._id": "61c70d915448262d1dca1a69"}, {
'$push': {
"userAllLists.$.items": {name: "test item name"}
}
});
this will ensure to push the item object to the specified usersAllLists object which has this id 61c70d915448262d1dca1a69
I have this structure in my Mother Model (this is a fixed structure and I just push cards or update them on these 3 array levels):
{
cards: {
starter: [],
intermediate: [],
advanced: [ {Object}, {Object}, {Object} ]
},
}
The Objects inside cards.advanced array above are like:
{
cards: [
{ // this is a single card object
title: 'this is a card',
id: 'main-2-1' // this is unique id only in advanced array, we may have exact id for a card in starter or in intermediate array
}
],
unit: 2 // this is the unit
}
Assuming I have access to Mother model like this:
const motherModel = await db.Mother.findOne({}); // this retrieves all data in the Model
How can we update a card object based on its id and the level it belongs to and replace the whole card object with newCard ?
const level = 'advanced'; // the level of the card we want to search for
const cardID = 'main-2-1'; // the exact id of the card we want to be replaced
const cardUnit = cardID.split('-')[1]; // I can calculate this as the unit in which the card exist inside
const newCard = { // new card to be replaced
title: 'this is our new updated card',
id: 'main-2-1'
}
I have tried this with no luck:
const updated = await db.Mother.update(
{ ["cards." + level + ".unit"]: cardUnit },
{ ["cards." + level + ".$.cards"]: newCard }
)
I have tried this one too but it doesn't change anything in the Model:
async function updateMotherCard(card, level) {
const cardID = card.id;
const cardUnit = cardID.split('-')[1];
const motherModel = await db.Mother.findOne({});
const motherLevel = motherModel.cards[level];
const selectedUnit = motherLevel.find(e => e.unit == cardUnit);
let selectedCard = selectedUnit.cards.find(e => e.id == cardID);
selectedCard = card;
const updated = await motherModel.save();
console.log(updated);
}
You can actually sort your problem out with the update method, but you have to do it in a different way if you are using MongoDB 4.2 or later. The second parameter can be the $set operation you want to perform or an aggregation pipeline. Using the later you have more liberty shaping the data. This is the way you can solve your problem, I will breakdown after:
db.collection.update({
"cards.advanced.unit": 2
},
[
{
$set: {
"cards.advanced": {
$map: {
input: "$cards.advanced",
as: "adv",
in: {
cards: {
$map: {
input: "$$adv.cards",
as: "advcard",
in: {
$cond: [
{
$eq: [
"$$advcard.id",
"main-2-1"
]
},
{
title: "this is a NEW updated card",
id: "$$advcard.id"
},
"$$advcard"
]
}
}
},
unit: "$$adv.unit"
}
}
}
}
}
],
{
new: true,
});
First with use the update method passing three parameters:
Filter query
Aggregation pipeline
Options. Here I just used new: true to return the updated document and make it easier to test.
This is the structure:
db.collection.update({
"cards.advanced.unit": 2
},
[
// Pipeline
],
{
new: true,
});
Inside the pipeline we only need one stage, the $set to replace the property advanced with an array we will create.
...
[
{
$set: {
"cards.advanced": {
// Our first map
}
}
}
]
...
We first map the advanced array to be able to map the nested cards array after:
...
[
{
$set: {
"cards.advanced": {
$map: {
input: "$cards.advanced",
as: "adv",
in: {
// Here we will map the nested array
}
}
}
}
}
]
...
We use the variable we declared on the first map and which contains the advanced array current item being mapped ( adv ) to access and map the nested "cards" array ( $$adv.cards ):
...
[
{
$set: {
"cards.advanced": {
$map: {
input: "$cards.advanced",
as: "adv",
in: {
cards: {
$map: {
input: "$$adv.cards",
as: "advcard",
in: {
// We place our condition to check for the chosen card here
}
}
},
unit: "$$adv.unit",
}
}
}
}
}
]
...
Lastly we check if the current card id is equal to the id being searched $eq: [ "$$advcard.id", "main-2-1" ] and return the new card if it matches or the current card:
...
{
$cond: [
{
$eq: [
"$$advcard.id",
"main-2-1"
]
},
{
title: "this is a NEW updated card",
id: "$$advcard"
},
"$$advcard"
]
}
...
Here is a working example of what is described:
https://mongoplayground.net/p/xivZGNeD8ng
Hi everyone I have an array of objects with some populated fields. This is the schema of the product.
import mongoose, { Schema } from 'mongoose';
const productSchema = new mongoose.Schema(
{
name: String,
description: String,
sku: String,
barcode: String,
isActive: Boolean,
quantity: Number,
availability: String,
taxClass: [{ type: Schema.Types.ObjectId, ref: 'TaxClass' }],
images: [{ type: Schema.Types.ObjectId, ref: 'Image' }],
variants: [{ type: Schema.Types.ObjectId, ref: 'Variant' }],
tags: [{ type: Schema.Types.ObjectId, ref: 'Tag' }],
price: {
comparePrice: Number,
price: Number
},
seo: {
name: String,
keywords: [
{
name: String
}
],
description: String,
image: String
}
},
{ timestamps: true }
);
const Product = mongoose.model('Product', productSchema);
export default Product;
So i have a function and I want to return all the products with the variant color of green.
export const returnFilteredProducts = async (_, { filters = null, page = 1, limit = 20 }, context) => {
await jwtAuthentication.verifyTokenMiddleware(context);
try {
let searchQuery = {};
const products = await Product.find(searchQuery).populate(['variants', 'tags', 'images', 'taxClass']);
console.log(products.filter((item) => item.variants.filter((e) => e.color.indexOf('green') >= 0)));
return {
products
};
} catch (error) {
handleError(error);
}
};
The thing is that it does not return me the document with a variant color of green, instead it returns all the documents.
I am implementing a filtering system so I don't filter the products with in the frontend with redux.
Regarding the filtering method that is applied into the products array:
products.filter((item) => item.variants.filter((e) => e.color.indexOf('green') >= 0))
Inner call item.variants.filter() returns an array.
The outer call: products.filter() will include the product item, since the array will coerce into true, even when empty.
You can use method Array.some() for the inner call,
which will return a boolean true if at least one item (e) in item.variants
has the desired color.
This way you will filter-out all the product items that do-not contain the desired color in at least one element of the item.variants array.
general programming problem here.
I have this array called SPACES
[
{
_id: 5e1c4689429a8a0decf16f69,
challengers: [
5dfa24dce9cbc0180fb60226,
5dfa26f46719311869ac1756,
5dfa270c6719311869ac1757
]
},
{
_id: 5e1c4eb9c9461510407d5e81,
challengers: [ 5dfa24dce9cbc0180fb60226, 5dfa26f46719311869ac1756 ],
}
]
And this array called USERS
[
{
_id: 5dfa24dce9cbc0180fb60226,
name: 'Account 1',
email: 'account1#gmail.com',
spaces: [ 5e1c4689429a8a0decf16f69, 5e1c4eb9c9461510407d5e81 ],
},
{
_id: 5dfa26f46719311869ac1756,
name: 'Account 2',
email: 'account2#gmail.com',
spaces: [ 5e1c4689429a8a0decf16f69, 5e1c4eb9c9461510407d5e81 ]
},
{
_id: 5dfa270c6719311869ac1757,
name: 'Account 3',
email: 'account3#gmail.com',
spaces: [ 5e1c4689429a8a0decf16f69 ]
}
]
What I want to do, is go through both, and instead of having the SPACES.challengers array be just IDS, I would like the array to contain each USER object.
So for example, if the USER has an ID that is inside the SPACES.challengers array, then push the user into that array (which will then be the entire object).
SO FAR I have tried this (I am not very good yet):
users.map( ( user ) => {
spaces.map( ( space ) => {
if ( user.spaces.includes( space._id ) ) {
space.challengers.push(user)
}
} );
} );
However, I am not getting inside the IF block. (Even if I did, not sure if it would work OR if this is even how to do it). It feels Odd doing double maps, as I get so many iterations, and it duplicates my push (cause I have no logic to see if it just has been pushed).
Assuming every entry in the Users array has a unique ID, we can build a Hashmap to store (id, index) pairs in order to search efficiently for an ID from Users array while looping through Spaces array.
let spaces = [{_id: '5e1c4689429a8a0decf16f69',challengers: ['5dfa24dce9cbc0180fb60226', '5dfa26f46719311869ac1756', '5dfa270c6719311869ac1757']},{_id: '5e1c4eb9c9461510407d5e81',challengers: [ '5dfa24dce9cbc0180fb60226', '5dfa26f46719311869ac1756' ],}]
let users = [{_id: '5dfa24dce9cbc0180fb60226',name: 'Account 1',email: 'account1#gmail.com',spaces: [ '5e1c4689429a8a0decf16f69', '5e1c4eb9c9461510407d5e81' ],},{_id: '5dfa26f46719311869ac1756',name: 'Account 2',email: 'account2#gmail.com',spaces: [ '5e1c4689429a8a0decf16f69', '5e1c4eb9c9461510407d5e81' ]},{_id: '5dfa270c6719311869ac1757',name: 'Account 3',email: 'account3#gmail.com',spaces: [ '5e1c4689429a8a0decf16f69' ]}]
let IDIndexMapping = {} // To store (_id, index) pairs, in order to improve search efficiency
for(let index in users) // Iterate through Users array using index
IDIndexMapping[users[index]._id] = index; // store (_id, index) pair in IDIndexMapping
// I'm avoiding using `map` and using vanilla `for` loop for space efficiency
// as map returns a new array but with `for` loop, we can perform changes in-place
for(let outerIndex in spaces){ // Iterate through `spaces` array using index
let challengers = spaces[outerIndex].challengers; // Get challengers array
for(let innerIndex in challengers){ // Iterate through challengers array using index
let ID = challengers[innerIndex]; // Get ID
if(ID in IDIndexMapping) // If ID exists in IDIndexMapping
spaces[outerIndex].challengers[innerIndex] = users[IDIndexMapping[ID]]; // Change ID to actual User object
}
}
console.log(spaces)
Output
[ { _id: '5e1c4689429a8a0decf16f69',
challengers: [ [Object], [Object], [Object] ] },
{ _id: '5e1c4eb9c9461510407d5e81',
challengers: [ [Object], [Object] ] } ]
.map and .find should work here. keep it simple.
var spaces = [
{
_id: "5e1c4689429a8a0decf16f69",
challengers: [
"5dfa24dce9cbc0180fb60226",
"5dfa26f46719311869ac1756",
"5dfa270c6719311869ac1757"
]
},
{
_id: "5e1c4eb9c9461510407d5e81",
challengers: ["5dfa24dce9cbc0180fb60226", "5dfa26f46719311869ac1756", "some non existent"]
}
],
users = [
{
_id: "5dfa24dce9cbc0180fb60226",
name: "Account 1",
email: "account1#gmail.com",
spaces: ["5e1c4689429a8a0decf16f69", "5e1c4eb9c9461510407d5e81"]
},
{
_id: "5dfa26f46719311869ac1756",
name: "Account 2",
email: "account2#gmail.com",
spaces: ["5e1c4689429a8a0decf16f69", "5e1c4eb9c9461510407d5e81"]
},
{
_id: "5dfa270c6719311869ac1757",
name: "Account 3",
email: "account3#gmail.com",
spaces: ["5e1c4689429a8a0decf16f69"]
}
],
result = spaces.map(({ _id, challengers }) => ({
_id,
challengers: challengers.map(challenger =>
users.find(user => user._id === challenger)
).filter(row => row)
}));
console.log(JSON.stringify(result, null, 2));
You can create a map of challengers for look-up and then put them in spaces.
//create user map for look-up
userMap = users.reduce((res, val) => ({
...res,
[val._id]: val
}), {});
//change challenger id with user object
inflatedSpaces = spaces.map(s => ({ ...s, challengers: s.challengers.map(c => userMap[c]) }));
You could map the users with a Map.
Beside the destructuring of the object for mapping this answer uses for this part
challengers: challengers.map(
Map.prototype.get, // cb with a prototype and using `this`
new Map(users.map(o => [o._id, o])) // thisArg
)
the above mentioned Map in two parts.
The lower part generates an instance of Map where _id of the users items is used as key and the whole object as value. This instance is uses as thisArg of Array#map, the second parameter.
The upper part is a prototype of Map, used as callback. And while an this object is supplied, a binding (Function#bind) is not necessary.
var spaces = [{ _id: '5e1c4689429a8a0decf16f69', challengers: ['5dfa24dce9cbc0180fb60226', '5dfa26f46719311869ac1756', '5dfa270c6719311869ac1757'] }, { _id: '5e1c4eb9c9461510407d5e81', challengers: ['5dfa24dce9cbc0180fb60226', '5dfa26f46719311869ac1756'] }],
users = [{ _id: '5dfa24dce9cbc0180fb60226', name: 'Account 1', email: 'account1#gmail.com', spaces: ['5e1c4689429a8a0decf16f69', '5e1c4eb9c9461510407d5e81'] }, { _id: '5dfa26f46719311869ac1756', name: 'Account 2', email: 'account2#gmail.com', spaces: ['5e1c4689429a8a0decf16f69', '5e1c4eb9c9461510407d5e81'] },{ _id: '5dfa270c6719311869ac1757', name: 'Account 3', email: 'account3#gmail.com', spaces: ['5e1c4689429a8a0decf16f69'] }],
result = spaces.map(({ _id, challengers }) => ({
_id,
challengers: challengers.map(
Map.prototype.get,
new Map(users.map(o => [o._id, o]))
)
}));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }