Cloud Function finished with status: 'timeout' while adding data to Firestore - node.js

Here's the code,
exports.onEmailRecieved = functions.database
.ref("/emails/recieved/{id}/")
.onCreate(async (snapshot, context) => {
const email = snapshot.val();
const id = context.params.id;
const trimmedEmailBody = String(email.body).replace("\n", "").trim();
if (trimmedEmailBody === cmd) {
const queueRef = fs.collection("requests").doc("all").collection("queue");
await fs
.runTransaction(async (t) => {
const doc = await t.get(queueRef);
const size = doc.size;
console.log(`Size: ${size}`);
console.log("Adding to queue.");
await queueRef
.add({
email: email.email,
subject: email.subject,
body: email.body,
})
.then(() => {
console.log("Successfully added to queue.");
})
.catch((err) => {
console.log(err);
})
.finally(() => {
console.log("It's finally over.");
});
return console.log("Worked?");
})
.then(() => {
return console.log("Complete");
})
.catch((err) => {
return console.log(err);
});
return console.log("Worked I guess.");
} else {
return console.log("Not equal.");
}
});
Don't mind the bunch of useless console.logs. Added em to debug the error.
That first console.log gets called and then nothing, no then, catch or finally functions get triggered and I get a function finished with status: 'timeout' message in the logs.
What I'm doing wrong?

The add() method returns a promise which then when you await a promise, the function is paused in a non-blocking way until the promise settles. It will wait for the transaction to be finished before resolving the creation of the document which results in timeout of the cloud function which by default is 1min. By removing the await on the add method you're instead executing the function. See code below:
messageRef
.add({
email: "email",
subject: "subj",
body: "body",
})
.then(() => {
console.log("Successfully added to queue.");
})
.catch((err) => {
console.log(err);
})
.finally(() => {
console.log("It's finally over.");
});
This will now return something like this:
Size: 1
Adding to queue.
test
Successfully added to queue.
It's finally over.
For more relevant information, you may check this documentations:
Sync, async, and promises
CollectionReference
How to use promises

Related

what is the best way to get data inside a cloud-function in case you don't want to send a response?

I Have the following function and I want to do a conditional inside of the snapshot and then do some actions,
the current issue is I a can see the first console.log in the logs but the the function is not proceeding in to the snapshot for some reason what is the best way to get data inside a cloud-function ? in case you don't want to send them as response ?
export const deleteClient = functions.https.onCall((data, context) => {
console.log(context.auth?.uid, data.clientAccountId, ':::::::::::::ID1 + ID2:::::::::::::');
db.collection('ClientsData')
.doc(data.clientAccountId)
.get()
.then((snapshot) => {
console.log(context.auth?.uid, data.clientAccountId, ':::::::::::::snapshot.data()?.sharedId:::::::::::::');
if (context.auth?.uid! === snapshot.data()?.sharedId) {
admin
.auth()
.deleteUser(data.clientAccountId)
.then(() => {
console.log('Successfully deleted user');
})
.catch((error: any) => {
console.log('Error deleting user:', error);
});
db.collection('ClientsData').doc(data.clientAccountId).delete();
}
})
.catch((err) => {});
});
If you do not want to return anything from the function, you can simply return null. Also you should log any errors in the catch block so you'll know if something is wrong. Try refactoring the code using async-await syntax as shown below:
export const deleteClient = functions.https.onCall(async (data, context) => {
try {
console.log(context.auth?.uid, data.clientAccountId, ':::::::::::::ID1 + ID2:::::::::::::');
const snapshot = await db.collection('ClientsData').doc(data.clientAccountId).get()
console.log(context.auth?.uid, data.clientAccountId, ':::::::::::::snapshot.data()?.sharedId:::::::::::::');
if (context.auth?.uid! === snapshot.data()?.sharedId) {
await Promise.all([
admin.auth().deleteUser(data.clientAccountId),
db.collection('ClientsData').doc(data.clientAccountId).delete()
]);
}
} catch (e) {
console.log(e)
}
return null;
});

Store data in an array using for each in node js sequelize

I am trying to push the fetched data in an array using foreach but it only returns the first data in the loop. Here is my code.
exports.getAllTrial = async function (req, res, next) {
try {
new Promise( async (resolve, reject) => {
var reservations = [];
await Schedule.getSchedule()
.then(data => {
data.forEach(async (element) => {
await saveReserve.getAllTrial({where: {scheduleID: element.id, date: "8/18/2020"}})
.then(trial => {
trial.forEach(response => {
reservations.push(response.scheduleID)
})
})
console.log(reservations);
resolve(reservations);
})
});
})
.then(value=>{
res.status(200).json(value);
})
.catch(err => {
console.log(err);
});
} catch (e) {
return res.status(400).json({ status: 400, message: e.message });
}
}
My expected output should be: [ 9, 10, 10 ] But it only returns [9].
Async code in a foreach loop is a bad idea, as it won't be executed one after the other. I suggest reading a bit more async/await and the concept of promise, as you are mixing things here (such as mixing await and .then). Also worth looking into Promise.all which will resolve a list of promises and array.map.
While I have no idea of what some variables such as saveReserve are supposed to be or do, your code might be simplified into:
exports.getAllTrial = async (req, res, next) => {
try {
const data = await Schedule.getSchedule()
const reservations = await Promise.all(
data.map(element => {
return saveReserve.getAllTrial({ where: { scheduleID: element.id, date: '8/18/2020' } })
})
)
return res.status(200).json(reservations)
} catch (e) {
return res.status(400).json({ status: 400, message: e.message })
}
}

