Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FR: FieldValue arrayUnion/arrayRemove for array with objects #1918

Open
emadalam opened this issue Jun 26, 2019 · 25 comments
Open

FR: FieldValue arrayUnion/arrayRemove for array with objects #1918

emadalam opened this issue Jun 26, 2019 · 25 comments

Comments

@emadalam
Copy link

emadalam commented Jun 26, 2019

[REQUIRED] Describe your environment

  • Operating System version: macOS 10.14.3
  • Browser version: Google Chrome 74.0.3729.169
  • Firebase SDK version: 6.0.2
  • Firebase Product: database

[REQUIRED] Describe the problem

Steps to reproduce:

firebase.firestore.FieldValue.arrayUnion and firebase.firestore.FieldValue.arrayRemove are great if the array is made up of primitive values. However to update a field value which is made up of array of objects, it's impossible to perform the operation without querying the actual data and performing the union/without operation on the client.

While this is alright if we already have queried the data, but in cases where we haven't queried the data to begin with, it's an unnecessary overhead to make this request. What's worse is when we haven't subscribed to the document updates and are trying to manipulate the array with the stale values, it's going to cause more harm than good, defeating the purpose of allowing array operations with arrayUnion and arrayRemove.

Relevant Code:

// existing document data
const data = {
  uid: 'xxxx'
  key: 'value',
  uploadedDocuments: [{
    uid: 'yyyy',
    name: 'something.png',
    url: '....'
  }, {
    uid: 'zzzz',
    name: 'something-else.png',
    url: '....'
  }]
}

// update document but it will add new object to the list
firebase.firestore
  .doc('myCollection/xxxx')
  .update({
    uploadedDocuments:
      firebase.firestore.FieldValue.arrayUnion({
        uid: 'yyyy',
        name: 'something.png',
        url: '....'
      })
  })

What is desirable is an option to set uniqueBy parameter or something similar.

firebase.firestore.doc('myCollection/xxxx')
  .update({
    uploadedDocuments:
      firebase.firestore.FieldValue.arrayUnion({
        uid: 'yyyy',
        name: 'something.png',
        url: '....'
      }, firebase.firestore.FieldValue.uniqueBy('uid'))
  })

// signature
firebase.firestore.FieldValue.arrayUnion(
  ...values,
  firebase.firestore.FieldValue.uniqueBy(key)
)
@schmidt-sebastian
Copy link
Contributor

schmidt-sebastian commented Jun 27, 2019

@emadalam Thanks for filing this issue! Since array-union currently treats all elements as distinct, using a nested key-value object might be a better fit for you. If you can key your data by user ID, you could potentially do the following:

firestore.doc('myCollection/xxxx').update(`uploadedDocuments/${uid}`, {name: ...} );

We will keep your feature request in mind as we shape our APIs, but you might be able to adjust your data model to take advantage of our existing features.

As a further comment, if you do have to rely on Arrays and need to download, modify and set your data manually, please take a look at our Transaction API. If you perform these operations in a transaction, then you will not encounter issues with stale data.

@emadalam
Copy link
Author

@schmidt-sebastian Thanks for your suggestion. Though it was just an example model. I do understand that I can model all my array needs into a hash map with unique keys, but that essentially means I'm not using arrays anymore.

Furthermore it would be tricky to remove items from the hash map without performing a complete update/set operation, while in case of firebase.firestore.FieldValue.arrayRemove it would be much simpler and convenient if it would support uniqueness identifier.

I'd keep an eye on this issue and see if the feature request gets implemented in near future 🙏

@schmidt-sebastian
Copy link
Contributor

@emadalam You can remove a single key from a map as such:

firestore.doc('myCollection/xxxx').update(`uploadedDocuments/${uid}`,FieldValue.delete());

@emadalam
Copy link
Author

@schmidt-sebastian Thanks for the input, however all the approaches are under the assumption that we model the data as hash map. The original question with array values, still remains.

