I'm currently trying Firestore, and I'm stuck at something very simple: "updating an array (aka a subdocument)".
My DB structure is super simple. For example:
proprietary: "John Doe",
sharedWith:
[
{who: "first#test.com", when:timestamp},
{who: "another#test.com", when:timestamp},
],
I'm trying (without success) to push new records into shareWith array of objects.
I've tried:
// With SET
firebase.firestore()
.collection('proprietary')
.doc(docID)
.set(
{ sharedWith: [{ who: "third#test.com", when: new Date() }] },
{ merge: true }
)
// With UPDATE
firebase.firestore()
.collection('proprietary')
.doc(docID)
.update({ sharedWith: [{ who: "third#test.com", when: new Date() }] })
None works. These queries overwrite my array.
The answer might be simple, but I could'nt find it...
Firestore now has two functions that allow you to update an array without re-writing the entire thing.
Link: https://firebase.google.com/docs/firestore/manage-data/add-data, specifically https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array
Update elements in an array
If your document contains an array field, you can use arrayUnion() and
arrayRemove() to add and remove elements. arrayUnion() adds elements
to an array but only elements not already present. arrayRemove()
removes all instances of each given element.
Edit 08/13/2018: There is now support for native array operations in Cloud Firestore. See Doug's answer below.
There is currently no way to update a single array element (or add/remove a single element) in Cloud Firestore.
This code here:
firebase.firestore()
.collection('proprietary')
.doc(docID)
.set(
{ sharedWith: [{ who: "third#test.com", when: new Date() }] },
{ merge: true }
)
This says to set the document at proprietary/docID such that sharedWith = [{ who: "third#test.com", when: new Date() } but to not affect any existing document properties. It's very similar to the update() call you provided however the set() call with create the document if it does not exist while the update() call will fail.
So you have two options to achieve what you want.
Option 1 - Set the whole array
Call set() with the entire contents of the array, which will require reading the current data from the DB first. If you're concerned about concurrent updates you can do all of this in a transaction.
Option 2 - Use a subcollection
You could make sharedWith a subcollection of the main document. Then
adding a single item would look like this:
firebase.firestore()
.collection('proprietary')
.doc(docID)
.collection('sharedWith')
.add({ who: "third#test.com", when: new Date() })
Of course this comes with new limitations. You would not be able to query
documents based on who they are shared with, nor would you be able to
get the doc and all of the sharedWith data in a single operation.
Here is the latest example from the Firestore documentation:
firebase.firestore.FieldValue.ArrayUnion
var washingtonRef = db.collection("cities").doc("DC");
// Atomically add a new region to the "regions" array field.
washingtonRef.update({
regions: firebase.firestore.FieldValue.arrayUnion("greater_virginia")
});
// Atomically remove a region from the "regions" array field.
washingtonRef.update({
regions: firebase.firestore.FieldValue.arrayRemove("east_coast")
});
You can use a transaction (https://firebase.google.com/docs/firestore/manage-data/transactions) to get the array, push onto it and then update the document:
const booking = { some: "data" };
const userRef = this.db.collection("users").doc(userId);
this.db.runTransaction(transaction => {
// This code may get re-run multiple times if there are conflicts.
return transaction.get(userRef).then(doc => {
if (!doc.data().bookings) {
transaction.set({
bookings: [booking]
});
} else {
const bookings = doc.data().bookings;
bookings.push(booking);
transaction.update(userRef, { bookings: bookings });
}
});
}).then(function () {
console.log("Transaction successfully committed!");
}).catch(function (error) {
console.log("Transaction failed: ", error);
});
Sorry Late to party but Firestore solved it way back in aug 2018 so If you still looking for that here it is all issues solved with regards to arrays.
https://firebase.googleblog.com/2018/08/better-arrays-in-cloud-firestore.htmlOfficial blog post
array-contains, arrayRemove, arrayUnion for checking, removing and updating arrays. Hope it helps.
To build on Sam Stern's answer, there is also a 3rd option which made things easier for me and that is using what Google call a Map, which is essentially a dictionary.
I think a dictionary is far better for the use case you're describing. I usually use arrays for stuff that isn't really updated too much, so they are more or less static. But for stuff that gets written a lot, specifically values that need to be updated for fields that are linked to something else in the database, dictionaries prove to be much easier to maintain and work with.
So for your specific case, the DB structure would look like this:
proprietary: "John Doe"
sharedWith:{
whoEmail1: {when: timestamp},
whoEmail2: {when: timestamp}
}
This will allow you to do the following:
var whoEmail = 'first#test.com';
var sharedObject = {};
sharedObject['sharedWith.' + whoEmail + '.when'] = new Date();
sharedObject['merge'] = true;
firebase.firestore()
.collection('proprietary')
.doc(docID)
.update(sharedObject);
The reason for defining the object as a variable is that using 'sharedWith.' + whoEmail + '.when' directly in the set method will result in an error, at least when using it in a Node.js cloud function.
#Edit (add explanation :) )
say you have an array you want to update your existing firestore document field with. You can use set(yourData, {merge: true} ) passing setOptions(second param in set function) with {merge: true} is must in order to merge the changes instead of overwriting. here is what the official documentation says about it
An options object that configures the behavior of set() calls in DocumentReference, WriteBatch, and Transaction. These calls can be configured to perform granular merges instead of overwriting the target documents in their entirety by providing a SetOptions with merge: true.
you can use this
const yourNewArray = [{who: "first#test.com", when:timestamp}
{who: "another#test.com", when:timestamp}]
collectionRef.doc(docId).set(
{
proprietary: "jhon",
sharedWith: firebase.firestore.FieldValue.arrayUnion(...yourNewArray),
},
{ merge: true },
);
hope this helps :)
addToCart(docId: string, prodId: string): Promise<void> {
return this.baseAngularFirestore.collection('carts').doc(docId).update({
products:
firestore.FieldValue.arrayUnion({
productId: prodId,
qty: 1
}),
});
}
i know this is really old, but to help people newbies with the issue
firebase V9 provides a solution using the arrayUnion and arrayRemove
await updateDoc(documentRef, {
proprietary: arrayUnion( { sharedWith: [{ who: "third#test.com", when: new Date() }] }
});
check this out for more explanation
Other than the answers mentioned above. This will do it.
Using Angular 5 and AngularFire2. or use firebase.firestore() instead of this.afs
// say you have have the following object and
// database structure as you mentioned in your post
data = { who: "third#test.com", when: new Date() };
...othercode
addSharedWith(data) {
const postDocRef = this.afs.collection('posts').doc('docID');
postDocRef.subscribe( post => {
// Grab the existing sharedWith Array
// If post.sharedWith doesn`t exsit initiated with empty array
const foo = { 'sharedWith' : post.sharedWith || []};
// Grab the existing sharedWith Array
foo['sharedWith'].push(data);
// pass updated to fireStore
postsDocRef.update(foo);
// using .set() will overwrite everything
// .update will only update existing values,
// so we initiated sharedWith with empty array
});
}
We can use arrayUnion({}) method to achive this.
Try this:
collectionRef.doc(ID).update({
sharedWith: admin.firestore.FieldValue.arrayUnion({
who: "first#test.com",
when: new Date()
})
});
Documentation can find here: https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array
Consider John Doe a document rather than a collection
Give it a collection of things and thingsSharedWithOthers
Then you can map and query John Doe's shared things in that parallel thingsSharedWithOthers collection.
proprietary: "John Doe"(a document)
things(collection of John's things documents)
thingsSharedWithOthers(collection of John's things being shared with others):
[thingId]:
{who: "first#test.com", when:timestamp}
{who: "another#test.com", when:timestamp}
then set thingsSharedWithOthers
firebase.firestore()
.collection('thingsSharedWithOthers')
.set(
{ [thingId]:{ who: "third#test.com", when: new Date() } },
{ merge: true }
)
If You want to Update an array in a firebase document.
You can do this.
var documentRef = db.collection("Your collection name").doc("Your doc name")
documentRef.update({
yourArrayName: firebase.firestore.FieldValue.arrayUnion("The Value you want to enter")});
Although firebase.firestore.FieldValue.arrayUnion() provides the solution for array update in firestore, at the same time it is required to use {merge:true}. If you do not use {merge:true} it will delete all other fields in the document while updating with the new value. Here is the working code for updating array without loosing data in the reference document with .set() method:
const docRef = firebase.firestore().collection("your_collection_name").doc("your_doc_id");
docRef.set({yourArrayField: firebase.firestore.FieldValue.arrayUnion("value_to_add")}, {merge:true});
If anybody is looking for Java firestore sdk solution to add items in array field:
List<String> list = java.util.Arrays.asList("A", "B");
Object[] fieldsToUpdate = list.toArray();
DocumentReference docRef = getCollection().document("docId");
docRef.update(fieldName, FieldValue.arrayUnion(fieldsToUpdate));
To delete items from array user: FieldValue.arrayRemove()
If the document contains a nested object in the form of an array, .dot notation can be used to reference and update nested fields.
Node.js example:
const users = {
name: 'Tom',
surname: 'Smith',
favorites: {
sport: 'tennis',
color: 'red',
subject: 'math'
}
};
const update = await db.collection('users').doc('Tom').update({
'favorites.sport': 'snowboard'
});
or Android sdk example:
db.collection("users").document("Tom")
.update(
'favorites.sport': 'snowboard'
);
There is a simple hack in firestore:
use path with "." as property name:
propertyname.arraysubname.${id}:
db.collection("collection")
.doc("docId")
.update({arrayOfObj: fieldValue.arrayUnion({...item})})
In this forEach I push some fields into an existing array.
How can i filter out where prop.isRequired = false?
So: (only) Loop everything in schema.properties, where isRequired = true;
angular.forEach(vm.schema.properties, function (prop,key) {
vm.mappingFields.push({ //this is an array
source: null, //this gets pushed
destination: key, //this gets pushed
fieldType: prop.type, //this gets pushed
isRequired: prop.isRequired, //this gets pushed
});
});
I'd do it in modern way like this:
vm.mappingFields = vm.schema.properties.filter({ isRequired } => isRequired).map(prop => {
source: null,
destination: key,
fieldType: prop.type,
isRequired: prop.isRequired
})
First we use ES6 Array.filter method then just Array.map to generate new array with needed fields and assign new generated array into vm.mappingFields.
Also I used ES6 Destructuring { isRequired } => isRequired to reduce code (prop => prop.isRequired) and make it more easy to read.
One more thing is when you generate new array the isRequired: prop.isRequired is unnecessary since we know that only elements with isRequired: true came here.. so I'd change it to isRequired: true
Of course you can achieve the same result using forEach and if condition inside as other contributors mentioned, but that is not as elegant as my answer. But to honest, my method require a slightly more ticks to finish since 2 Array cycles evaluating, but remember we write code for humans, not for machines.
For now I fixed it like this, but this does not seem like the most clean/decent way. If anyone could come up with a better solution I would appreciate it.
angular.forEach(vm.schema.properties, function (prop, key) {
if (prop.isRequired == "true") {
vm.mappingFields.push({
source: null,
destination: key,
fieldType: prop.type,
isRequired: "Required",
});
};
I am going to break this down step by step for what I want to happen so hopefully people can understand what I am wanting.
Using React/Redux, Lodash
I have many post that are sent from a back end api as an array. Each post has an _id. When I call on the action getAllPost() it gives me back that array with all the post. This is working just fine.
I then dispatch type GET_ALL_POSTS and it triggers the reducer reducer_posts to change/update the state.
reducer:
export default function(state = {}, action) {
switch(action.type) {
case GET_ALL_POSTS:
const postsState = _.mapKeys(action.payload.data, '_id');
//const newPostsState = _.map(postsState, post => {
//const newComments = _.mapKeys(post.comments, '_id');
//});
return postsState;
break;
default:
return state;
break;
}
}
As you can see I change the array into one giant object that contains many post as objects with keys that are equal to their '_id'. This works just fine and returning this part of the state also works fine.
As I mentioned each of these posts has a comments value that is an array. I would like to change the comments array into one large object that holds each comment as an object with a key that is equal to their '_id' just like I did in the post.
Now I need to do this all at once and return the newly created state with One large object that contains all the post as objects and on each of those post there should be a comments object that contains all the comments as objects. I will try to write some example code to show what I am trying to do.
Example:
BigPostsObject {
1: SinglePostObject{},
2: SinglePostObject{},
3: SinglePostObject {
_id: '3',
author: 'Mike',
comments: BigCommentObject{1: SingleCommentObject{}, 2: SingleCommentObject{}}
}
}
I hope that the example kind of clears up what I am trying to do. If it still is confusing as to what I am doing then please ask and also please do not say things like use an array instead. I know I can use an array, but that is not helpful to this post as if others want to do it this way that is not helpful information.
Write a function that processes all the comments from the comments array for each post you have in the posts array:
function processComment(post) {
post.bigCommentsObject = _.mapKeys(post.comments, '_id');
// now the comments array is no longer needed
return _.omit(post, ['comments']);
}
Now use that function to turn each comments array into a big object with all the comments WHILE it still is in the array. Then afterwards turn the array itself in a big object:
const commentsProcessed = _.map(action.payload.data, procesComment);
const postsState = _.mapKeys(commentsProcessed, '_id');
I believe nowadays JS builtin function can do this without requiring external libraries. Anyway this should be the way to go. I will really encourage you getting back to js builtin functions.
var data = [
{
_id: '3',
title: 'Going on vaccation',
comments:[
{_id: 1, comment: 'hello'},
{_id: 2, comment: 'world'}
]
},
{
_id: '2',
title: 'Going to dinner',
comments:[
{_id: 1, comment: 'hello'},
{_id: 2, comment: 'world'}
]
}
]
//you can use JS builtin reduce for this
var transformedPost= _.reduce(data, function(posts, post) {
var newPost = Object.assign({}, post)
newPost._id=post._id
//you can use js builtin map for this
newPost.comments = _.mapKeys(post.comments, '_id')
// if you are using es6, replace the last three line with this
//return Object.assign({}, posts, {[newPost._id]: newPost})
var item = {}
item[newPost._id]=newPost
return Object.assign({}, posts, item)
},{});
console.log(transformedPost)
https://jsbin.com/suzifudiya/edit?js,console
I would like to use mongoose in this case. Let's say we have a Schema like this:
const userSchema = new Schema({
name: {
first: { type: String, required: true },
last: { type: String, required: true },
},
email: { type: String, required: true, unique: true, lowercase: true },
});
Let's say we already have document with first and last name. I need to update only first name and email with the following args object:
const updateUser = {
name: {
first: Eddy,
},
email: 'eddy#gordo.io'
};
If it will be used with mongoose update methods in most cases it would also change last name to null. That's because JavaScript doesn't support deep object merge and so on. So what's the best way to gain possibility to merge objects properly?
I've found the way to do it below but it doesn't seem to be the best solution:
User.findById(args.id, (error, user) => {
if (error) throw err;
user.name.first = args.name.first ? args.name.first : user.name.first;
user.name.last = args.name.last ? args.name.last : user.name.last;
user.email = args.email ? args.email : user.email;
user.save((err, updatedUser) => {
if (err) throw err;
return updatedUser;
});
});
I and Eslint don't like this with reassigning and unmaintainable code. Have somebody better idea? I'm not sure that I need to use lodash and other libs only for this capability.
Here is what I've done with this issue.
Thanks to Neil Lunn with this idea.
We're need to map all object keys to dot.notation and pass to the $set.
Here is the gist with this function. If we need two level nesting we can add another condition and mapping.
Anyway I hope that Mongoose API will resolve us to make deep merge with nested objects, but for now Neil's solution is the best in my opinion.
I am trying to populate references nested within other references. I have it working but it seems kinda hacky and was wondering if there is any other way to accomplish this:
return Q.ninvoke(BoardSchema, 'find', {'_id': id}).then(function(board) {
return Q.ninvoke(BoardSchema, 'populate', board, {path: 'lanes'}).then(function(board){
return Q.ninvoke(LaneSchema, 'populate', board[0].lanes, {path: 'cards'}).then(function(lanes){
board.lanes = lanes;
return board;
});
});
});
Is there some method to populate all references, or return the second populate as part of the board call without manually setting it like I am now?
You should be able to populate multiple to populate nested documents like so:
Item.find({}).populate('foo foo.child').exec(function(err, items) {
// Do something here
});
This requires that refs are setup in the Schema definitions.
If this doesn't work, which to be honest is most of the times for some reason, you can chain your finds. But this doesn't differ much from your code.
Item.find({}).populate('foo').exec(function(err, items) {
Item.find(items).populate('bar').exec(function(err, items) {
// Even more nests if you like
});
});
Based on the response of Gideon
Item.find({ _id: id})
.populate({
path: 'foo',
model: 'FooModel',
populate: {
path: 'child',
model: 'ChildModel'
}
})
.exec(function(err, items) {
// ...
});