How to create a new field in firestore using cloud functions?

I am a newbie in cloud functions. I want to create a new field counter whenever a document is created.
I tried the following codes:
exports.createCounter = functions.firestore.document('users/{userId}').onCreate((snap,
context) => {
console.log('onCreate created');
return snap.data.ref.set({counter: 0}, {merge: true})
.then(() => {
console.log("Count is created! "+userId);
})
.catch((error) => {
console.error("Counter Error writing document: ", error);
});
});
and using firebase-admin:
exports.createCounter = functions.firestore.document('users/{userId}').onCreate((snap, context) => {
const id = context.params.userId;
console.log('onCreate created');
return admin.firestore().collection('users')
.doc(id).set({counter: 0}, {merge: true})
.then(() => {
console.log("Document successfully written! "+id);
})
.catch((error) => {
console.error("Error writing document: ", error);
});
});
But both are not triggered when a new document is created.
UPDATE
At first, I create users/userId/collection/doc. When it is created, I want to add a counter field into users/userId
Update following your comment
You can add a field to the users/userId document when you create a doc under the users/userId/theCollection collection by modifying the code of the second solution presented below.
Just trigger at the level of the subcollection, get the parent document id through the context object and build its DocumentReferencebased on this id, as follows:
exports.createCounter = functions.firestore.document('users/{userId}/theCollection/{docId}').onCreate((snap,
context) => {
const id = context.params.userId;
return admin.firestore().collection('users')
.doc(id).set({ counter: 0 }, { merge: true })
.then(() => {
console.log("Count is created! " + userId);
return null;
})
.catch((error) => {
console.error("Counter Error writing document: ", error);
return null;
});
});
Initial answer
The following should do the trick:
exports.createCounter = functions.firestore.document('users/{userId}').onCreate((snap,
context) => {
console.log('onCreate created');
const docRef = snap.ref;
return docRef.set({counter: 0}, {merge: true})
.then(() => {
console.log("Count is created! "+userId);
return null;
})
.catch((error) => {
console.error("Counter Error writing document: ", error);
return null;
});
});
Note that instead of doing snap.data.ref to get the DocumentReference you have to do snap.ref. As a matter of fact, snap is a DocumentSnapshot and you need to use its ref property.
Your second code snippet should normally work, but you need to return a value in the then() and in the catch() to indicate to the Cloud Function platform that the work of the function is complete. Note that you need to do the same thing for the above code. I would suggest you watch the 3 videos about "JavaScript Promises" from the Firebase video series: https://firebase.google.com/docs/functions/video-series/ which explain this key point.
exports.createCounter = functions.firestore.document('users/{userId}').onCreate((snap, context) => {
const id = context.params.userId;
console.log('onCreate created');
return admin.firestore().collection('users')
.doc(id).set({counter: 0}, {merge: true})
.then(() => {
console.log("Document successfully written! "+id);
return null;
})
.catch((error) => {
console.error("Error writing document: ", error);
return null;
});
});

Jest how to test express API POST request?

