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);
});
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');
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);
});