/firebase-functions-interop

Firebase Functions Interop Library for Dart.

Primary LanguageDartBSD 3-Clause "New" or "Revised" LicenseBSD-3-Clause

Build Status Pub Gitter

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.

What is this?

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

Status

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

Usage

Make sure you have Firebase CLI installed as well as a Firebase account and a test app.

See Getting started for more details.

1. Create Initial Project

$ 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.

2. Initialize Dart Project

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

Then run pub get to install dependencies.

3 Write a Basic Function

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.

4. Build your Function(s)

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

5. Deploy

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

6. Test it

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.

7. Scripts (optional)

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",

        "...": "..."
    }
}

Examples

HTTPS 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();
}

Realtime Database Function

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

Firestore Function

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

Pubsub Functions

void main() {
  functions['logPubsub'] = functions.pubsub.topic('my-topic').onPublish(logPubsub);
}

void logPubsub(Message message, EventContext context) {
  print(message.json["name"]);
}

Storage Functions

void main() {
  functions['logStorage'] = functions.storage.object().onChange(logStorage);
}

void logStorage(ObjectMetadata data, EventContext context) {
  print(data.name);
}

Auth Functions

void main() {
  functions['logAuth'] = functions.auth.user().onCreate(logAuth);
}

void logAuth(UserRecord data, EventContext context) {
  print(data.email);
}

Configuration

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

HTTPS Functions Details

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.

Features and Bugs

Please file feature requests and bugs at the issue tracker.

See the development file for instructions on running the test suite.