update firestore document field once created using cloud function - node.js

In one version of my published application, there is a bug that creates a document in my collection, and in one of the fields in this document (called "Popularity"), the field equals a string "null" instead of a string of a number, say "4.5".
What it causes is that on the client-side when a user reads the data and tries to use Double.parseDouble("null") it gives an error while it should have used Double.parseDouble("4.5").
I want to add some cloud function trigger that will listen to any document that is created in that collection and if the created document has this field that equals to "null", to update it to "0.0".
My firestore is built as follows:
Users (collection) - > userId (document) -> fields (Popularity, ID, Title)
I'm new to cloud functions and I am not sure if I use correctly the .update in the end as all I could find is examples for .onUpdate and not for .onCreate examples.
I tried to use the following:
const functions = require('firebase-functions');
exports.createUser = functions.firestore
.document('Users/{userId}')
.onCreate((snap, context) => {
const newValue = snap.data();
const Popularity = newValue.Popularity;
if (Popularity != "null") {
return null;
}
if (Popularity == "null") {
return snap.update({
Popularity: "0.0"
}, {merge: true});
}
});
But I got the following error in my log:
TypeError: snap.update is not a function
and
TypeError: snap.update is not a function
I also tried to use:
exports.createUser = functions.firestore
.document('Users/{bookId}')
.onWrite((change, context) => {
const data = change.after.data();
const previousData = change.before.data();
const oldDocument = change.before.data();
if (data.Popularity != 'null' && previousData.Popularity != 'null') {
return null;
}
if (data.Popularity == 'null' || previousData.Popularity == 'null') {
return change.after.ref.set({Popularity: '0.0'}, {merge: true});
}
});
But then I got:
TypeError: Cannot read property 'Popularity' of undefined
Is there anything else I'm missing here?
Thank you

It looks like snap is a DocumentSnapshot object, which (if you check its documentation) does indeed not have an update() method. To update the document that the snapshot came from, you'll want to call snap.ref.update(...).

Related

Error: Value for argument "documentPath" is not a valid resource path. Path must be a non-empty string. /*what is empty or not a string*/

this is the error that I get. I checked multiple times that the paths that I indicate are actually pointing at something in the database. I'm kinda going crazy about why this is not working, so help will be appreciated. (the same error is given by both functions, two times every invocation of the function)
this is my code:
exports.onCreatePost = functions.firestore
.document('/time/{userid}/date/{postId}')
.onCreate (async (snapshot, context) => {
const postCreated = snapshot.data();
const userID = context.params.userid;
const postID = context.params.postId;
//get all the followers who made the post
const userFollowerRef = admin.firestore().collection('time').doc(userID).collection('followers');
const querySnap = await userFollowerRef.get();
//add the post in each follower timeline
querySnap.forEach(doc => {
const followerid = doc.id;
admin.firestore().collection('time').doc(followerid).collection('timelinePosts').doc(postID).set(postCreated);
})
});
//when a post is updated
exports.onUpdatePost = functions.firestore
.document('/time/{userid}/date/{postid}')
.onUpdate(async (change, context) => {
const postUpdated = change.after.data();
const userID = context.params.userid;
const postID = context.params.postId;
//get all the followers who made the post
const userFollowerRef = admin.firestore().collection('time').doc(userID).collection('followers');
const querySnap = await userFollowerRef.get();
//Update the post in each follower timeline
querySnap.forEach(doc => {
const follower = doc.id;
admin.firestore().collection('time').doc(follower).collection('timelinePosts').doc(postID)
.get().then(doc => {
if (doc.exists) {
doc.ref.update(postUpdated);
}
});
});
});
I personally don't know how to log each variable and did not find how to do it online. I'll keep searching but in the mindtime I can share my extensive logs that from my interpretation are not very useful but maybe is just because I'm inexperienced.
this is the error log
enter image description here
In function exports.onUpdatePost ...you're likely trying to access documentPath null (or something alike that). Add logging, this permits to log custom debug information into the log which you've screenshotted. When logging every step of the procedure, it's a whole lot easier to determine what is happening and why - or why not, when skipping something. Alike this you should be able to solve the issue on your own. My functions logging actually utilizes emojis, because UTF-8 is being supported: ✅❌ (visual indicators make the log more readable).
The cause seems to be one of these instructions:
admin.firestore().collection('time') // it is being assumed that it exists
.doc(userID) // argument or return value may be null
.collection('followers') // return value may be null
Or:
admin.firestore().collection('time') // it is being assumed that it exists
.doc(follower) // argument or return value may be null
.collection('timelinePosts') // return value may be null
.doc(postID) // argument or return value may be null
eg. one first has to check if follower != null or empty and if the desired document even exists. The same goes for userID and .doc(userID) (the seemingly "own" timeline).
if (follower != null && follower.length > 0) {
admin.firestore().collection('time').doc(follower).get().then(timeline => {
functions.logger.log('timeline: ' follower + ', ' + timeline.exists);
if (timeline.exists) {
} else {
}
});
}
documentPath == null comes from .doc() + userID, followerid, follower or postID.

