Using updateOne method to update an object field inside array - throws error "Cannot create field 'url' in element" - node.js

I have MongoDB database (with Mongoose) containing a collection of Products (among others), which looks like this:
[
{
name: 'Product A',
url: 'product-a',
category: 'accesory',
price: 12,
shortDescription: ['example description'],
technicalSpecs: [{ speed: 10, weight: 20 }],
images: [],
reviews: [],
relatedProducts: [
{
url: 'product-b',
name: 'Product B',
// to be added in Update query
//id: id_of_related_product
}
]
} /* other Product objects */
]
As every MongoDB document is provided with _id property by default, but within the relatedProducts array i only have url and name properties, i want to add the id property (associated with corresponding Product) for each object in the relatedProducts array, so i will be able to conveniently query and process those related products.
I came up with an idea to query all Products to get only those, which have non-empty relatedProducts array. Then i loop them and i search for Product model, which has specific url and name properties - this let's me get it's true (added by MongoDB) _id. At the end i want to add this _id to matching object inside relatedProducts array.
My code:
async function assignIDsToRelatedProducts(/* Model constructor */ Product) {
const productsWithRelatedOnes = await Product.find(
{ relatedProducts: { $ne: [] }}, ['relatedProducts', 'name', 'url']
);
for (const productItem of productsWithRelatedOnes) {
for (const relatedProduct of productItem.relatedProducts) {
const product = await Product.findOne(
{ url: relatedProduct.url, name: relatedProduct.name },
'_id'
);
// throws error
await productItem.updateOne(
{ 'relatedProducts.url': relatedProduct.url },
{ $set: { 'relatedProducts.$.id': product._id } }
);
}
}
}
However it throws the following error:
MongoError: Cannot create field 'url' in element {relatedProducts: [ /* array's objects here */ ]}
I don't know why MongoDB tries to create field 'url', as i use it to project/query url field (not create it) in updateOne method. How to fix this?
And - as i am newbie to MongoDB - is there a simpler way of achieving my goal? I feel that those two nested for..of loops are unnecessary, or even preceding creation of productsWithRelatedOnes variable is.
Is it possible to do with Mongoose Virtuals? I have tried it, but i couldn't match virtual property within the same Product Model - attach it to each object in relatedProducts array - after calling .execPopulate i received either an empty array or undefined (i am aware i should post at-the-time code of using Virtual, but for now i switched to above solution).

Although i didn't find solution or even reason of my problem, i solved it with a slightly other approach:
async function assignIDsToRelatedProducts(Product) {
const productsHavingRelatedProducts = Product.find({ relatedProducts: { $ne: [] }});
for await (const withRelated of productsHavingRelatedProducts) {
for (const relatedProductToUpdate of withRelated.relatedProducts) {
const relatedProduct = await Product
.findOne(
{ url: relatedProductToUpdate.url, name: relatedProductToUpdate.name },
['url', '_id']
);
await Product.updateMany(
{ 'relatedProducts.url': relatedProduct.url },
{ $set: { 'relatedProducts.$.id': relatedProduct._id } }
);
}
}
const amountOfAllProducts = await Product.find({}).countDocuments();
const amountOfRelatedProductsWithID = await Product
.find({ 'relatedProducts.id': { $exists: true } }).countDocuments();
console.log('All done?', amountOfAllProducts === amountOfRelatedProductsWithID);
}
Yet, i still suppose it can be done more concisely, without the initial looping. Hopefully somebody will suggest better solution. :)

Related

Unable to select specific field for MongoDB find operation

I am trying to select only one field from a mongo document and print the value for it. I found this answer https://stackoverflow.com/a/25589150 which showed how we can achieve this. Below I have tried doing the same yet the entire document ends up getting printed.
const mongoHost =
'somemongourl'
const mongodb = require('mongodb');
const { MongoClient } = mongodb;
MongoClient.connect(
mongoHost,
{ useNewUrlParser: true },
async (error, client) => {
if (error) {
return console.log('Unable to connect to database!');
}
const db = client.db('cartDatabase');
const values = await db
.collection('cart')
.find({ customer_key: 'c_1' }, { customer_key: 1, _id: 0 })
.toArray();
console.log(values);
}
);
This is the output for example I got :-
[
{
_id: new ObjectId("611b7d1a848f7e6daba69014"),
customer_key: 'c_1',
products: [ [Object] ],
coupon: '',
discount: 0,
vat: 0,
cart_total: 999.5,
cart_subtotal: 999.5
}
]
This is what I was expecting -
[
{
customer_key: 'c_1'
}
]
The standard Node.js MongoDB driver requires a top-level projection property for the options parameter if you wish to project your documents. This would result in the second parameter of your find() call looking like this:
{ projection: { customer_key: 1, _id: 0 } }
This is indicated in the Node.js MongoDB driver API documentation, which is notably not a 1-to-1 match with the MongoDB shell API.
As of the time of this answer, you could find the collection.find() reference here. This reference shows the following method signature (again as of when this answer was written):
find(filter: Filter<WithId<TSchema>>, options?: FindOptions<Document>)
Following the FindOptions parameter takes us to this reference page, which details the various top-level options properties available for the find() method. Among these is the projection property in question.
In short, don't use the normal MongoDB documentation as a reference for your programming language's MongoDB driver API. There will often be disconnects between the two.

Check data exist or not when inserting multiple data in array with knex.js

For now, I insert the product first and insert multiple product tags in array.
But the product tags will be duplicate without validating the tag is existed or not.
How could I check tag is existed or not when I insert the tags?
Here is the code I used:
let product = {
product: {
name: 'techtech',
tagline: '描述',
thumbnail: 'images/products/techtech/thumbnail.png'
},
tags: [
{ name: 'Android', slug: 'android' },
{ name: 'MacOS', slug: 'macos' },
{ name: 'Windows', slug: 'windows' },
{ name: 'Linux', slug: 'linux' }
],
links: [ { url: 'techtech.cc' } ],
covers: [
{ url: 'images/products/techtech/cover_0.png', index: 0 },
{ url: 'images/products/techtech/cover_1.png', index: 1 }
]
}
// insert product
return db.transaction(trx => {
return trx('products')
.insert(product.product)
.returning('id')
// insert product_tags
.then(productIDs => {
return trx(productTags)
.insert(product.tags)
.returning('id')
// insert product_producttag_mapping
.then(productTagIDs => {
let productTags = productTagIDs.map((productTagID) => {
let productTag = {
product_id: productID,
producttag_id: productTagID
}
return productTag;
});
return trx('product_producttag_mapping')
.insert(productTags)
.returning('id')
})
})
Be careful, when you insert the product, your variable is productIDs, but you are creating productTag useing productId. You might want to add const productId = productIds[0].
When creating the productTag, you also need to use .id, like the following. Because knex is returning you objects with the property.
let productTag = {
product_id: productID.id,
producttag_id: productTagID.id
}
but to your actual question: What about making the tag name unique? This would give you a nice error on insert. Without transaction you could just ignore that error. Postgres and Mysql have a nice feature ON CONFLICT DO NOTHING. I am not sure if the databases would return the existing id when they detect a duplication. So I think you do not get around reading the existing tags. Find them by their name. With an index on that column, it should be reasonable fast.
pro tip: when rewriting your code using async/await, the small mistakes become more obvious and you can avoid the callback hell. You will less likely be tempted to avoid an extra query.

Mongoose: updateMany() is not working as expected

I'm using mongoose to handle my DB queries. I'm trying to update a set of records entirely using this method. Mode code looks like this:
// prepare database query
const filter = { type: 'company' };
const update = req.body.payload; // payload contains the array of objects (i.e. updated records)
const options = { new: true, runValidators: true }
// find and update the taxonomy record
await Taxonomy.updateMany(filter, update, options);
But whenever I run this query I'm getting following error in the console:
Error [MongooseError]: Invalid update pipeline operator: "_id"
I suppose there is something wrong in my update payload. The req.body.payload looks like this:
[
{
_id: '5ef3d08c745428001d92f896',
type: 'company',
name: 'Company Size',
__v: 0
},
{
_id: '5ef3cdc5745428001d92f893',
type: 'company',
name: 'Company Industry',
__v: 0
}
]
Can you please tell me what actually is wrong here?
This is not the right usage of updateMany() - it is aimed to update many documents with a single change.
To update many documents use bulkwrite() (docs) :
async function myUpdateMany(Model, objectsArray) {
try {
let ops = []
for (let obj of (objectsArray || [])) {
ops.push({
updateOne: {
filter: { platformId: obj.platformId },
update: obj,
upsert: false, // set "true" if you want to add a new document if it doesn't exist
}
})
}
Model.bulkWrite(ops, { ordered: false });
} catch (err) {
throw Error("myUpdateMany error: " + err)
}
}
Regarding runValidators, according to this, it seems to work by default.

mongoose find with populate after where clause not working

I m not able to populate and find the specify category post in mongoose. It alway return me all post, it does not filter the post with specifying the category name, in searchQuery.category I m passing the searched category.
var query = posts.find();
if (searchQuery.tags) {
searchQuery instanceof Array ?
query.where('tags').in(searchQuery.tags) :
query.where('tags').equals(searchQuery.tags);
}
if (searchQuery.publish) {
query.where('publish').equals(true);
}
if (searchQuery.category) {
query.populate({
path: 'category',
match: { name: searchQuery.category },
model :'Category'
});
}
if (searchQuery.series) {
query.populate({
path: 'series',
match: { name: searchQuery.series }
});
}
return countPost(searchQuery).then(function (count) {
return query.select('title heading tags description blogImage createAt')
.skip(((parseInt(pageNo, 10) || 1) - 1) * parseInt(limit, 10))
.limit(parseInt(limit, 10) || 10)
.exec()
.then(function (posts) {
return { post: posts, total: count, currentPage: pageNo, limit: limit };
});
});
Here my code, it does not evaluate the category.
this code give me filtered record
query.populate({
path: 'category',
match: { name: searchQuery.category },
model :'Category'
}).exec(function(err, post){ console.log(post)}); // this query return only searchedQuery object.
The .populate method works a bit differently.
It does not filter the original collection. Instead it applies the query you pass to the collection that is populated. If the query applies it populates. Else - sets null instead of populated document.
So disregarding this match: { name: searchQuery.series } you will still receive all the posts.
There are 2 ways of solving this.
Get all the posts and filter out where category is null (not populated)
Use aggregation with $lookup operator and populate at DB level

mongoose recursive populate

I have been searching for a while and I didn't find any good answer. I have n-deep tree that I am storing in DB and I would like to populate all parents so in the end I get the full tree
node
-parent
-parent
.
.
-parent
So far I populate to level 2, and as I mentioned I need to get to level n.
Node.find().populate('parent').exec(function (err, items) {
if (!err) {
Node.populate(items, {path: 'parent.parent'}, function (err, data) {
return res.send(data);
});
} else {
res.statusCode = code;
return res.send(err.message);
}
});
you can do this now (with https://www.mongodb.com/blog/post/introducing-version-40-mongoose-nodejs-odm)
var mongoose = require('mongoose');
// mongoose.Promise = require('bluebird'); // it should work with native Promise
mongoose.connect('mongodb://......');
var NodeSchema = new mongoose.Schema({
children: [{type: mongoose.Schema.Types.ObjectId, ref: 'Node'}],
name: String
});
var autoPopulateChildren = function(next) {
this.populate('children');
next();
};
NodeSchema
.pre('findOne', autoPopulateChildren)
.pre('find', autoPopulateChildren)
var Node = mongoose.model('Node', NodeSchema)
var root=new Node({name:'1'})
var header=new Node({name:'2'})
var main=new Node({name:'3'})
var foo=new Node({name:'foo'})
var bar=new Node({name:'bar'})
root.children=[header, main]
main.children=[foo, bar]
Node.remove({})
.then(Promise.all([foo, bar, header, main, root].map(p=>p.save())))
.then(_=>Node.findOne({name:'1'}))
.then(r=>console.log(r.children[1].children[0].name)) // foo
simple alternative, without Mongoose:
function upsert(coll, o){ // takes object returns ids inserted
if (o.children){
return Promise.all(o.children.map(i=>upsert(coll,i)))
.then(children=>Object.assign(o, {children})) // replace the objects children by their mongo ids
.then(o=>coll.insertOne(o))
.then(r=>r.insertedId);
} else {
return coll.insertOne(o)
.then(r=>r.insertedId);
}
}
var root = {
name: '1',
children: [
{
name: '2'
},
{
name: '3',
children: [
{
name: 'foo'
},
{
name: 'bar'
}
]
}
]
}
upsert(mycoll, root)
const populateChildren = (coll, _id) => // takes a collection and a document id and returns this document fully nested with its children
coll.findOne({_id})
.then(function(o){
if (!o.children) return o;
return Promise.all(o.children.map(i=>populateChildren(coll,i)))
.then(children=>Object.assign(o, {children}))
});
const populateParents = (coll, _id) => // takes a collection and a document id and returns this document fully nested with its parents, that's more what OP wanted
coll.findOne({_id})
.then(function(o){
if (!o.parent) return o;
return populateParents(coll, o.parent))) // o.parent should be an id
.then(parent => Object.assign(o, {parent})) // replace that id with the document
});
Another approach is to take advantage of the fact that Model.populate() returns a promise, and that you can fulfill a promise with another promise.
You can recursively populate the node in question via:
Node.findOne({ "_id": req.params.id }, function(err, node) {
populateParents(node).then(function(){
// Do something with node
});
});
populateParents could look like the following:
var Promise = require('bluebird');
function populateParents(node) {
return Node.populate(node, { path: "parent" }).then(function(node) {
return node.parent ? populateParents(node.parent) : Promise.fulfill(node);
});
}
It's not the most performant approach, but if your N is small this would work.
Now with Mongoose 4 this can be done. Now you can recurse deeper than a single level.
Example
User.findOne({ userId: userId })
.populate({
path: 'enrollments.course',
populate: {
path: 'playlists',
model: 'Playlist',
populate: {
path: 'videos',
model: 'Video'
}
}
})
.populate('degrees')
.exec()
You can find the official documentation for Mongoose Deep Populate from here.
Just don't :)
There is no good way to do that. Even if you do some map-reduce, it will have terrible performance and problems with sharding if you have it or will ever need it.
Mongo as NoSQL database is really great for storing tree documents. You can store whole tree and then use map-reduce to get some particular leafs from it if you don't have a lot of "find particular leaf" queries. If this doesn't work for you, go with two collections:
Simplified tree structure: {_id: "tree1", tree: {1: [2, {3: [4, {5: 6}, 7]}]}}. Numbers are just IDs of nodes. This way you'll get whole document in one query. Then you just extract all ids and run second query.
Nodes: {_id: 1, data: "something"}, {_id: 2, data: "something else"}.
Then you can write simple recurring function which will replace node ids from first collection with data from second. 2 queries and simple client-side processing.
Small update:
You can extend second collection to be a little more flexible:
{_id: 2, data: "something", children:[3, 7], parents: [1, 12, 13]}
This way you'll be able to start your search from any leaf. And then, use map-reduce to get to the top or to the bottom of this part of tree.
This is a more straight forward approach to caub's answer and great solution. I found it a bit hard to make sense of at first so I put this version together.
Important, you need both 'findOne' and 'find' middleware hooks in place for this solution to work. *
* Also, the model definition must come after the middleware definition *
const mongoose = require('mongoose');
const NodeSchema = new mongoose.Schema({
children: [mongoose.Schema.Types.ObjectId],
name: String
});
const autoPopulateChildren = function (next) {
this.populate('children');
next();
};
NodeSchema
.pre('findOne', autoPopulateChildren)
.pre('find', autoPopulateChildren)
const Node = mongoose.model('Node', NodeSchema)
const root = new Node({ name: '1' })
const main = new Node({ name: '3' })
const foo = new Node({ name: 'foo' })
root.children = [main]
main.children = [foo]
mongoose.connect('mongodb://localhost:27017/try', { useNewUrlParser: true }, async () => {
await Node.remove({});
await foo.save();
await main.save();
await root.save();
const result = await Node.findOne({ name: '1' });
console.log(result.children[0].children[0].name);
});
I tried #fzembow's solution but it seemed to return the object from the deepest populated path. In my case I needed to recursively populate an object, but then return the very same object. I did it like that:
// Schema definition
const NodeSchema = new Schema({
name: { type: String, unique: true, required: true },
parent: { type: Schema.Types.ObjectId, ref: 'Node' },
});
const Node = mongoose.model('Node', NodeSchema);
// method
const Promise = require('bluebird');
const recursivelyPopulatePath = (entry, path) => {
if (entry[path]) {
return Node.findById(entry[path])
.then((foundPath) => {
return recursivelyPopulatePath(foundPath, path)
.then((populatedFoundPath) => {
entry[path] = populatedFoundPath;
return Promise.resolve(entry);
});
});
}
return Promise.resolve(entry);
};
//sample usage
Node.findOne({ name: 'someName' })
.then((category) => {
if (category) {
recursivelyPopulatePath(category, 'parent')
.then((populatedNode) => {
// ^^^^^^^^^^^^^^^^^ here is your object but populated recursively
});
} else {
...
}
})
Beware it's not very efficient. If you need to run such query often or at deep levels, then you should rethink your design
Maybe a lot late for that but mongoose has some documentation on this :
Ancestors Tree Array
Materialized Path Tree Array
I think the first one is more appropriate to you as you are looking to populate parents.
With that solution, you can with one regex query, search all the documents matching your designered output tree.
You would setup documents with this Schema :
Tree: {
name: String,
path: String
}
Paths field would be the absolute path in your tree :
/mens
/mens/shoes
/mens/shoes/boots
/womens
/womens/shoes
/womens/shoes/boots
For example you could search all the childrens of your node '/mens/shoes' with one query :
await Tree.find({ path: /^\/mens/shoes })
It would return all the documents where the path starts with /mens/shoes :
/mens/shoes
/mens/shoes/boots
Then you'd only need some client-side logic to arrange it in a tree structure (a map-reduce)

Resources