Write Firebase Cloud functions in Dart, run in Node.js. This is an early development preview, open-source project.
Using
1.0.0-dev.*
version? See UPGRADING.md for details on breaking changes and upgrade instructions.
firebase_functions_interop
provides interoperability layer for
Firebase Functions Node.js SDK. Firebase functions written in Dart
using this library must be compiled to JavaScript and run in Node.js.
Luckily, a lot of interoperability details are handled by this library
and a collections of tools from Dart SDK.
Here is a minimalistic "Hello world" example of a HTTPS cloud function:
import 'package:firebase_functions_interop/firebase_functions_interop.dart';
void main() {
functions['helloWorld'] = functions.https.onRequest(helloWorld);
}
void helloWorld(ExpressHttpRequest request) {
request.response.writeln('Hello world');
request.response.close();
}
Version 1.0.0 is considered stable though not feature complete. Below is status report of already implemented functionality by namespace:
- functions
- functions.config
- functions.analytics
- functions.auth
- functions.firestore 🔥
- functions.database
- functions.https
- functions.pubsub
- functions.storage
- functions.remoteConfig
Make sure you have Firebase CLI installed as well as a Firebase account and a test app.
See Getting started for more details.
$ mkdir myproject
$ cd myproject
$ firebase init functions
This creates functions
subdirectory in your project's root which contains
standard Node.js package structure with package.json
and index.js
files.
Go to functions
sub-folder and add a pubspec.yaml
with following contents:
name: myproject_functions
description: My project functions
version: 0.0.1
environment:
sdk: '>=2.0.0-dev <3.0.0'
dependencies:
# Firebase Functions bindings
firebase_functions_interop: ^1.0.0
dev_dependencies:
# Needed to compile Dart to valid Node.js module.
build_runner: ^1.0.0
build_node_compilers: ^0.2.0
Git:
firebase_functions_interop:
git:
url: https://github.com/tekartikdev/firebase-functions-interop
ref: dart2_3
Then run pub get
to install dependencies.
Create functions/node/index.dart
and type in something like this:
import 'package:firebase_functions_interop/firebase_functions_interop.dart';
void main() {
functions['helloWorld'] = functions.https.onRequest(helloWorld);
}
void helloWorld(ExpressHttpRequest request) {
request.response.writeln('Hello world');
request.response.close();
}
Copy-pasting also works.
Version 1.0.0
of this library depends on Dart 2 and the new build_runner
package. Integration with dart2js and DDC compilers is provided by
build_node_compilers
package which should already be in dev_dependencies
in pubspec.yaml
(see step 2).
Create functions/build.yaml
file with following contents:
targets:
$default:
sources:
- "node/**"
- "lib/**"
builders:
build_node_compilers|entrypoint:
generate_for:
- node/**
options:
compiler: dart2js
# List any dart2js specific args here, or omit it.
dart2js_args:
- --minify
By default
build_runner
compiles with DDC which is not supported by this library at this point. Above configuration makes it compile Dart with dart2js.
To build run following:
$ cd functions
$ pub run build_runner build --output=build
The result of pub run
is located in functions/build/node/index.dart.js
.
In your functions/package.json
, set the main
field to point to this file:
{
"...": "...",
"main": "build/node/index.dart.js"
}
Alternatively, you can replace the default index.js
with the built version:
$ cp functions/build/node/index.dart.js functions/index.js
Deploy using Firebase CLI:
$ firebase deploy --only functions
You can navigate to the new HTTPS function's URL printed out by the deploy command.
For the Realtime Database function, login to the Firebase Console and try
changing values under /messages/{randomValue}/original
.
You can use NPM scripts to simplify the work-flow of serving and deploying functions.
Update your functions/package.json
to be like so:
{
"...": "...",
"scripts": {
"build": "pub run build_runner build --output=build",
"watch": "pub run build_runner watch --output=build",
"preserve": "npm run build",
"serve": "firebase serve --only functions",
"predeploy": "npm run build",
"deploy": "firebase deploy --only functions",
"preshell": "npm run build",
"shell": "firebase experimental:functions:shell",
"...": "..."
}
}
import 'package:firebase_functions_interop/firebase_functions_interop.dart';
void main() {
functions['helloWorld'] = functions.https.onRequest(helloWorld);
}
void helloWorld(ExpressHttpRequest request) {
request.response.writeln('Hello world');
request.response.close();
}
void main() {
functions['makeUppercase'] = functions.database
.ref('/messages/{messageId}/original')
.onWrite(makeUppercase);
}
FutureOr<void> makeUppercase(
Change<DataSnapshot<String>> change, EventContext context) {
final DataSnapshot<String> snapshot = change.after;
var original = snapshot.val();
var pushId = context.params['testId'];
print('Uppercasing $original');
var uppercase = pushId.toString() + ': ' + original.toUpperCase();
return snapshot.ref.parent.child('uppercase').setValue(uppercase);
}
void main() {
functions['makeNamesUppercase'] = functions.firestore
.document('/users/{userId}').onWrite(makeNamesUppercase)
}
FutureOr<void> makeNamesUppercase(Change<DocumentSnapshot> change, EventContext context) {
// Since this is an update of the same document we must guard against
// infinite cycle of this function writing, reading and writing again.
final snapshot = change.after;
if (snapshot.data.getString("uppercasedName") == null) {
var original = snapshot.data.getString("name");
print('Uppercasing $original');
UpdateData newData = new UpdateData();
newData.setString("uppercasedName", original.toUpperCase());
return snapshot.reference.updateData(newData);
}
return null;
}
void main() {
functions['logPubsub'] = functions.pubsub.topic('my-topic').onPublish(logPubsub);
}
void logPubsub(Message message, EventContext context) {
print(message.json["name"]);
}
void main() {
functions['logStorage'] = functions.storage.object().onChange(logStorage);
}
void logStorage(ObjectMetadata data, EventContext context) {
print(data.name);
}
void main() {
functions['logAuth'] = functions.auth.user().onCreate(logAuth);
}
void logAuth(UserRecord data, EventContext context) {
print(data.email);
}
Firebase SDK provides a way to set and access environment variables from your Firebase functions.
Environment variables are set using Firebase CLI, e.g.:
firebase functions:config:set some_service.api_key="secret" some_service.url="https://api.example.com"
For more details see https://firebase.google.com/docs/functions/config-env.
To read these values in a Firebase function use functions.config
.
Below example also uses node_http package which provides a HTTP client powered by Node.js I/O.
import 'package:firebase_functions_interop/firebase_functions_interop.dart';
import 'package:node_http/node_http.dart' as http;
void main() {
functions['helloWorld'] = functions.https.onRequest(helloWorld);
}
void helloWorld(ExpressHttpRequest request) async {
/// fetch env configuration
final config = functions.config;
final String serviceKey = config.get('someservice.key');
final String serviceUrl = config.get('someservice.url');
/// `http.get()` function is exposed by the `node_http` package.
var response = await http.get("$serviceUrl?apiKey=$serviceKey");
// do something with the response, e.g. forward response body to the client:
request.response.write(response.body);
request.response.close();
}
Firebase uses the Express.js web framework for HTTPS functions with body-parser
middleware enabled by default (documentation).
The ExpressHttpRequest
exposed by this library extends standard dart:io
HttpRequest
interface, which means it is also a stream of bytes. However
if body-parser
middleware already decoded request body then listening for
data on the request would hang since it's already been consumed. Use
ExpressHttpRequest.body
field to get decoded request body in this case.
Please file feature requests and bugs at the issue tracker.
See the development file for instructions on running the test suite.