denormalizing in firebase functions - copy data from other docs on create

I'm trying to figure out how to create a merged/denormalized document for an order document in firebase, as described in the "Five Uses for Cloud Functions" Firebase video (https://www.youtube.com/watch?v=77XmRDtOL7c)
User creates the basic document, functions pulls in data from several other documents to create the desired result.
Here's a basic example of what I'd like to accomplish.
exports.orderCreate = functions.firestore
.document('orders/{docId}').onCreate((snap, context) => {
const id = context.params.docId;
const orderDoc = snap.data();
const branchId = orderDoc.branchId;
const branchDoc = admin.firestore().collection('branches').doc(branchId);
const bn = branchDoc.brandName;
const ln = branchDoc.locationName;
const logo = branchDoc.imageURL;
return admin.firestore().collection('orders')
.doc(id).set({
branchBrandName: bn,
branchLocationName: ln,
branchLogo: logo
}, { merge: true });
});
Which way do I wave my hands to make this work? Thanks!
With admin.firestore().collection('branches').doc(branchId); you actually declare a DocumentReference. Then, in order to get the values of the document fields, you need to call the asynchronous get() method.
So the following should do the trick:
exports.orderCreate = functions.firestore
.document('orders/{docId}').onCreate((snap, context) => {
const id = context.params.docId;
const orderDoc = snap.data();
const branchId = orderDoc.branchId;
const branchDoc = admin.firestore().collection('branches').doc(branchId);
return branchDoc.get()
.then(branchDocSnapshot => {
const bn = branchDocSnapshot.data().brandName;
const ln = branchDocSnapshot.data().locationName;
const logo = branchDocSnapshot.data().imageURL;
return admin.firestore().collection('orders')
.doc(id).set({
branchBrandName: bn,
branchLocationName: ln,
branchLogo: logo
}, { merge: true });
});
});
You may need to deal with the case the doc does not exists, depending on your data model and app functions. See here in the doc.

Index messed up if I upload more than one file at once

I've got the following firebase function to run once a file is uploaded to firebase storage.
It basically gets its URL and saves a reference to it in firestore. I need to save them in a way so that I can query them randomly from my client. Indexes seem to be to best fit this requirement.
for the firestore reference I need the following things:
doc ids must go from 0 to n (n beeing the index of the last
document)
have a --stats-- doc keeping track of n (gets
incremented every time a document is uploaded)
To achieve this I've written the following node.js script:
const incrementIndex = admin.firestore.FieldValue.increment(1);
export const image_from_storage_to_firestore = functions.storage
.object()
.onFinalize(async object => {
const bucket = gcs.bucket(object.bucket);
const filePath = object.name;
const splittedPath = filePath!.split("/");
// se siamo nelle immagini
// path = emotions/$emotion/photos/$photographer/file.jpeg
if (splittedPath[0] === "emotions" && splittedPath[2] === "photos") {
const emotion = splittedPath[1];
const photographer = splittedPath[3];
const file = bucket.file(filePath!);
const indexRef = admin.firestore().collection("images")
.doc("emotions").collection(emotion).doc("--stats--");
const index = await indexRef.get().then((doc) => {
if (!doc.exists) {
return 0;
} else {
return doc.data()!.index;
}
});
if (index === 0) {
await admin.firestore().collection("images")
.doc("emotions")
.collection(emotion)
.doc("--stats--")
.set({index: 0});
}
console.log("(GOT INDEX): " + index);
let imageURL;
await file
.getSignedUrl({
action: "read",
expires: "03-09-2491"
})
.then(signedUrls => {
imageURL = signedUrls[0];
});
console.log("(GOT URL): " + imageURL);
var docRef = admin.firestore()
.collection("images")
.doc("emotions")
.collection(emotion)
.doc(String(index));
console.log("uploading...");
await indexRef.update({index: incrementIndex});
await docRef.set({ imageURL: imageURL, photographer: photographer });
console.log("finished");
return true;
}
return false;
});
Getting to the problem:
It works perfectly if I upload the files one by one.
It messes up the index if I upload more than one file at once, because two concurrent uploads will read the same index value from --stats-- and one will overwrite the other.
How would you solve this problem? would you use another approach instead of the indexed one?
You should use a Transaction in which you:
read the value of the index (from "--stats--" document),
write the new index and
write the value of the imageURL in the "emotion" doc.
See also the reference docs about transactions.
This way, if the index value is changed in the "--stats--" document while the Transaction is being executed, the Cloud Function can catch the Transaction failure and generates an error which finishes it.
In parallel, you will need to enable retries for this background Cloud Function, in order it is retried if the Transaction failed in a previous run.
See this documentation item https://firebase.google.com/docs/functions/retries, including the video from Doug Stevenson which is embedded in the doc.

