How to update document in firebase cloud function - firebase

In my cloud function I want to update my document from 'dashboard' collection when a new student added to 'students' collection.
const getActiveStudents = () => {
return db.collection('/students/').where('status', '==', true).get().then(
snapshot => {
let studentsCount = snapshot.docs.length;
db.collection('/dashboard/').where('type', '==', 'students').get().then(
result => {
if (result.docs.length === 0) {
db.collection('dashboard').add({
count: studentsCount,
type: 'students',
label: 'Active students'
});
}else {
result.docs[0].ref.update({
count: studentsCount,
type: 'students',
label: 'Active students'
});
}
return result;
}
).catch(error => {
console.log(error);
});
return snapshot;
}
).catch(error => {
console.log(error);
})
}
exports.onChangesInStudents = functions.firestore.document('/students/{studentId}').onWrite(event => {
getActiveStudents();
return;
});
When I add a new student, instead of updating document it adds a new document to my 'dashboard' collection.
How should I organize my code in order to properly update the quantity of students.

as #Doug mentioned, iterating over the entire collection is too heavy. instead you can stream the query results and iterate over keys, using query.stream().
to access and update a single field in a document, first retrieve the document by its ID with doc(), then use update() while specifying the field.
here's an example of implementation based on your scenario.
package.json
{
"dependencies": {
"firebase-admin": "^6.5.1",
"firebase-functions": "^2.1.0"
}
}
index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
const studentsRef = admin.firestore().collection('students');
const dashboardRef = admin.firestore().collection('dashboard');
exports.addStudent = functions.firestore
.document('students/{studentId}')
.onCreate((snap, context) => {
var newStudent = snap.data();
console.log('New student in collection: ', newStudent);
var activeCount = 0;
studentsRef.where('status', '==', true).select().stream()
.on('data', () => {
++activeCount;
}).on('end', () => {
dashboardRef.where('type', '==', 'students').get()
.then(querySnap => {
if (querySnap.docs[0].data().count == activeCount){
console.log('No new active student: ', querySnap.docs[0].data());
} else {
console.log('New active count: ', activeCount);
console.log('Student Dashboard before update: ', querySnap.docs[0].id, '=>', querySnap.docs[0].data());
dashboardRef.doc(querySnap.docs[0].id).update({
count: activeCount
});
console.log('Active student count updated: ', querySnap.docs[0].data().count, '=>', activeCount);
};
});
});
return null
});
gcloud
gcloud functions deploy addStudent \
--runtime nodejs8 \
--trigger-event providers/cloud.firestore/eventTypes/document.create \
--trigger-resource "projects/[PROJECT_ID]/databases/(default)/documents/students/{studentId}"

When a function is triggered, you might want to get data from a document that was updated, or get the data prior to update.
You can get the prior data by using change.before.data(), which contains the document snapshot before the update.
Similarly, change.after.data() contains the document snapshot state after the update.
Node.js
exports.updateUser = functions.firestore
.document('users/{userId}')
.onUpdate((change, context) => {
// Get an object representing the current document
const newValue = change.after.data();
// ...or the previous value before this update
const previousValue = change.before.data();
//...therefore update the document as.
admin.firestore().collection('user').doc(docId).update(snapshot.after.data());
});
Reference:-
https://firebase.google.com/docs/functions/firestore-events

Related

Firestore - How to store current user data and keep previews one?