I need to test if my POST request to my endpoint works properly with a Jest test. I had the idea of first getting the count of my Services table (I'm using sequelize orm), then to send a new post request and to finally get the new count and compare if the old count + 1 will equal to the new count, if true then the POST request works just fine.
test('Create a valid Service', async (done) => {
const service = {
name: "cool",
description: "description"
};
await Service.count().then(async function (count) {
await request(app)
.post('/api/services')
.send(service)
.then(async () => {
await Service.count().then(function (newcount) {
expect(newcount).toBe(count + 1);
});
})
.catch(err => console.log(`Error ${err}`));
});
});
For me the test looks fine, but when I run it I get:
Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.
Is something missing or is there even a better way to test a POST request? with Jest?
It is because you are not calling the done callback passed in jest callback function. It can be done like this.
test('Create a valid Service', async(done) => {
const service = {
name: "cool",
description: "description"
};
await Service.count().then(async function (count) {
await request(app)
.post('/api/services')
.send(service)
.then(async() => {
await Service.count().then(function (newcount) {
expect(newcount).toBe(count + 1);
// execute done callback here
done();
});
})
.catch(err => {
// write test for failure here
console.log(`Error ${err}`)
done()
});
});
});
You can also write this code in this way so that the readability can be improved and maximize the use of async/await.
test('Create a valid Service', async(done) => {
const service = {
name: "cool",
description: "description"
};
try {
const count = await Service.count();
await request(app).post('/api/services').send(service)
const newCount = await Service.count()
expect(newCount).toBe(count + 1);
done()
} catch (err) {
// write test for failure here
console.log(`Error ${err}`)
done()
}
});
By default Jest also resolves the promise in async/await case. We can achieve this without the callback function also
test('Create a valid Service', async() => {
const service = {
name: "cool",
description: "description"
};
try {
const count = await Service.count();
await request(app).post('/api/services').send(service)
const newCount = await Service.count()
expect(newCount).toBe(count + 1);
} catch (err) {
// write test for failure here
console.log(`Error ${err}`)
}
});

What's wrong with this use of async await?

I am trying to download tracks via the soundcloud API, and then launch a callback once an indeterminant amount of tracks is downloaded. When I run the below code, I see "All done" being console logged before anything else, even though I intend for it to be the last thing... What am I doing wrong?
// Deps
import fs from 'fs'
import SC from 'node-soundcloud'
import request from 'request'
// Write mp3 function
function writeMP3(track) {
return new Promise((resolve, reject) => {
console.log('Starting download: ', track.title)
request.get(track.download_url)
.on('error', err => {
// reject('Download error: ', err)
})
.on('finish', () => {
() => resolve('Download complete')
})
.pipe(fs.createWriteStream(`./data/temp/${track.title}_${track.user.username}.mp3`))
})
}
async function asyncTrackFetch(track) {
return await writeMP3(track)
}
// Array of promises to callback upon
const trackActions = []
SC.init({
id: 'MY_ID',
secret: 'MY_SECRET'
})
SC.get('/tracks', (err, tracks) => {
if (err) {
throw new Error(err)
} else {
console.log('Tracks fetched: ', tracks.length)
tracks.map(track => {
if (track.downloadable) {
console.log('downloadable')
trackActions.push(asyncTrackFetch(track))
}
})
}
})
// Perform requests async
Promise.all(trackActions).then(() => {
console.log('All done')
console.log(fs.readdirSync('./data/temp'))
})
Promise.all(trackActions) waits on whatever promises are in trackActions, but trackActions is empty at the time you make the call. You're only adding promises to the array after your SC.get callback gets called.
Try putting your Promise.all... block inside the SC.get callback like this:
SC.get('/tracks', (err, tracks) => {
if (err) {
throw new Error(err)
} else {
console.log('Tracks fetched: ', tracks.length)
tracks.map(track => {
if (track.downloadable) {
console.log('downloadable')
trackActions.push(asyncTrackFetch(track))
}
})
Promise.all(trackActions).then(() => {
console.log('All done')
console.log(fs.readdirSync('./data/temp'))
})
}
})
It's worth mentioning as well that your line throw new Error(err) will crash the program since there's nowhere for that error to be caught.
As Antonio Val mentioned, there are better ways to do this. If you promisify the node-soundcloud library then the last part of your code could look like this:
SC.get('/tracks').then(tracks => {
// No need for trackedActions array.
return Promise.all(tracks.filter(track => track.downloadable)
.map(track => asyncTrackFetch(track)))
}).then(fetchedTracks => {
console.log('All done fetching tracks', fetchedTracks)
}).catch(err => {
// Handle error.
})
Or inside an async function,
try {
const tracks = await SC.get('/tracks')
const fetchPromises = tracks
.filter(track => track.downloadable)
.map(track => asyncTrackFetch(track))
const fetchedTracks = await Promise.all(fetchPromises)
console('All done fetching tracks.', fetchedTracks)
} catch (err) {
// Handle error
}
I think the easiest way would be to move Promise.all after tracks.map loop finished.
A more elegant solution would be to promisify SC.get as well and use async await along all your code.
UPDATE:
Couldn't test it so not sure if it works, but it would be something like this:
import fs from 'fs'
import SC from 'node-soundcloud'
import request from 'request'
function writeMP3(track) {
return new Promise((resolve, reject) => {
console.log('Starting download: ', track.title)
request.get(track.download_url)
.on('error', err => {
// reject('Download error: ', err)
})
.on('finish', () => {
() => resolve('Download complete')
})
.pipe(fs.createWriteStream(`./data/temp/${track.title}_${track.user.username}.mp3`))
})
}
function getTracks() {
return new Promise((resolve, reject) => {
SC.get('/tracks', (err, tracks) => {
if (err) {
return reject(err)
}
console.log('Tracks fetched: ', tracks.length)
resolve(tracks)
})
})
}
SC.init({
id: 'MY_ID',
secret: 'MY_SECRET'
})
With async await:
async function start() {
const tracks = await getTracks();
for (let track of tracks) {
await writeMP3(track)
}
}
start()
.then(() => {
console.log('All done')
console.log(fs.readdirSync('./data/temp'))
})
.catch((err) => {
// insert error handler here
})
If you just want to use Promises:
getTracks
.then((tracks) => {
const promiseArray = tracks.map((track) => {
return writeMP3(track)
})
return Promise.all(promiseArray)
})
.then(() => {
console.log('All done')
console.log(fs.readdirSync('./data/temp'))
})
.catch((err) => {
// insert error handler here
})

Resources