How to delete document collection and all nested data from auth.user.onDelete trigger - firebase

Currently, the logic for deleting user data is the following:
import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';
const firestore_tools = require('firebase-tools');
admin.initializeApp();
const Auth = admin.auth();
const UsersCollection = admin.firestore().collection(`users`);
exports.deleteUserDocuments = functions.auth.user().onDelete((user) => {
const userID = user.uid;
UsersCollection.doc(userID)
.delete({})
.catch(error => {
return error
});
});
But since the user document record contains nested collections that contain other documents and collections they are still preserved due to the fact:
When you delete a document, Cloud Firestore does not automatically delete the documents within its sub-collections
I've researched a bit and found a documentation on how to create a callable function:
https://firebase.google.com/docs/firestore/solutions/delete-collections
But I wonder is it possible to have this logic instead executed from the auth.user.onDelete trigger?
Update with the Solution
const firestore_tools = require('firebase-tools');
exports.deleteUserDocuments = functions.auth.user().onDelete((user) => {
const userID = user.uid;
const project = process.env.GCLOUD_PROJECT;
const token = functions.config().ci_token;
const path = `/users/${userID}`;
console.log(`User ${userID} has requested to delete path ${path}`);
return firestore_tools.firestore
.delete(path, {
project,
token,
recursive: true,
yes: true,
})
.then(() => {
console.log(`User data with ${userID} was deleted`);
})
});

You can run whatever code you want in whatever trigger you want. The type of the trigger doesn't have any bearing on the type of code you can run.

Related

Firebase Function Unable to Find userId and tweetId

I am using Firebase functions for Firestore database. I am trying to update a field based on the new tweet being added.
Here is my Firebase Function on production:
const admin = require('firebase-admin')
admin.initializeApp()
const db = admin.firestore()
const functions = require("firebase-functions");
functions.logger.log("START OF FUNCTION");
exports.myFunction = functions.firestore
.document('timelines/{userId}/tweets/{tweetId}')
.onCreate((change, context) => {
const userId = context.params.userId
const tweetId = context.params.tweetId
functions.logger.log(context.params.userId);
functions.logger.log(context.params.tweetId);
db.doc(`/timelines/${userId}/tweets/${tweetId}`).update({likeCount: 200})
})
I am triggering it through an iPhone app. I am logged in to my account and I add a new Tweet. The Firebase function does get called but userId and tweetId are undefined. I am not sure why they are undefined. Any ideas?
Without knowing your client-side logic it's difficult to know if there are other issues. I would suggest adding some error handling to narrow down the cause. You could also try pulling it from the data response instead of context (assuming the schema matches).
Also note using 'snap' instead of 'change' as change is generally reserved for 'onWrite' and 'onUpdate' hooks.
exports.myFunction = functions.firestore
.document('timelines/{userId}/tweets/{tweetId}')
.onCreate(async (snap, context) => {
try {
const { userId, tweetId } = snap.data();
functions.logger.log(userId);
functions.logger.log(tweetId);
return await db.doc(`/timelines/${userId}/tweets/${tweetId}`).update({ likeCount: 200 });
}
catch (error) {
functions.logger.log(error);
}
});

Read from firebase storage and write to firestore using firebase functions

