Currently, I`m trying to delete all the nested hirachy datas when some documents deleted using Firestore Trigger
and my code and error code are below
const admin = require('firebase-admin');
const firebase_tools = require('firebase-tools');
const functions = require('firebase-functions');
admin.initializeApp({
}
);
exports.firestore_delete_trigger_test = functions.firestore
.document('collection1/{docs1}/collection2/{docs2}')
.onDelete(async (change: any, context: any) => {
const path = change.path;
await firebase_tools.firestore
.delete(path, {
project: process.env.GCLOUD_PROJECT,
recursive: true,
yes: true,
token: functions.config().fb.token
});
return {
path: path
};
});
and the Error code is below
FirebaseError: Must specify a path.
at Object.reject (/workspace/node_modules/firebase-tools/lib/utils.js:122:27)
why it shows error code? I do not want "callable functions" because of security
Don't ever use any when writing in TypeScript as you essentially "switch off" TypeScript and lose valuable type information such as why your code isn't working.
If you use the following lines, the type of change (which should be snapshot) and context are automatically set as QueryDocumentSnapshot and EventContext respectively as defined by the onDelete method.
exports.firestore_delete_trigger_test = functions.firestore
.document('collection1/{docs1}/collection2/{docs2}')
.onDelete(async (snapshot, context) => {
});
Note: A QueryDocumentSnapshot is like a regular DocumentSnapshot, but it is guaranteed to exist (snap.exists always returns true and snap.data() will never return undefined).
Now that the type is restored to snapshot, you'll see that you can't read a property called path from it as it's not set. In your current code, path will just be set to undefined, which leads to the error saying you need to specify a path.
const path = snapshot.path; // ✖ Property 'path' does not exist on type 'QueryDocumentSnapshot'. ts(2339)
This is because path is a property of DocumentReference. To get the snapshot's underlying DocumentReference, you access it's ref property.
const path = snapshot.ref.path; // ✔
Next, in Node 10+ Cloud Functions, the list of available environment variables was changed and process.env.GCLOUD_PROJECT was removed. To get the current project ID, you can use either
const PROJECT_ID = JSON.parse(process.env.FIREBASE_CONFIG).projectId;
or
const defaultApp = admin.intializeApp();
const PROJECT_ID = defaultApp.options.projectId;
This then makes your (modernized) code:
import * as admin from 'firebase-admin';
import * as firebase_tools from 'firebase-tools';
import * as functions from 'firebase-functions';
const PROJECT_ID = JSON.parse(process.env.FIREBASE_CONFIG).projectId;
admin.initializeApp(); // config is automatically filled if left empty
// although it isn't needed here
export const firestore_delete_trigger_test = functions.firestore
.document('collection1/{docs1}/collection2/{docs2}')
.onDelete(async (snapshot, context) => {
const path = snapshot.ref.path;
await firebase_tools.firestore
.delete(path, {
project: PROJECT_ID,
recursive: true,
yes: true,
token: functions.config().fb.token
});
return {
path: path
};
});
Related
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).
I am following the firebase documentation for unit testing. I created a simple cloud function that is triggered when a firestore document is created. It turns the value in that document to uppercase. I tried the function by creating a document and it worked as expected.
I use jest for the test. When I run the test I get the following error:
NOT_FOUND: No document to update: projects/myproject/databases/(default)/documents/test/testId
10 | return admin.firestore().doc(`test/` + snap.id)
> 11 | .update({ input: input });
| ^
12 | });
I added a console.log to my function to see what data was passed by the unit-test. It passes the right data (meaning the right document id and the right value for "input".
I'm not sure what I'm missing here. It's a simple function and a simple test. I don't understand how the function could receive the right data but not found the document, or how the document is not created in firestore when the test is run.
The function:
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
export const makeUpperCase = functions.firestore
.document('test/{id}')
.onCreate((snap, context) => {
const data = snap.data();
const input = data.input.toUpperCase();
console.log('new data: ' + input + ', id: ' + snap.id);
return admin.firestore().doc('test/' + snap.id)
.update({ input: input });
});
index.ts:
import * as admin from 'firebase-admin';
admin.initializeApp();
export { makeUpperCase } from './makeUpperCase';
the test (basicTest.test.ts):
import * as functions from 'firebase-functions-test';
import * as admin from 'firebase-admin';
import 'jest';
const testEnv = functions({
databaseURL: "https://mydb.firebaseio.com",
projectId: "myproject",
}, './service-account.json');
testEnv.mockConfig({});
import { makeUpperCase } from '../src/index';
describe('basicTest', () => {
let wrapped: any;
beforeAll(() => {
wrapped = testEnv.wrap(makeUpperCase);
});
test('converts input to upper case', async () => {
const path = 'test/testId';
const data = { input: 'a simple test' };
const snap = await testEnv.firestore.makeDocumentSnapshot(data, path);
await wrapped(snap);
const after = await admin.firestore().doc(path).get();
expect(after.data()!.input).toBe('A SIMPLE TEST');
});
});
The problem comes from your use of the import syntax. Unlike require, the import instructions are performed asynchronously before the script that is importing them is executed.
import * as module1 from 'module1';
console.log(module1);
import * as module2 from 'module2';
console.log(module1);
is effectively (not exactly) the same as
const module1 = require('module1');
const module2 = require('module2');
console.log(module1);
console.log(module2);
Because of this quirk, in your current code, admin.firestore() is incorrectly pointing at your live Firestore database and test.firestore is pointing at your test Firestore database. This is why the document doesn't exist, because it exists on your test database, but not your live one.
This is why the documentation you linked uses require statements, and not the import syntax.
// this is fine, as long as you don't call admin.initializeApp() before overriding the config and service account as part of test's initializer below
import * as admin from 'firebase-admin';
import 'jest';
// Override the configuration for tests
const test = require("firebase-functions-test")({
databaseURL: "https://mydb.firebaseio.com",
projectId: "myproject",
}, './service-account.json');
test.mockConfig({});
// AFTER mocking the configuration, load the original code
const { makeUpperCase } = require('../src/index');
describe('basicTest', () => {
let wrapped: any;
beforeAll(() => {
wrapped = test.wrap(makeUpperCase);
});
test('converts input to upper case', async () => {
const path = 'test/testId';
const data = { input: 'a simple test' };
const snap = await test.firestore.makeDocumentSnapshot(data, path);
await wrapped(snap);
const after = await admin.firestore().doc(path).get();
expect(after.data()!.input).toBe('A SIMPLE TEST');
});
});
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);
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.
// The Cloud Functions for Firebase SDK to create Cloud Functions and
setup triggers.
const functions = require('firebase-functions');
// The Firebase Admin SDK to access the Firebase Realtime Database.
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.giveCard = functions.firestore
.document('Profiles/{profileId}/cards/{cardsId}/_given/{_givenID}')
.onWrite((event) => {
// Get the field values of what I am working with
const oldGiven = event.data.previous.data()['given'];
const newGiven = event.data.data()['given'];
// Get the cardID to make sure that is there
const cardID = event.params.cardsId;
// An array to go through
const give_profiles = event.data.data()['given_profiles'];
// error cardDatatwo is returned as undefined
const cardDatatwo = newGiven.parent;
// error cardDatathree is returned as undefined
const cardDatathree = event.data.ref.root
// // error cardDatafour cannot read propoerty of undefined
// const cardDatafour = cardDatathree.child('Profiles/{profileId}/cards/{cardsId}')
// error cardDatafive 'The value of cardfive is DocumentReference...
const cardDatafive = event.data.ref.firestore.doc('Profiles/{profileId}/cards/{cardsId}');
// Check that the values have changed
if (newGiven == oldGiven) return;
if (newGiven !== undefined) {
console.log('The old value of given is', oldGiven);
console.log('The new value of given is', newGiven);
console.log('The value of the card is', cardID);
console.log('The value of the cardtwo is', cardDatatwo);
console.log('The value of the cardthree is', cardDatathree);
// console.log('The value of the cardfour is', cardDatafour);
console.log('The value of the cardfive is', cardDatafive);
for (var profile of give_profiles) {
console.log(profile);
};
return;
}
return console.log("No given value");
});
I am having great difficulty in getting the root for Firestore working with Cloud Functions. It works differently of course.
I am try to get a value up the path towards the root after an onUpdate has been fired further down.
.parent does not work
functions.database.ref of course does not work as that's the realtime database
and cannot use
firebase.firestore() is also not working in node
and event.data.ref.firestore.doc comes back as undefined.
I am sure have gone through every option.
Hope you can help.
Wo
According to the documentation, you can access collections via firestore, like this:
exports.giveCard = functions.firestore
.document('Profiles/{profileId}/cards/{cardsId}/_given/{_givenID}')
.onWrite((event) => {
// other code
const ref = event.data.ref.firestore.doc('your/path/here');
return ref.set({foo: 'bar'}).then(res => {
console.log('Document written');
});
});
You can use firestore to build a path to whatever part of the database you're seeking to access. You can also use event.data.ref.parent, like so:
exports.giveCard = functions.firestore
.document('Profiles/{profileId}/cards/{cardsId}/_given/{_givenID}')
.onWrite((event) => {
// other code
const parentref = event.data.ref.parent;
const grandparentref = parentref.parent; // gets to cardsId
return grandparentref.set({foo: 'bar'}).then(res => {
console.log('Document written');
});
});