Until the feature request is added, I'm using the below version for now that makes use of transaction api to update the array. I still don't like the fact that I need to fetch data and run a transaction just to perform an update operation, but I guess as of now there's no better alternate that exists 🤷‍♂If you know any better/faster alternate to perform the same, feel free to mention here 💪

import isPlainObject from 'lodash.isplainobject'
import get from 'lodash.get'
import partialRight from 'lodash.partialright'
import unionBy from 'lodash.unionby'
import reject from 'lodash.reject'
import firebase, { db } from 'configs/firebase'

export async function arrayUnion({ documentRef, path, uniqueBy, data }) {
  if (!uniqueBy || !isPlainObject(data)) {
    return documentRef.update({
      [path]: firebase.firestore.FieldValue.arrayUnion(data),
    })
  }

  const updateFn = partialRight(unionBy, uniqueBy)
  return updateArray({ documentRef, path, data, updateFn })
}

export async function arrayRemove({ documentRef, path, uniqueBy, data }) {
  if (!uniqueBy || !isPlainObject(data)) {
    return documentRef.update({
      [path]: firebase.firestore.FieldValue.arrayRemove(data),
    })
  }

  const updateFn = values => reject(values, { [uniqueBy]: data[uniqueBy] })
  return updateArray({ documentRef, path, data, updateFn })
}

export async function updateArray({
  documentRef,
  path,
  data,
  updateFn = firebase.firestore.FieldValue.arrayUnion,
}) {
  if (!documentRef || !path) return

  return db.runTransaction(async transaction => {
    const doc = await transaction.get(documentRef)
    if (!doc.exists) return

    const dataArray = Array.isArray(data) ? data : [data]
    const existingValues = get(doc.data(), path)
    const newValues = updateFn(existingValues, dataArray)
    await transaction.update(documentRef, { [path]: newValues })
  })
}

Usage:

arrayUnion({
  documentRef: db.doc('myCollection/xxxx'),
  path: 'uploadedDocuments',
  uniqueBy: 'uid',
  data: {
    uid: 'yyyy',
    name: 'something.png',
    url: '....',
  },
})

arrayRemove({
  documentRef: db.doc('myCollection/xxxx'),
  path: 'uploadedDocuments',
  uniqueBy: 'uid',
  data: {
    uid: 'yyyy',
    name: 'something.png',
    url: '....',
  },
})

@schmidt-sebastian
Copy link
Contributor

schmidt-sebastian commented Jul 1, 2019

@emadalam Unfortunately, there is no succinct way to manipulate array data based on partial equality. We will keep this issue updated as we evolve our APIs. If you do need to use arrays in your documents, updating them inside a Transaction (as shown in your code sample) is the best way forward at this point.

@goleary
Copy link

goleary commented Nov 12, 2019

I'd like to register a vote for this functionality.

@napindc
Copy link

napindc commented Feb 1, 2020

Very much needed!

@xnio94
Copy link

xnio94 commented Feb 6, 2020

i encounter the same problem but in my case, i already downloaded the data because i'm subscribing to a stream, the problem is that I'm not sure how to combine this with the Transaction API
in other words, i don't know how to make a transaction without redownloading the data because i'm already subscribing to it.
so the solution I'm considering is to store the data JSON-serialized, and instead of dealing with an array of objects I'll deal with an array of JSON strings, I know it's ugly and may generate problems in the future but until now it seems the most reasonable solution for me
i hope firebase.firestore.FieldValue.arrayUnion and firebase.firestore.FieldValue.arrayRemove will support non-primitives values soon

@viktorlarsson
Copy link

viktorlarsson commented Mar 18, 2020

@emadalam Did you get this to work? Trying out your code, but i'm a bit unsure if partialRight does what.. I want it do to. Take a simple checklist for example:

{id: 1, checked: true},
{id: 2, checked: false}
]
const data = {
   id: 2, 
  checked: true
}

arrayUnion({
      documentRef: firestore()
        .collection(`collection/checklists`)
        .doc(checklist.id),
      path: "checklistitems",
      uniqueBy: "id",
      data
    })

