How to copy, delete, or move a document in Firestore

To move a Firestore document from one collection to another, including all existing sub-collections, we will need a Cloud function that copies all documents and then delete existing ones. In this post, you'll find working functions to use with some explanations.

Reading a document's content and creating a new one in another collection with it. That is something doable even with the client library. But the documents can have sub-collections with more docs in it. And unless you have a list with all possible sub-collections for every document, so you can iterate over it and copy the internals, you'll need a cloud function that can go through all sub-collections.

Copy

Here is an example of how to copy a document. Just a single one or with all sub-collections possible is specified with the recursive param. And addData parameter is for any key-values that should be added to the resulting document. Like user id and the time, the copy was made.

export const copyDoc = async (
  collectionFrom: string,
  docId: string,
  collectionTo: string,
  addData: any = {},
  recursive = false,
): Promise<boolean> => {
  // document reference
  const docRef = admin.firestore().collection(collectionFrom).doc(docId);

  // copy the document
  const docData = await docRef
    .get()
    .then((doc) => doc.exists && doc.data())
    .catch((error) => {
      console.error('Error reading document', `${collectionFrom}/${docId}`, JSON.stringify(error));
      throw new functions.https.HttpsError('not-found', 'Copying document was not read');
    });

  if (docData) {
    // document exists, create the new item
    await admin
      .firestore()
      .collection(collectionTo)
      .doc(docId)
      .set({ ...docData, ...addData })
      .catch((error) => {
        console.error('Error creating document', `${collectionTo}/${docId}`, JSON.stringify(error));
        throw new functions.https.HttpsError(
          'data-loss',
          'Data was not copied properly to the target collection, please try again.',
        );
      });

    // if copying of the subcollections is needed
    if (recursive) {
      // subcollections
      const subcollections = await docRef.listCollections();
      for await (const subcollectionRef of subcollections) {
        const subcollectionPath = `${collectionFrom}/${docId}/${subcollectionRef.id}`;

        // get all the documents in the collection
        return await subcollectionRef
          .get()
          .then(async (snapshot) => {
            const docs = snapshot.docs;
            for await (const doc of docs) {
              await copyDoc(subcollectionPath, doc.id, `${collectionTo}/${docId}/${subcollectionRef.id}`, true);
            }
            return true;
          })
          .catch((error) => {
            console.error('Error reading subcollection', subcollectionPath, JSON.stringify(error));
            throw new functions.https.HttpsError(
              'data-loss',
              'Data was not copied properly to the target collection, please try again.',
            );
          });
      }
    }
    return true;
  }
  return false;
};

First, we are reading the document and then creating a new one on the new destination. In case there are sub-collections, we are listing all subcollections and copying every document using the same function.

Here, you can see, that I'm using a lot for await ... of instead of Promise.all(). This is to make all the operations running one after another and take less memory. This is important when writing Firebase functions and I'll have another post about that soon.

Delete

Same approach when we want to delete a document, but somehow reversed: delete documents in the sub-collections and then the document itself.

export const deleteDoc = async (docPath: string): Promise<boolean> => {
  // document reference
  const docRef = admin.firestore().doc(docPath);

  // subcollections
  const subcollections = await docRef.listCollections();
  for await (const subcollectionRef of subcollections) {
    await subcollectionRef
      .get()
      .then(async (snapshot) => {
        const docs = snapshot.docs;
        for await (const doc of docs) {
          await deleteDoc(`${docPath}/${subcollectionRef.id}/${doc.id}`);
        }
        return true;
      })
      .catch((error) => {
        console.error('Error reading subcollection', `${docPath}/${subcollectionRef.id}`, JSON.stringify(error));
        return false;
      });
  }

  // when all subcollections are deleted, delete the document itself
  return docRef
    .delete()
    .then(() => true)
    .catch((error) => {
      console.error('Error deleting document', docPath, JSON.stringify(error));
      return false;
    });
};

Yes, I know that in the official Firebase documentation there is another example that requires using firebase-tools to delete a document with all sub-collections, but I prefer to use the function above.

Move

Like I wrote in the beginning: moving a document in Firestore is literally copying it to the new destination and then removing from the original collection. Since we already have functions to copy and delete — it is now easy:

export const moveDoc = async (
  collectionFrom: string,
  docId: string,
  collectionTo: string,
  addData?: any,
): Promise<boolean | Error> => {
  // copy the organisation document
  const copied = await copyDoc(collectionFrom, docId, collectionTo, addData, true);

  // if copy was successful, delete the original
  if (copied) {
    await deleteDoc(`${collectionFrom}/${docId}`);
    return true;
  }
  throw new functions.https.HttpsError(
    'data-loss',
    'Data was not copied properly to the target collection, please try again.',
  );
};

Keep these three functions in some file with utility functions and use them whenever you need to copy, delete, or move a document.

HTH