Firestore query sometimes doesn't contain updated data - node.js

I'm using firestore's node sdk. In my code I call function1, which updates a table in firestore. When that function ends I call function2, which runs a query to get a reference of the table. About 80% of the time it works, but sometimes the data I need, which was added to the document in function1 doesn't come back in the snapshot so an error is thrown.
I added an await keyword before the update, but that doesn't seem to make the code wait for the firestore update to complete.
I suppose I could also return the data I was going to update in function1 and pass it into function2, but that feels kind of hacky, though I suppose I'd save a little money because I wouldn't have to get 1 document anymore. I could also make it one big function, but that would make it an 100 line function.
Here's an abbreviated version of my code:
const function1 = async (tableId) => {
const firestore = admin.firestore();
const tableSnapshot = await firestore.collection('tables').doc(tableId).get();
await tableSnapshot.ref.update({ smallBlind: {seat: 1, amount: 5000} }) // the seat number and amount number wont always be 1 and 5000. Otherwise I wouldn't need to look it up in function2
}
const function2 = async (tableId) => {
const firestore = admin.firestore();
const tableSnapshot = await firestore.collection('tables').doc(tableId).get();
const tableData = tableSnapshot.data();
const smallBlind = tableSnapshot.data().smallBlind; // the smallBlind data is not there. so smallBlind is undefined
}
const startStuff = async () => {
await function1(42); // example tableId is passed in
await function2(42);
}
startStuff()

The above code has no async issues. I had a different async issue in another portion of my code, which caused my problem.

Related

Why does my async code not work properly unless I log the promise?

I have some async code that makes calls to a mongo database and inserts/fetches items. When I am developing locally, the code below works fine. However, when I make the mongoose instance connect to MongoDb Atlas, issues arise. In particular, it seems that my code does not work properly unless I console.log the promise, which makes no sense to me. For example, with the console.log statement, all my tests pass as expected. Without it, 35 tests fail... This is because the promise I am expecting returns null, when it should return some JSON object from the database. Is my code not blocking properly?
It feels like I'm dealing with Schrodinger's cat... Any help would be appreciated. Thanks in advance.
Below is an example promise/function call. I then pass it into _executeQuery. I have await on relevant functions, so I don't think it's because I'm missing the word await somewhere.
async _inSomeAsyncFunction = () => {
const dbQueryPromise = this._dbModel.findById(_id, modelView).lean();
await this._executeQuery({ dbQueryPromise, isAccessPermitted: true })
}
_executeQuery basically gets the result of the promise if the user has access.
private _executeQuery = async (props: {
isAccessPermitted: boolean;
dbQueryPromise: Promise<any>;
}): Promise<any> => {
const { isAccessPermitted, dbQueryPromise } = props;
if (!isAccessPermitted) {
throw new Error('Access denied.');
}
console.log(dbQueryPromise, 'promise'); // without this line, dbQueryResult would be null...
const dbQueryResult = await dbQueryPromise;
return dbQueryResult;
};
After some more testing, I found out that the first API call works but any calls after that returns null...
EDIT:
this._dbModel is some mongoose schema. For example,
const dbSchema= new Schema({
name: String,
});
const dbModel = mongoose.model('DbSchema', dbSchema);
Try replacing your dbQueryPromise as follows:
const dbQueryPromise = this._dbModel.findById(_id, modelView).lean().exec();
Mongoose queries do not get executed unless you pass a callBack function or use exec()
For anyone else having similar problems, here's how I solved it:
I changed
const dbQueryResult = await dbQueryPromise;
to
const dbQueryResult = await dbQueryPromise.then((doc) => {
return doc;
});

delete data in firestore after n time using cloud functions