Seems to just leave the array in the same state.

@emadalam
Copy link
Author

@viktorlarsson The code works just fine for the use case. However from what I see for your specific use case you'd need to change the updateFn of the arrayUnion function to account for merging the values.

const updateFn = (values, updates) =>
  _(values)
    .keyBy(uniqueBy)
    .merge(_.keyBy(updates, uniqueBy))
    .values()
    .value()

export async function arrayUnion({ documentRef, path, uniqueBy, data }) {
  if (!uniqueBy || !isPlainObject(data)) {
    return documentRef.update({
      [path]: firebase.firestore.FieldValue.arrayUnion(data),
    })
  }

  const updateFn = (values, updates) =>
    _(values)
      .keyBy(uniqueBy)
      .merge(_.keyBy(updates, uniqueBy))
      .values()
      .value()
  return updateArray({ documentRef, path, data, updateFn })
}

@viktorlarsson
Copy link

Works like a charm, thanks @emadalam !

@ajain-1
Copy link

ajain-1 commented Apr 14, 2020

Since we are talking about arrayUnion for an array of objects, how is it possible to add an object to an array of objects? For example below is some sample data

myTodoList: [
    {text: 'wash the dishes', completed: true},
    {text: 'clean room', completed: false}
]

How would I add to the above array of objects another todo object? arrayUnion simply isn't adding an object to an array and seems to only be able to add basic values. I would appreciate some help.

@rafikhan
Copy link
Contributor

@ShadeAJ1 - The arrayUnion() method supports adding entire objects during an update(). For example:

  docRef.update({
    myTodoList: firebase.firestore.FieldValue.arrayUnion({ text: "take out trash", completed: true })
  });

If this isn't working please open a new issue with repro steps so we can look into it.

@ajain-1
Copy link

ajain-1 commented Apr 16, 2020

@rafikhan I know how to add an object that way but what about with a variable like this..

myObject = {text: 'take out trash', completed: true}

docRef.update({
    myTodoList: firebase.firestore.FieldValue.arrayUnion(myObject)
});

I would use this where I do not know all the values of an object or if I have different amounts of values in objects I would like to add. Adding an object like this doesn't work and the object doesn't get added to the array. Let me know if I should open a separate issue and I will do so. Thank you!

@emadalam
Copy link
Author

emadalam commented Apr 17, 2020

@rafikhan Why is this issue closed? Is the original feature request implemented as part of some PR or release somewhere? It's strange at the very least to just randomly close the issue for an unrelated random comment 🤷

@rafikhan
Copy link
Contributor

@emadalam - You're right. I usually close them for customer support issues and it should have been left open since the FR hasn't been resolved.

@rafikhan rafikhan reopened this Apr 17, 2020
@ajain-1
Copy link

ajain-1 commented Apr 17, 2020

@emadalam @rafikhan sorry for the unrelated question. I just asked it here because it relates to arrayUnion with objects. Should I submit a new feature request about being able to pass in an object instead of explicitly defining all fields using arrayUnion? Let me know.

@rafikhan
Copy link
Contributor

@ShadeAJ1 yes, you should file a new issue with a repro of your issue.

@ajain-1
Copy link

ajain-1 commented Apr 18, 2020

@rafikhan ok I will do so.

@SebastianMena-185
Copy link

SebastianMena-185 commented Jul 3, 2020

problem when I use arrayUnion () in firebase it deletes all the array that I have saved in my database

@rafikhan
Copy link
Contributor

rafikhan commented Jul 6, 2020

@SebastianMena-185 - Please open a new issue with a code sample.

@Joonpark13
Copy link

I'd like to register a vote for this functionality.

➕ 1

@yosefeliezrie

This comment has been minimized.

@yosefeliezrie
Copy link

yosefeliezrie commented Nov 6, 2020

@schmidt-sebastian Thanks for the input, however all the approaches are under the assumption that we model the data as hash map. The original question with array values, still remains.