I'm trying to store user data on Firestore which is I have multiple things to add such as (taskIndex,levelName,step,steps) and I did it successfully also I'm getting that user info after refresh or killing the app, but the problem here is that on my App I have multiple indexes and Levels and each of them has their special steper, once I add current user data previews one gets deleted, so how can I fix this?
This is what i did for storing the data
//Set userInfo
useEffect(() => {
const setUser = async () => {
await setDoc(doc(db, 'user', uid), {
uid: uid,
step: step,
steps: steps,
taskIndex: taskIndex,
levelName: levelName,
});
};
And getting data
// Get userInfo
const userRef = collection(db, 'user');
useEffect(() => {
const getUserInfo = async () => {
const data = await getDocs(userRef);
setUserData(data.docs.map((doc) => ({ ...doc.data(), id: doc.id })));
};
getUserInfo();
}, []);
useEffect(() => {
const userInfo =
userData &&
userData.map((items) => {
setStep(items.step);
setSteps(items.steps);
setTaskIndex(items.taskIndex);
setLevelName(items.levelName);
});
}, [userData]);

How to get all items from subcollection Firebase Firestore Vue

How do I get all the comments from the subcollection?
This is mine reusable function to get comments collection.
import { ref, watchEffect } from 'vue';
import { projectFirestore } from '../firebase/config';
const getCollection = (collection, id, subcollection) => {
const comments = ref(null);
const error = ref(null);
// register the firestore collection reference
let collectionRef = projectFirestore
.collection(collection)
.doc(id)
.collection(subcollection);
const unsub = collectionRef.onSnapshot(
snap => {
let results = [];
snap.docs.forEach(doc => {
doc.data().createdAt && results.push(doc.data());
});
// update values
comments.value = results;
error.value = null;
},
err => {
console.log(err.message);
comments.value = null;
error.value = 'could not fetch the data';
}
);
watchEffect(onInvalidate => {
onInvalidate(() => unsub());
});
return { error, comments };
};
export default getCollection;
And this is mine Comments.vue where i passing arguments in setup() function (composition API)
const { comments } = getAllComments('posts', props.id, 'comments');
When i console.log(comments) its null, in snapshot doc.data() is good but somehow results too is empty array even if i push doc.data() to results array and pass it to comments.value.
Can someone help me how to get that subcollection?
This is my Comment.vue component
export default {
props: ['id'],
setup(props) {
const { user } = getUser();
const content = ref('');
const { comments } = getAllComments('posts', props.id, 'comments');
const ownership = computed(() => {
return (
comments.value && user.value && user.value.uid == comments.value.userId
);
});
console.log(comments.value);
}
return { user, content, handleComment, comments, ownership };
},
};
const getCollection = (collection, id, subcollection) => {
const comments = ref(null);
const error = ref(null);
// Firestore listener
return { error, comments };
}
The initial value of comments here is null and since Firebase operations are asynchronous, it can take a while before the data loads and hence it'll log null. If you are using comments in v-for then that might throw an error.
It'll be best if you set initial value to an empty array so it'll not throw any error while the data loads:
const comments = ref([]);
Additionally, if you are fetching once, use .get() instead of onSnapshot()

Firestore batch delete don't work while using emulator with react-native

I want to try some code with firestore emulator before using it in production, I want basically to retrieve a collection documents sort them and set them again in the collection:
I have this error while doing a batch delete :
[Error: [firestore/permission-denied] The caller does not have permission to execute the specified operation.]
the code:
useEffect(() => {
(async () => {
await admin_sortUserRanksDB()
})()
}, [])
const admin_sortUserRanksDB = async () => {
const usersData = await admin_getUserDataDBAndClean()
populateUserCollection(usersData)
}
const admin_getUserDataDBAndClean = async () => {
try {
const querySnapshot = await firestore()
.collection('users')
.orderBy('experience_amount', 'desc')
.get();
let rank = 1;
let newDataUsers = [];
for (const user of querySnapshot.docs) {
const userData = user.data();
userData.rank = rank;
newDataUsers.push(userData)
rank++
}
await deleteUserCollection(querySnapshot)
return newDataUsers;
} catch (error) {
if (!__DEV__) {
crashlytics().log(
`error getUserDataDB()
userActions.js ===>> ${error.message}`
);
}
console.log('error getUserDataDB ', error)
return null
}
}
const deleteUserCollection = async (usersQuerySnapshot) => {
// Create a new batch instance
const batch = firestore().batch();
usersQuerySnapshot.forEach(documentSnapshot => {
batch.delete(documentSnapshot.ref);
});
console.log('==============')
return batch.commit();
}
const populateUserCollection = usersData => {
if (usersData) {
const batch = firestore().batch();
usersData.forEach(doc => {
let docRef = firestore()
.collection('users')
.doc(); //automatically generate unique id
batch.set(docRef, doc);
});
batch
.commit()
.catch(error => {
console.log('error populating users', error)
});
}
}
After posting an issue to react-native-firebase repo i was suggested to modify my rules to be open (only locally) and the batch delete worked.
I used the allow read, write: if true in firestore.rules file
link to issue on GitHub

Sometimes firebase cloud functions didn't execute

Hi I'm using a cloud function to aggregate data created on a sub-collection to the parent collection, when I test it it works like a charm, but when I deploy it to my production environment that sometimes (is not common but occurs) the data is no aggregated to the parent document.
I think that the function is not executed because I don't get any error on the logs.
Here is my function code
exports.aggregateTranlationsToSong = functions.firestore
.document("songs2/{songId}/translations/{langId}")
.onCreate((event, context) => {
const { songId, langId } = context.params;
console.log({ songId, langId });
let songRef = admin
.firestore()
.collection("songs2")
.doc(songId);
return admin
.firestore()
.runTransaction(transaction => {
return transaction.get(songRef).then(songSnap => {
let actualSongData = songSnap.data();
let translations = actualSongData.lyric_translations;
if (translations === undefined || translations === null)
translations = {};
translations[langId] = true;
console.log({ translations });
return transaction.update(songRef, {
lyric_translations: translations
});
});
})
.catch(e => {
console.error(e);
});
});

How to ensure that a cloud function is running every time a new document gets created?

I am uploading my questions and answers to my quiz to Firestore. For that I am using following function:
const firestore = admin.firestore();
const settings = { timestampsInSnapshots: true };
firestore.settings(settings);
if (data && (typeof data === "object")) {
Object.keys(data).forEach(docKey => {
var data_to_push = data[docKey];
data_to_push['category'] = "Wirtschaft";
firestore.collection(collectionKey).add(data_to_push).then((res) => {
console.log("Document " + docKey + " successfully written!");
}).catch((error) => {
console.error("Error writing document: ", error);
});
});
This function works fine, all the documents I need are created but whenever a document get created I have another function that is running:
// This function adds the doc ids of newly created questions to an arrayList
exports.AddKeyToArray = functions.region('europe-west1').firestore.document('Questions/{nameId}').onCreate(async (snp, context) => {
console.log(snp.id);
console.log(context.params);
await db.collection("Questions_keys").doc(snp.data().category).update({ "questions": admin.firestore.FieldValue.arrayUnion(snp.id) }).then(() => {
return console.log("Key added");
}).catch(async (e) => {
console.log(e);
if (e.code === 5) {
await db.collection("Questions_keys").doc(snp.data().category).set({ "questions": admin.firestore.FieldValue.arrayUnion(snp.id) }).then(() => {
return console.log("First time key added");
}).catch(e => {
return console.log(e);
})
}
})
return "okay";
})
This function basically gets the document id of the previously added question/answer and creates an array with all the document ids of that quiz category (so I then later can get a random question without much reading operations). The problem is that not all document ids are added to the array so I wanted to know if there is a better way to ensure that all the document ids are added into the array.
I upload sometimes 500 documents at once, would be a solution to reduce the documents I upload at once to ensure a better performance of the second function?
Any help is much appreciated!
I suggest that rather than using cloud functions here is to create another collection in your database. This way you can add more questions to that collection easily. This design will increase performance as what you will need is only query the new collection directly and this way you will avoid all the complication needed to manage and work with Cloud Functions.
With help I found a solution: The following function uploads data to firestore and gets the ids of the documents and sets it to an array:
...
const firestore = admin.firestore();
const settings = { timestampsInSnapshots: true };
firestore.settings(settings);
if (data && (typeof data === "object")) {
Object.keys(data).forEach(async docKey => {
var data_to_push = data[docKey];
data_to_push['category'] = "Deutschland";
await firestore.collection(collectionKey).add(data_to_push).then(async (res) => {
var key = (res['_path']['segments'][1]);
await firestore.collection("Questions_keys").doc(data_to_push['category']).update({ "questions": admin.firestore.FieldValue.arrayUnion(key) }).then(() => {
console.log("Key added: " + key);
}).catch(async (e) => {
if (e.code === 5) {
await firestore.collection("Questions_keys").doc(data_to_push['category']).set({ "questions": admin.firestore.FieldValue.arrayUnion(key) }).then(() => {
return console.log("First time key added");
}).catch(e => {
return console.log(e);
})
}
console.log(e);
})
}).catch((error) => {
console.error("Error writing document: ", error);
});
});
}

Resources