I have been trying to find a way in which the documents from a collection in firestore can get deleted after a certain amount of time in my case 24h, for doing this I realized that I have to use firebase cloud functions so after investigating I found this post which does the work that I need to add in my project except that I am using typescript instead of javascript which is being used in that question and I cant change it to javascript because I am already using typescipt for sending notifications to users. How can I change the following code to typescript?
//index.ts
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp()
const db = admin.firestore();
export const deleteOldItems = functions.database
.ref('stories/{storyId}')
.onWrite((change, context) => {
var ref = change.after.ref.parent;
var now = Date.now();
var cutoff = now - 24 * 60 * 60 * 1000;
var oldItemsQuery = ref.orderByChild('created').endAt(cutoff);
return oldItemsQuery.once('value', function(snapshot) {
// create a map with all children that need to be removed
var updates = {};
snapshot.forEach(function(child) {
updates[child.key] = null
});
// execute all updates in one go and return the result to end the function
return ref.update(updates);
});
});
here is an image of the data that needs to be erased after 24h, created field is the date when the document was created and deleted id the date where the doc should be deleted.
EDIT: I turns out the problem is not because of using typescript but because I am using firestore instead of firebase real time database so I created this code which is supposed to make things work but it doesnt. How can I fix this?
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp()
const db = admin.firestore();
export const deleteOldItems = functions.firestore
.document('stories/{storyId}')
.onWrite(async (change, context) => {
const getData = await db.collection('stories').where('deleted', '>', Date.now()).get().then(
(snapshot) => snapshot.forEach((doc) =>
doc.ref.delete()
)
);
return getData;
});
You are mixing async/await with then and, in addition, you are not waiting that the set of calls to the delete() asynchronous method are all completed. So the Cloud Function platform may clean up the Cloud Function instance before all the work is done. You need to wait that all this asynchronous work is done before returning a Promise or a value. More details here in the doc.
The following should do the trick:
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp()
const db = admin.firestore();
export const deleteOldItems = functions.firestore
.document('stories/{storyId}')
.onWrite(async (change, context) => {
const querySnapshot = await db.collection('stories').where('deleted', '>', Date.now()).get();
const promises = [];
querySnapshot.forEach((doc) => {
promises.push(doc.ref.delete());
});
return Promise.all(promises);
});
Since you want to delete a variable number of documents, you need to use Promise.all() to execute in parallel the asynchronous deletion tasks (Since the delete() method returns a Promise).
Updates following you questions in the comments below:
1/ Promise.all() returns a single Promise that resolves to an array of the results of the input promises. This returned promise will resolve when all of the promises in the array have resolved. The main interest of Promise.all() is that it returns a single Promise, therefore you can easily know when all the calls to the asynchronous delete() method (which returns a Promise) are done.
2/ The onWrite event type is triggered when a doc is created, modified or deleted. Since you used this event type in your question I kept it. If you want to schedule the execution, use a scheduled Cloud Function. The "inner code" is exactly the same.
I think that ".onWrite(async (change, context) " its only called when the document is created, you need to create a task every x time to check If there is a document to delete, then you make the query and delete de document.
You can use Cron job from you server o task scheduler, I think google cloud have its own task scheduler.
A functions can not last more than 60 seconds, after that sent an error.

Cannot get FieldValue.increment working in Firestore emulator with Admin SDK

