Firestore - cannot encode value - when adding a document using Promise.all() with MongoDB Insert

Viewed 187

Initially I had this code:

const newUserObject = { name: "username", uid: "FirebaseUID", posts: [] }

await Promise.all([
    admin.firestore().collection("users").doc("FirebaseUID").set(newUserObject),
    mongoDb.collection("users").insertOne(newUserObject)
])

For some reason, this doesn't work and returns the error "cannot encode value". Here is the complete stack trace:

Cannot encode value: 60bb87e2cb98774f01d8bb0b
       at Serializer.encodeValue (~/node_modules/@google-cloud/firestore/build/src/serializer.js:168:15)
       at Serializer.encodeFields (~/node_modules/@google-cloud/firestore/build/src/serializer.js:57:30)
       at Function.fromObject (~/node_modules/@google-cloud/firestore/build/src/document.js:99:53)
       at Object.op (~/node_modules/@google-cloud/firestore/build/src/write-batch.js:267:58)
       at ~/node_modules/@google-cloud/firestore/build/src/write-batch.js:457:44
       at Array.map (<anonymous>)
       at WriteBatch._commit (~/node_modules/@google-cloud/firestore/build/src/write-batch.js:457:31)
       at processTicksAndRejections (internal/process/task_queues.js:95:5)
       at async Promise.all (index 0)
   Caused by: Error
       at WriteBatch.commit (~/node_modules/@google-cloud/firestore/build/src/write-batch.js:426:23)
       at DocumentReference.set (~/node_modules/@google-cloud/firestore/build/src/reference.js:343:14)
       at ~/dist/handlers/users.js:75:68
       at Generator.next (<anonymous>)
       at fulfilled (~/dist/handlers/users.js:5:58)
       at processTicksAndRejections (internal/process/task_queues.js:95:5)

The value looks similar to MongoDB ObjectID but I am sure I am not passing that in Firestore object in any way. I checked an answer to a similar question which said you cannot set empty arrays in a new document but that doesn't seem right to me.

If I rewrite my code as follows, it works like charm.

await admin.firestore().collection("users").doc("FirebaseUID").set(newUserObject)
await mongoDb.collection("users").insertOne(newUserObject)

To confirm, nothing is undefined in the object I am trying to set. The issue occurs only when I use it with MongoDB's insertOne in a Promise.all().

Steps to clone the issue:

npm init -y
npm i firebase-admin mongodb

Then copy this code:

const admin = require("firebase-admin")
const mongo = require("mongodb")

const client = mongo.MongoClient("mongodb://localhost:27017/dbname", {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    auth: {
        user: "mongodbusername",
        password: "mongodbpassword"
    }
})

client.connect().then(async () => {
    console.log("MongoClient Connected");
    admin.initializeApp({
        credential: admin.credential.cert("serviceAccountKey.json"),
        databaseURL: "https://<project-id>.firebaseio.com"
    })

    const newUserObject = {name: "NewUser", uid: "userUID", posts: ["PostID1"]}
    
    // UPDATING INDIVIDUALLY
    // await admin.firestore().collection("users").doc(newUserObject.uid).set(newUserObject)
    // await client.db("lasergg").collection("users").insertOne(newUserObject)
    // Works perfectly

    // UPDATING IN A PROMISE.ALL()
    await Promise.all([
        admin.firestore().collection("users").doc(newUserObject.uid).set(newUserObject),
        client.db("local").collection("users").insertOne(newUserObject)
    ])
    // Throws error Error: Cannot encode value: 60bdee0a77659f27d493dec8

    console.log("All Documents Added!");
})

I modified the source code of Firestore Node SDK here and logged the 'val' parameter [console.log(val, typeof val)] that encodeValue function takes and the output was:

60be04dd53e7965bf4a23173 object

Edit: I logged values being passed in encodeValue() when running promises individually and the output is:

enter image description here

I ran the same when running in Promise.all():

enter image description here

This is what I edited in 'serialier.js':

enter image description here

1 Answers

Fix

You should be able to fix this problem by passing a copy of newUserObject into MongoDB, like this:

await Promise.all([
    admin.firestore().collection("users").doc("FirebaseUID").set(newUserObject),
    mongoDb.collection("users").insertOne({ ...newUserObject })
])

Note this: insertOne({ ...newUserObject }).

Explanation

When you try to insert a document into MongoDB without an _id field, MongoDB creates one automatically by generating an ObjectId. The operation basically looks like this: newUserObject._id = new ObjectId().

It seems that when you run firestore.set() and mongo.insertOne() in parallel, this happens before the serializer step in Firestore.

So whenever the serializer starts encoding, it sees this new _id field and tries to encode it, and that's where it fails. Note that it sees the _id field because you are passing the same object reference (newUserObject) to both firestore.set() and mongo.insertOne().

This explains why executing the operations sequentially works. Here you first insert the document into Firestore and only then into MongoDB, so the _id field is no longer a problem:

await admin.firestore().collection("users").doc("FirebaseUID").set(newUserObject)
await mongoDb.collection("users").insertOne(newUserObject)

However, if you were to reverse the order here and inserted into MongoDB first, you'd encounter the same problem.

Related