Firebase check if database exists

I have a firebase database of form: https://imgur.com/ar8A3DN
I would like two functions:
1. refExists, check if any child exists in database. So that
refExists('datasets') = true
refExists('foo') = false
createChild that creates a new child.
My firebase database instance is declared via:
const accountKeyPath = path.join(__dirname, 'path/to/serviceAccountKey.json')
const accountKey = require(accountKeyPath);
const firebaseAdmin = admin.initializeApp(accountKey);
const dbRef = firebaseAdmin.database().ref('datasets');
The interesting thing is that dbRef and this code which should return an error:
const badRef = firebaseAdmin.database().ref('foo')
both output the same thing. So it's unclear how to check the existence of foo when ref('datasets') and ref('foo') behave the same way.
The way to check whether an element exists is by trying to retrieve a snapshot of it - if the snapshot returns null then the element does not exist.
Adding elements is as simple as calling set on the desired element path with a data object.
function refExists(path) {
firebaseAdmin.database().child(path).once('value', (snap) => {
if (snap.val() !== null) {
console.log('ref exists');
}
});
}
function addRef(newPath, data) {
firebaseAdmin.database().child(newPath).set(data);
}

Stubbing virtual attributes of Mongoose model

Is there a way to stub a virtual attribute of a Mongoose Model?
Assume Problem is a model class, and difficulty is a virtual attribute. delete Problem.prototype.difficulty returns false, and the attribute is still there, so I can't replace it with any value I want.
I also tried
var p = new Problem();
delete p.difficulty;
p.difficulty = Problem.INT_EASY;
It didn't work.
Assigning undefined to Problem.prototype.difficulty or using sinon.stub(Problem.prototype, 'difficulty').returns(Problem.INT_EASY);
would throw an exception "TypeError: Cannot read property 'scope' of undefined", while doing
var p = new Problem();
sinon.stub(p, 'difficulty').returns(Problem.INT_EASY);
would throw an error "TypeError: Attempted to wrap string property difficulty as function".
I am running out of ideas. Help me out! Thanks!
mongoose internally uses Object.defineProperty for all properties. Since they are defined as non-configurable, you can't delete them, and you can't re-configure them, either.
What you can do, though, is overwriting the model’s get and set methods, which are used to get and set any property:
var p = new Problem();
p.get = function (path, type) {
if (path === 'difficulty') {
return Problem.INT_EASY;
}
return Problem.prototype.get.apply(this, arguments);
};
Or, a complete example using sinon.js:
var mongoose = require('mongoose');
var sinon = require('sinon');
var problemSchema = new mongoose.Schema({});
problemSchema.virtual('difficulty').get(function () {
return Problem.INT_HARD;
});
var Problem = mongoose.model('Problem', problemSchema);
Problem.INT_EASY = 1;
Problem.INT_HARD = 2;
var p = new Problem();
console.log(p.difficulty);
sinon.stub(p, 'get').withArgs('difficulty').returns(Problem.INT_EASY);
console.log(p.difficulty);
As of the end of 2017 and the current Sinon version, stubbing only some of the arguments (e.g. only virtuals on mongoose models) can be achieved in the following manner
const ingr = new Model.ingredientModel({
productId: new ObjectID(),
});
// memorizing the original function + binding to the context
const getOrigin = ingr.get.bind(ingr);
const getStub = S.stub(ingr, 'get').callsFake(key => {
// stubbing ingr.$product virtual
if (key === '$product') {
return { nutrition: productNutrition };
}
// stubbing ingr.qty
else if (key === 'qty') {
return { numericAmount: 0.5 };
}
// otherwise return the original
else {
return getOrigin(key);
}
});
The solution is inspired by a bunch of different advises, including one by #Adrian Heine

Resources