With Cloud Functions, you can handle events in the
Firebase Realtime Database with no need to update client code.
Cloud Functions lets you run Realtime Database operations with full administrative
privileges, and ensures that each change to Realtime Database is processed
individually. You can make Firebase Realtime Database changes via the
DataSnapshot
or via the Admin SDK.
In a typical lifecycle, a Firebase Realtime Database function does the following:
- Waits for changes to a particular Realtime Database location.
- Triggers when an event occurs and performs its tasks (see What can I do with Cloud Functions? for examples of use cases).
- Receives a data object that contains a snapshot of the data stored in the specified document.
Trigger a Realtime Database function
Create new functions for Realtime Database events
with functions.database
. To
control when the function triggers, specify one of the event handlers, and
specify the Realtime Database path where it will listen for events.
Set the event handler
Functions let you handle Realtime Database events at two levels of specificity; you can listen for specifically for only creation, update, or deletion events, or you can listen for any change of any kind to a path. Cloud Functions supports these event handlers for Realtime Database:
onWrite()
, which triggers when data is created, updated, or deleted in Realtime Database.onCreate()
, which triggers when new data is created in Realtime Database.onUpdate()
, which triggers when data is updated in Realtime Database .onDelete()
, which triggers when data is deleted from Realtime Database .
Specify the instance and path
To control when and where your function should trigger, call ref(path)
to specify a path, and optionally specify a Realtime Database instance
with instance('INSTANCE_NAME')
. If you do not
specify an instance, the function deploys to the default Realtime Database instance for
the Firebase project For example:
- Default Realtime Database instance:
functions.database.ref('/foo/bar')
- Instance named "my-app-db-2":
functions.database.instance('my-app-db-2').ref('/foo/bar')
These methods direct your function to handle writes at a certain path within
the Realtime Database instance. Path specifications match all writes that touch a path,
including writes
that happen anywhere below it. If you set the path
for your function as /foo/bar
, it matches events at both of these locations:
/foo/bar
/foo/bar/baz/really/deep/path
In either case, Firebase interprets that the event occurs at /foo/bar
,
and the event data includes
the old and new data at /foo/bar
. If the event data might be large,
consider using multiple functions at deeper paths instead of a single
function near the root of your database. For the best performance, only request
data at the deepest level possible.
You can specify a path component as a wildcard by surrounding it with curly
brackets; ref('foo/{bar}')
matches any child of /foo
. The values of these
wildcard path components are available within the
EventContext.params
object of your function. In this example, the value is available as
context.params.bar
.
Paths with wildcards can match multiple events from a single write. An insert of
{
"foo": {
"hello": "world",
"firebase": "functions"
}
}
matches the path "/foo/{bar}"
twice: once with "hello": "world"
and again with "firebase": "functions"
.
Handle event data
When handling a Realtime Database event, the data object returned is a
DataSnapshot
.
For onWrite
or onUpdate
events, the
first parameter is a Change
object that contains two snapshots
that represent the data state before
and after the triggering event. For onCreate
and onDelete
events,
the data object returned is a snapshot of the data created or deleted.
In this example, the function retrieves the snapshot for the specified path, converts the string at that location to uppercase, and writes that modified string to the database:
// Listens for new messages added to /messages/:pushId/original and creates an // uppercase version of the message to /messages/:pushId/uppercase exports.makeUppercase = functions.database.ref('/messages/{pushId}/original') .onCreate((snapshot, context) => { // Grab the current value of what was written to the Realtime Database. const original = snapshot.val(); functions.logger.log('Uppercasing', context.params.pushId, original); const uppercase = original.toUpperCase(); // You must return a Promise when performing asynchronous tasks inside a Functions such as // writing to the Firebase Realtime Database. // Setting an "uppercase" sibling in the Realtime Database returns a Promise. return snapshot.ref.parent.child('uppercase').set(uppercase); });
Accessing user authentication information
From EventContext.auth
and EventContext.authType
,
you can access
the user information, including permissions, for the user that triggered
a function. This can be useful for enforcing security rules,
allowing your function to complete different operations based on the user's
level of permissions:
const functions = require('firebase-functions/v1');
const admin = require('firebase-admin');
exports.simpleDbFunction = functions.database.ref('/path')
.onCreate((snap, context) => {
if (context.authType === 'ADMIN') {
// do something
} else if (context.authType === 'USER') {
console.log(snap.val(), 'written by', context.auth.uid);
}
});
Also, you can leverage user authentication information to "impersonate" a user and perform write operations on the user's behalf. Make sure to delete the app instance as shown below in order to prevent concurrency issues:
exports.impersonateMakeUpperCase = functions.database.ref('/messages/{pushId}/original')
.onCreate((snap, context) => {
const appOptions = JSON.parse(process.env.FIREBASE_CONFIG);
appOptions.databaseAuthVariableOverride = context.auth;
const app = admin.initializeApp(appOptions, 'app');
const uppercase = snap.val().toUpperCase();
const ref = snap.ref.parent.child('uppercase');
const deleteApp = () => app.delete().catch(() => null);
return app.database().ref(ref).set(uppercase).then(res => {
// Deleting the app is necessary for preventing concurrency leaks
return deleteApp().then(() => res);
}).catch(err => {
return deleteApp().then(() => Promise.reject(err));
});
});
Reading the previous value
The Change
object has a
before
property that lets you inspect what was saved to Realtime Database before the
event. The before
property returns a DataSnapshot
where all
methods (for example,
val()
and
exists()
)
refer to the previous value. You can read the new value again by either using
the original DataSnapshot
or reading the
after
property. This property on any Change
is another DataSnapshot
representing
the state of the data after the event happened.
For example, the before
property can be used to make sure the function only
uppercases text when it is first created:
exports.makeUppercase = functions.database.ref('/messages/{pushId}/original')
.onWrite((change, context) => {
// Only edit data when it is first created.
if (change.before.exists()) {
return null;
}
// Exit when the data is deleted.
if (!change.after.exists()) {
return null;
}
// Grab the current value of what was written to the Realtime Database.
const original = change.after.val();
console.log('Uppercasing', context.params.pushId, original);
const uppercase = original.toUpperCase();
// You must return a Promise when performing asynchronous tasks inside a Functions such as
// writing to the Firebase Realtime Database.
// Setting an "uppercase" sibling in the Realtime Database returns a Promise.
return change.after.ref.parent.child('uppercase').set(uppercase);
});