I had tried this typescript code 👇
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import serviceAccount from "/Users/300041370/Downloads/serviceKey.json";
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
const buckObj = functions.storage.bucket("myBucket").object();
export const onWikiWrite = buckObj.onFinalize(async (object) => {
const filePath = object.name ?? "test.json";
const bucket = admin.storage().bucket("myBucket");
bucket.file(filePath).download().then((data) => {
const contents = data[0];
data = {"key": "value"};
const doc = admin.firestore().collection("myCollection").doc();
doc.set(data);
});
});
but this gave me following error
"status":{"code":7,"message":"Insufficient permissions to (re)configure a trigger (permission denied for bucket myBucket). Please, give owner permissions to the editor role of the bucket and try again.
I had asked this question here but it got closed as duplicate of this question. It basically said, storage.bucket("myBucket") feature is not supported and that I'll have to instead use match for limiting this operation to files in this specific bucket/folder. Hence, I tried this 👇
const buckObj = functions.storage.object();
export const onWikiWrite = buckObj.onFinalize(async (object) => {
if (object.name.match(/myBucket\//)) {
const fileBucket = object.bucket;
const filePath = object.name;
const bucket = admin.storage().bucket(fileBucket);
bucket.file(filePath).download().then((data) => {
const contents = data[0];
const doc = admin.firestore().collection("myCollection").doc();
const data = {content: contents}
doc.set(data);
});
}
});
I am still facing the same issue. I'll repeat that here:
"status":{"code":7,"message":"Insufficient permissions to (re)configure a trigger (permission denied for bucket myBucket). Please, give owner permissions to the editor role of the bucket and try again.
Since version 1.0 of the Firebase SDK for Cloud Functions, firebase-admin shall be initialized without any parameters within the Cloud Functions runtime.
The following should work (I've removed the check on filePath):
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp();
export const onWikiWrite = functions.storage
.object()
.onFinalize(async (object) => {
const fileBucket = object.bucket;
const filePath = object.name;
const bucket = admin.storage().bucket(fileBucket);
return bucket
.file(filePath)
.download()
.then((data) => {
const contents = data[0];
return admin
.firestore()
.collection('myCollection')
.add({ content: contents });
});
});
Note that we return the chain of promises returned by the asynchronous Firebase methods. It is key, in a Cloud Function which performs asynchronous processing (also known as "background functions") to return a JavaScript promise when all the asynchronous processing is complete.
We also use the add() method instead of doing doc().set().
Finally, when checking the value of the filePath, be aware of the fact that there is actually no concept of folder or subdirectory in Cloud Storage (See this answer).

FIREBASE FATAL ERROR: Database initialized multiple times

I have multiple database instances in my firebase app. I am trying to write into three database instances in firebase cloud functions. My understanding by following this document is no need to initialize multiple apps for each database instance. We can initialize one and pass in the database url. As a side note, I have another function with similar kind of functionality where I have trigger event in one database and write data to other database instance and it works fine.
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
const app = admin.app();
export const onStart =
functions.database.instance('my-db-1')
.ref('path')
.onCreate(async (snapshot, context) => {
return await onCreate('my-db-1',snapshot,context);
});
export const onStartDb01 = functions.database.instance('my-db-2')
.ref('path')
.onCreate(async (snapshot, context) => {
return await onCreate('my-db-2', snapshot, context);
});
async function onCreate(dbInstance: string, snapshot:
functions.database.DataSnapshot, context: functions.EventContext):
Promise<any> {
const defaultDb = app.database(defaultDbUrl);
const actvDb = app.database(actvDbUrl);
await defaultDb.ref('path')
.once("value")
.then(snap => {
const val = snap.val();
---do something and write back---
});
await actvDb.ref('path')
.once("value")
.then(snap => {
const val = snap.val();
---do something and write back---
});
return true;
}
But when a db event is fired, it logs the error as below
Error: FIREBASE FATAL ERROR: Database initialized multiple times. Please make sure the format of the database URL matches with each database() call.
You'll need to initialize a separate app() for each database instance.
Based on Doug's answer here that should be something like this:
const app1 = admin.initializeApp(functions.config().firebase)
const app2 = admin.initializeApp(functions.config().firebase)
And then:
const defaultDb = app1.database(defaultDbUrl);
const actvDb = app2.database(actvDbUrl);

dialogflow chatbot how to go to firestore database and return with support relevant questions?

I am using "firestore" database for my "dialogflow" chat bot which I already created for an online grocery store. The problem is: I want my chatbot to initially ask questions from users to find the proper item title in my database and then return to user by asking 3-4 support relevant questions about that item. the questions must be the item attributes (Brand, color, size...) and will vary from one item to another. so the chatbot will stream down the user to find the best item.
can you please help me to find the answer? I already created the codes but they don't work and I do not know what is wrong with that. If you already created this and have the index.js file, I appreciate to propose me here.
index.js:
'use strict';
const functions = require('firebase-functions');
// Import admin SDK
const admin = require('firebase-admin');
const {
WebhookClient
} = require('dialogflow-fulfillment');
process.env.DEBUG = 'dialogflow:*'; // enables lib debugging statements
admin.initializeApp(functions.config().firebase);
// here we get the database in a variable
const db = admin.firestore();
const data = {...};
// Add a new document in collection "dialogflow" with document ID 'agent'
const dialogflowAgentRef = db.collection('dialogflow').doc('agent').set(data);
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({
request,
response
});
function writeToDb(agent) {
// Get parameter from Dialogflow with the string to add to the database doc
const databaseEntry = agent.parameters.databaseEntry;
// Get the database collection 'dialogflow' and document 'agent' and store
// the document {entry: "<value of database entry>"} in the 'agent' document
const dialogflowAgentRef = db.collection('dialogflow').doc('agent').where('title', '==', title);
return db.runTransaction(t => {
t.set(dialogflowAgentRef, {
entry: databaseEntry
});
return Promise.resolve('Write complete');
}).then(doc => {
agent.add(`Wrote "${databaseEntry}" to the Firestore database.`);
}).catch(err => {
console.log(`Error writing to Firestore: ${err}`);
agent.add(`Failed to write "${databaseEntry}" to the Firestore database.`);
});
}
function readFromDb(agent) {
// Get the database collection 'dialogflow' and document 'agent'
const dialogflowAgentDoc = db.collection('dialogflow/agent/rss/channel/item'); // .doc('agent')
// Get the value of 'entry' in the document and send it to the user
return dialogflowAgentDoc.get()
.then(doc => {
if (!doc.exists) {
agent.add('No data found in the database!');
} else {
agent.add(doc.data().entry);
}
return Promise.resolve('Read complete');
}).catch(() => {
agent.add('Error reading entry from the Firestore database.');
agent.add('Please add a entry to the database first by saying, "Write <your phrase> to the database"');
});
}
// Map from Dialogflow intent names to functions to be run when the intent is matched
let intentMap = new Map();
intentMap.set('ReadFromFirestore', readFromDb);
intentMap.set('WriteToFirestore', writeToDb);
agent.handleRequest(intentMap);
});
There are a number of issues with your code as you've shown it that could cause problems reading and writing with the Firestore database.
It looks like you're trying to find an existing collection to write to with the line
const dialogflowAgentRef = db.collection('dialogflow').doc('agent').where('title', '==', title);
but title isn't defined anywhere, which I suspect causes an error. Furthermore, doc() returns a DocumentReference, but there is no where() method in a DocumentReference.
Remember that you need to structure Firestore using alternating collections and documents. So your "firebase" collection can contain a document named "agent", and that document may have subcollections.
When you're trying to read with
const dialogflowAgentDoc = db.collection('dialogflow/agent/rss/channel/item');
You're getting a collection, but then trying to treat it as a document. The comment suggests that you're trying to read a specific doc from this collection (which makes sense), but you're loading that document by a hard-coded string "agent", rather than trying to get the agent from the parameters passed to you from Dialogflow.
Finally - the paths in the read and write sections don't match. Using hard-coded paths are fine when testing, but make sure you're using matching paths and that they reflect the collection/doc/collection/doc/... path requirement.
So in both cases, you might have a reference that looks something like
const docTitle = agent.parameters.title;
const docRef = db.collection('dialogflow').doc(title);
Which, if you have defined a "title" parameter in your Intents in Dialogflow, will use this to reference the doc, which you can then read or write.
thanks for the answer I already changed my database to real time firebase instead of firestore. still having problem with support relevant questions. I want to go to my real time database to find the item by search using "oederByChild" and "equalTo" methods as I found these in people questions and answer in this website. still cannot find and item title through my database child. here is the codes are written:
'use strict';
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const {
WebhookClient
} = require('dialogflow-fulfillment');
process.env.DEBUG = 'dialogflow:*'; // enables lib debugging statements
admin.initializeApp(functions.config().firebase);
const db = admin.database();
// const ref = db.ref('server/saving-data/fireblog');
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({
request,
response
});
console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
console.log('Dialogflow Request body: ' + JSON.stringify(request.body));
// Get the database collection 'dialogflow' and document 'agent' and store
// the document {entry: "<value of database entry>"} in the 'agent' document
function writeToDb(agent) {
const databaseEntry = agent.parameters.databaseEntry;
const acc = db.ref('rss/channel/item/4/title'); //**This worked! */
acc.set({
entry: databaseEntry
});
return Promise.resolve('write complete')
.then(_acc => {
agent.add(`Wrote ${databaseEntry} to the realtime database.`);
return false;
}).catch(err => {
console.log(`Error writing to Firestore: ${err}`);
agent.add(`Failed to write "${databaseEntry}" to the Firestore database.`);
});
}
// and this is when we want to write to in the same child, keeping the old values:
//const acc = db.ref('/rss/channel/item/5/color'); //**This worked! */
//const result = acc.child(databaseEntry).set({entry: databaseEntry});
//agent.add(`Wrote ${databaseEntry} to the realtime database.`);
//console.log(result.key);
//});
// to read data
function readFromDb(agent) {
const any = agent.parameters.any;
agent.add(`Thank you...`);
var rootRef = db.ref();
var childref = rootRef.child("rss/channel/item");
return childref.orderByChild("title").equalTo("Icebreaker").once("value").then(function(snapshot){ //has been taken from the bus example: https://stackoverflow.com/questions/51917390/dialogflow-how-do-i-pass-a-parameter-through-in-a-firebase-query
var colored = snapshot.child("color/__text").val();
var sized = snapshot.child("size/__text").val();
agent.add(`Your search result for ` + any + ` Throughout the database is ` + colored +
` Color and ` + sized + ` Size`);
return Promise.resolve('Read complete');
}).catch(() => {
agent.add('Error reading entry from the Firestore database.');
agent.add('Please add a entry to the database first by saying, "Write <your phrase> to the database"');
});
}
// Map from Dialogflow intent names to functions to be run when the intent is matched
let intentMap = new Map();
intentMap.set('IWannaBuy', readFromDb);
intentMap.set('WriteToFirebase', writeToDb);
agent.handleRequest(intentMap);
});
enter code here
[this is how my database is][1]
[1]: https://i.stack.imgur.com/QdFy5.png

Setting a firestore document when a new user is created with firebase auth using node.js

I'm using firebase cloud functions, firebase auth and firestore.
I've done this before with firebase database but just not sure with firestore how to set a document in the users collection to the uid of a newly created firebase auth user.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
const db = admin.firestore()
exports.createUser = functions.auth.user().onCreate(event => {
const uid = event.data.uid;
console.log(uid);
db.collection('users').doc(uid);
});
The above completes ok in the logs but the uid isn't getting set in the database. Do I need to call set at some stage?
const collection = db.collection("users")
const userID = "12345" // ID after created the user.
collection.doc(userID).set({
name : "userFoo", // some another information for user you could save it here.
uid : userID // you could save the ID as field in document.
}).then(() => {
console.log("done")
})
Note, that the onCreate return has changed, it does return the user now, so event.data.uid isn't valid anymore.
The full function should look something like this, it will create a document with the user's uid in the "users" root-collection.
exports.createUser = functions.auth.user().onCreate((user) => {
const { uid } = user;
const userCollection = db.collection('users');
userCollection.doc(uid).set({
someData: 123,
someMoreData: [1, 2, 3],
});
});

Resources