I am trying to get a simple atomic increase/decrease working in Firebase Cloud Functions with Firestore triggers.
To test these, I want to execute local tests with the Firestore emulator.
But I always get the error
FirebaseError: Function DocumentReference.update() called with invalid data. Unsupported field value: a custom object (found in field myfield)
My code is as simple as following
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp();
export const mytest = functions.firestore.document('my/{doc}').onUpdate(async (change, context) => {
return perform();
});
export async function perform(): Promise<FirebaseFirestore.WriteResult> {
const myDocRef = .... // this is a FirebaseFirestore.DocumentReference
const decrement = admin.firestore.FieldValue.increment(1);
return myDocRef.update({
myfield: decrement
});
}
In my tests I just call the perform function and then I receive the above error. It's definitely the FieldValue.increment that causes the error because if I just put a hardcoded number instead of the decrement in the update call, it works (updates to that hardcoded number).
I am on Firebase CLI 7.2.1 and emulator 1.6.2 which - according to Most efficient way to increment a value of everything in Firebase and https://github.com/firebase/firebase-js-sdk/issues/1799 should support FieldValue.increment in the emulator.
What am I doing wrong?
What version of firebase-admin are you using?
From your description it sounds like you're calling perform() directly from a Node.js script. I would recommend re-working your setup to more closely match what will actually happen in production: a client will connect directly to Firestore, create a document, and your function will be triggered.
Here's an example you can run that does this:
const admin = require('firebase-admin');
const functions = require('firebase-functions');
admin.initializeApp();
exports.createItem = functions.https.onRequest(async (request, response) => {
const item = await admin.firestore().collection("items").add({ addedAt: Date.now() });
response.send(`added ${item.id}\n`);
});
const counter = admin.firestore().collection("counters").doc("items");
exports.increment = functions.firestore.document('items/{itemId}').onCreate(async (change, context) => {
await counter.set({ total: admin.firestore.FieldValue.increment(1) }, { merge: true });
const current = await counter.get();
console.log(`incremented counter to ${current.data().total}`);
});
The first function, createItem, is an HTTP trigger that adds a document to the items/ collection. The second function is a Firestore trigger that increments a field in the counters/items document whenever a document is created.
When I run
curl -X POST http://localhost:5001/ryanpbrewster-test/us-central1/createItem
it creates a document and sends a response like
added LGOCapHSQtlXKIMEA8Do
This triggers the other function, which I can see in the emulator logs:
I function: Beginning execution of "increment"
> incremented counter to 6
I function: Finished "increment" in ~1s

Firebase Cloud Functions - move data in Firestore

Have a very basic understanding of the Typescript language, but would like to know, how can I copy multiple documents from one firestore database collection to another collection?
I know how to send the request from the app's code along with the relevant data (a string and firebase auth user ID), but unsure about the Typescript code to handle the request...
Thats a very broad question, but something like this can move data in moderate sizes from one collection to another:
import * as _ from 'lodash';
import {firestore} from 'firebase-admin';
export async function moveFromCollection(collectionPath1: string, collectionPath2: string): void {
try {
const collectionSnapshot1Ref = firestore.collection(collectionPath1);
const collectionSnapshot2Ref = firestore.collection(collectionPath2);
const collectionSnapshot1Snapshot = await collectionSnapshot1Ref.get();
// Here we get all the snapshots from collection 1. This is ok, if you only need
// to move moderate amounts of data (since all data will be stored in memory)
// Now lets use lodash chunk, to insert data in batches of 500
const chunkedArray = _.chunk(collectionSnapshot1Snapshot.docs, 500);
// chunkedArray is now an array of arrays, with max 500 in each
for (const chunk of chunkedArray) {
const batch = firestore.batch();
// Use the batch to insert many firestore docs
chunk.forEach(doc => {
// Now you might need some business logic to handle the new address,
// but maybe something like this is enough
const newDocRef = collectionSnapshot2Ref.doc(doc.id);
batch.set(newDocRef, doc.data(), {merge: false});
});
await batch.commit();
// Commit the batch
}
console.log('Done!');
} catch (error) {
console.log(`something went wrong: ${error.message}`);
}
}
But maybe you can tell more about the use case?

running queries in firebase using async / await

Appreciating that firebase has added support for promises, is there a way to run a query like the following inside of an async function?:
const eventref = this.db.ref('cats/whiskers');
const value = await eventref.once('value')
Running the above returns a promise for value, I'm hoping to get the json blob that is stored at cats/whiskers.
The result of value is a snapshot, we need 1 more step to get the value. This should be like:
const eventref = this.db.ref('cats/whiskers');
const snapshot = await eventref.once('value');
const value = snapshot.val();

Resources