Until the feature request is added, I'm using the below version for now that makes use of transaction api to update the array. I still don't like the fact that I need to fetch data and run a transaction just to perform an update operation, but I guess as of now there's no better alternate that exists 🤷‍♂If you know any better/faster alternate to perform the same, feel free to mention here 💪

import isPlainObject from 'lodash.isplainobject'
import get from 'lodash.get'
import partialRight from 'lodash.partialright'
import unionBy from 'lodash.unionby'
import reject from 'lodash.reject'
import firebase, { db } from 'configs/firebase'

export async function arrayUnion({ documentRef, path, uniqueBy, data }) {
  if (!uniqueBy || !isPlainObject(data)) {
    return documentRef.update({
      [path]: firebase.firestore.FieldValue.arrayUnion(data),
    })
  }

  const updateFn = partialRight(unionBy, uniqueBy)
  return updateArray({ documentRef, path, data, updateFn })
}

export async function arrayRemove({ documentRef, path, uniqueBy, data }) {
  if (!uniqueBy || !isPlainObject(data)) {
    return documentRef.update({
      [path]: firebase.firestore.FieldValue.arrayRemove(data),
    })
  }

  const updateFn = values => reject(values, { [uniqueBy]: data[uniqueBy] })
  return updateArray({ documentRef, path, data, updateFn })
}

export async function updateArray({
  documentRef,
  path,
  data,
  updateFn = firebase.firestore.FieldValue.arrayUnion,
}) {
  if (!documentRef || !path) return

  return db.runTransaction(async transaction => {
    const doc = await transaction.get(documentRef)
    if (!doc.exists) return

    const dataArray = Array.isArray(data) ? data : [data]
    const existingValues = get(doc.data(), path)
    const newValues = updateFn(existingValues, dataArray)
    await transaction.update(documentRef, { [path]: newValues })
  })
}

Usage:

arrayUnion({
  documentRef: db.doc('myCollection/xxxx'),
  path: 'uploadedDocuments',
  uniqueBy: 'uid',
  data: {
    uid: 'yyyy',
    name: 'something.png',
    url: '....',
  },
})

arrayRemove({
  documentRef: db.doc('myCollection/xxxx'),
  path: 'uploadedDocuments',
  uniqueBy: 'uid',
  data: {
    uid: 'yyyy',
    name: 'something.png',
    url: '....',
  },
})

@emadalam I am trying to do something similar but I can't figure out how your workaround works. I don't think it makes a difference by my app is in react and using react-redux-firebase but its a very similar issue to yours with arrayRemove not working

My data looks like this

const data = {
  listName: 'xxxx'
  uid: 'value',
  tasks: [{
     title: 'fist task array item',
    notes:  'xxx',
    url:  'xxx',
    dueDate:  'xxx',
    completed:  'xxx',
  }, {
     title: 'second task array item',
    notes:  'xxx',
    url:  'xxx',
    dueDate:  'xxx',
    completed:  'xxx',
  },{
     title: 'third task array item',
    notes:  'xxx',
    url:  'xxx',
    dueDate:  'xxx',
    completed:  'xxx',
  }]
}

I want to be able to remove a single task by array index. Right now i have (in my example i am passing 3 which is an example array index, but in reality, I am passing the array key programmatically via react props but that shouldn't make a difference to this). Any ideas thanks!

currentTaskList
      .update({
        tasks: firebase.firestore.FieldValue.arrayRemove(3),
      })
      .then((docRef) => {
        console.log(
          "Task Removed: " + task.title + " Index: " + tasksArrayIndex
        );
      })
      .catch((error) => {
        console.error("Error: " + error);
      });

@mmontag
Copy link

mmontag commented Jul 5, 2024

I was pretty surprised to find out arrayRemove doesn't address by index.
You can't delete the third item alone from ['apple', 'banana', 'apple'].

Wouldn't removing by index be easy to implement (compared to a deep equality check that scans the entire array)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests