How can change my field to hold an array of objects? - node.js

I am using mongodb to store the data.
I have a field "campus" to hold the ObjectIds of Campus data.
Intially the user had a single campus, but later on the type of campus had to be modified to hold an array of campus. But the initial records remained to be the same.
Now while I'm trying to push a new campusId, some records are not updating because of type change.
I have some 500 records in which some records have single objectId and in some records the campus is an array of ObjectIds.
Like this,
"campusIds" : ObjectId("5c8f304a4350a990f7c40")
And,
"campusIds" : [
ObjectId("5d136de2a02b2c14947652"),
ObjectId("5d136f0da02b2c14947656"),
ObjectId("5d137082a02b2c14947658"),
ObjectId("5d14520da02b2c1494765c"),
ObjectId("5d1459dd81801aef0865a8") ]
I cannot manaully change the records in mongodb as the records in large number.
Is there a way I can change the field to an array in my db?

Create a query which only filters documents where the field exists and it's of ObjectId type ($type 7). The results from this query can then be used to do a bulk update on the collection.
The following async operation does the above:
(async () => {
try {
const docs = await db.collection('collectionName').find({
'campus': {
'$exists': true,
'$type': 7
}
}).toArray()
const ops = docs.map(({ _id, campus }) => ({
'updateOne': {
'filter': { _id } ,
'update': { '$set': { 'campusIds': [campus] } }
}
}))
db.collection('collectionName').bulkWrite(ops)
} catch(err) {
// handle error
}
})()

Related

Firestore Query Nested Map

Looking to construct a query against a firestore collection ('parent') where the documents have a nested map (2 logical levels deep). Specifically when the first map has dynamic keys which are not known at the time of running the query. As an example:
Document 1
{
codes: {
abc: {
id: 'hi'
},
def: {
id: 'there'
}
}
}
Document 2
{
codes: {
ghi: {
id: 'you'
},
zmp: {
id: 'guys'
}
}
}
What I would like to do is have a WHERE clause that takes a wildcard for a key in the document. ie.
firestore.collection('parent').WHERE('codes.*.id', '==', 'there')
// Results in Document 1
or
firestore.collection('parent').WHERE('codes.*.id', '==', 'you')
// Results in Document 2
Is there any way to achieve this behavior without having to resort to generating subcollection documents to be used for indexing, or polluting the document itself with a second map that maps ids to codes.
== Not ideal solution 1 (subcollections) ==
Build out the server so that when these documents are filed, a subcollection ('child') is maintained with documents that contain the related information. As an example filing Document 1 above would require filing two documents in the child subcollection:
{
id: 'hi'
code: 'abc'
}
{
id: 'there'
code: 'def'
}
Now we can query for the id we want, and get the parent reference, and follow that all the way back to the parent...
firestore.collectionGroup('child').where('id', '==', 'there')
.get()
.then(snapshot => {
for(const doc of snapshot.docs) {
return doc.ref.parent.parent
}
return Promise.reject('no parents, how sad.')
})
.then(ref => ref.get())
.then(snapshot => snapshot.data())
.then(parent => {
// Thank goodness, the parent is Document 1!
}
The downside to this is maintenance of the sub collections, as well as a number of extra operations against firestore.
== Not ideal solution 2 (model pollution) ==
Another way to achieve this is to implement another map or an array in the document itself which simply contains the ids which would then let us query on those values. ie
{
codes: {
abc: {
id: 'hi'
},
def: {
id: 'there'
}
},
codeids:['hi','there']
}
Although this is easy to query:
.WHERE('codeids', 'ARRAY CONTAINS', 'hi')
I don't like the idea of adding fields that are not meaningful to the consumer of the document (the purpose of the field only being to facilitate a documents ability to be queried due to system constraints)
Open to suggestions!

Mongoose bulk update

I want to be able to update an array of objects where each object has a new unique value assigned to it.
Here is a simplified example of what I'm doing. items is an array of my collection items.
let items = [{_id: '903040349304', number: 55}, {_id: '12341244', number: 1166}, {_id: '667554', number: 51115}]
I want to assign a new number to each item, and then update it in collection:
items = items.map(item => {
item.number = randomInt(0, 1000000);
return item;
})
What would be the best way to update the collection at once? I know that I could do it in forEach instead of map, how ever this seems as a dirty way of doing it, as it won't do the bulk update.
items.forEach(async (item) => {
await this.itemModel.update({_id: item._id}, {number: randomInt(0, 1000000)})
});
I've checked the updateMany as well but my understanding of it is that it's only used to update the documents with a same new value - not like in my case, that every document has a new unique value assigned to it.
After a bit of thinking, I came up with this solution using bulkWrite.
const updateQueries = [];
items.forEach(async (item) => {
updateQueries.push({
updateOne: {
filter: { _id: item._id },
update: { number: item.number },
},
});
});
await this.itemModel.bulkWrite(updateQueries);
About bulkWrite
Sends multiple insertOne, updateOne, updateMany, replaceOne,
deleteOne, and/or deleteMany operations to the MongoDB server in one
command. This is faster than sending multiple independent operations
(like) if you use create()) because with bulkWrite() there is only one
round trip to MongoDB.
You can call an aggregate() to instantly update them without needing to pull them first:
Step1: get a random number with mongoDb build in $rand option which returns a number between 0 and 1
Step2: $multiply this number by 1000000 since that is what you defined ;)
Step3: use another $set with $floor to remove the decimal portion
YourModel.aggregate([
{
'$set': {
'value': {
'$multiply': [
{
'$rand': {}
}, 1000000
]
}
}
}, {
'$set': {
'value': {
'$floor': '$value'
}
}
}
])
Here a picture of how that looks in mongo Compass as a proof of it working:

Search in array child firestore

How can I retrieve all the documents who match a child in a data structure like this:
{
[
id: {
name: "name",
products: {
items: [
productName: "this is the product Name"
]
}
}
]
}
The parameter i try to compare is the one inside products.items[0].productName.
this is how i tried but it does not retrieve anything:
try{
var data = [];
const byName = await dbRef.where('producto.items[0].producto', '==', req.params.nombre).get();
console.log(byName);
if (byName.empty) {
console.log('No matching documents.');
res.send('No matching documents.');
return;
}
byName.forEach(doc => {
console.log(doc.id, '=>', doc.data());
data.push(doc.data());
});
res.send(data);
}catch(err){
res.send(err);
}
If you want to search across all items in the items array for one that matches the value you have, you can use the array-contains operator:
dbRef.where('producto.items', 'array-contains', { producto: req.params.nombre})
But note that this only works if the array only contains the producto field in each item. The reason is that array-contains (and other array-level operators) work on complete items only.
So if the items in producto.items have multiple subfields, and you want to match on one/some of them, you can't use array-contains. In that case, you're options are:
Store the items names in a separate/additional array field product-names and then query on that with array-contains.
Store the array items in a subcollection and query that.
Use a map instead of an array to store these values. This will generate many extra indexes though, which both adds to your storage cost, and may get you to the limit on the number of indexes.

Unable to perform operations over Mongoose result

Product
.findAll()
.then((products) => {
/* Perform operations */
}
The above query returns an array of products. For eg,
[
{
name: Mobile,
price: 10000
},
{
name: Laptop,
price: 20000
},
]
I need to make some changes to the products array (add new fields based on the values of existing fields, and delete those existing fields). I tried few methods, but none are working:
products.forEach((product) => {
product.[updatedPrice] = updatedPrice;
delete product[price];
}
Array.map() is also not working.
The following works, but I don't know the working behind, and why it is happening. Also, how to delete a field using the same.
products.forEach((product) => {
product.set('updatedPrice', updatedPrice, {strict: false})
}
The thing here to note is that in .then((products) products object here is not a JSON object but a Mongoose Document object and the set method you are using is defined by the mongoose. You can refer it from here https://mongoosejs.com/docs/guide.html#strict
We can do 2 things:
use lean() (returns a plain js object)
Product.findAll().lean().then((products) => {
/* Perform operations*/
});
use toJSON() method on products object to convert mongoose object to js object
Product.findAll().then((products) => {
products = products.toJSON();
/* Perform operations*/
});
Thanks

Cloud firestore trigger query by documentID for array of ids

I am trying to write a transaction that first query documents by documentId from a list of ids, then makes some updates.
I am getting the error:
The corresponding value for FieldPath.documentId() must be a string or a DocumentReference.
For example:
const indexArray = [..list of doc ids...]
const personQueryRef = db.collection("person").where(admin.firestore.FieldPath.documentId(), "in", indexArray)
return db.runTransaction(transaction => {
return transaction.get(personQueryRef).then(personQuery => {
return personQuery.forEach(personRef => {
transaction.update(personRef, { ...update values here })
//more updates etc
})
})
})
I am wanting to do this in an onCreate and onUpdate trigger. Is there another approach I should be taking?
Update
The error still persists when not using a transaction, so this is unrelated to the problem.
The problem does not occur when the query is .where(admin.firestore.FieldPath.documentId(), "==", "just_one_doc_id"). So, the problem is with using FieldPath.documentId() and in.
It sounds like the type of query you're trying to do just isn't supported by the SDK. Whether or not that's intentional, I don't know. But if you want to transact with multiple documents, and you already know all of their IDs, you can use getAll(...) instead:
// build an array of DocumentReference objects
cost refs = indexArray.map(id => db.collection("person").doc(id))
return db.runTransaction(transaction => {
// pass the array to getAll()
return transaction.getAll(refs).then(docs => {
docs.forEach(doc => {
transaction.update(doc.ref, { ...update values here })
})
})
})

Resources