ElmFire: Use the Firebase API in Elm
Virtually all features of the Firebase Web API are exposed as a library for Elm:
- Setting, removing and modifying values
- Transactions
- Querying data, both one-time and per subscription
- Complex queries with sorting, filtering and limiting
- Authentication
- User management
- Offline capabilities
In addition to these base functions the package elmfire-extra provides a higher-level synchronization API, which allows you to treat your Firebase data like a local Elm-Dict.
Demo application for these APIs: Collaborative TodoMVC
This library currently targets Elm version 0.16.
A new version with an effect manager for Elm 0.17/0.18 is under development. Please stay tuned!
API Usage
The API design corresponds closely to the targeted Firebase JavaScript API. Please refer to the original documentation for further discussions of the concepts.
In the following we give a short overview of the API. Detailed documentation is embedded in the source code.
Constructing Firebase Locations
To refer to a Firebase path use a Location
.
It can be built with the following functions:
-- Location is an opaque type.
fromUrl : String -> Location
sub : String -> Location -> Location
parent : Location -> Location
root : Location -> Location
push : Location -> Location
location : Reference -> Location
These are all pure functions. They don't touch a real Firebase until the resulting location is used in one of the tasks outlined below.
Example:
location : Location
location =
fromUrl "https://elmfire.firebaseio-demo.com/test"
|> parent
|> sub "anotherTest"`
|> push
References to Locations
Most actions on a Firebase location return a reference to that location. Likewise, query results contain a reference to the location of the reported value.
References can inform about the key or the complete URL of the referred location. And a reference may be converted back to a location, which can be used in a new task.
There is a special task to open a location without modifying or querying it, which results in a reference if the location is valid. It's generally not necessary to explicitly open a constructed location, but it may be used to check the validity of a location or to cache Firebase references.
-- Reference is an opaque type
key : Reference -> String
toUrl : Reference -> String
location : Reference -> Location
open : Location -> Task Error Reference
Modifying Values
set : Value -> Location -> Task Error Reference
setWithPriority : Value -> Priority -> Location -> Task Error Reference
setPriority : Priority -> Location -> Task Error Reference
update : Value -> Location -> Task Error Reference
remove : Location -> Task Error Reference
These tasks complete when synchronization to the Firebase servers has completed. On success they result in a Reference to the modified location. They result in an error if the location is invalid or if you have no permission to modify the data.
Values are given as Json values, i.e. Json.Encode.Value
.
Example:
port write : Task Error ()
port write =
set (Json.Encode.string "new branch") (push location)
`andThen`
(\ref -> ... ref.key ... )
Transactions
Atomic modifications of the data at a location can be achieved by transactions.
A transaction takes an update function that maps the previous value to a new value. In case of a conflict with concurrent updates by other clients the update function is called repeatedly until no more conflict is encountered.
transaction : (Maybe Value -> Action) ->
Location ->
Bool ->
Task Error (Bool, Snapshot)
type Action = Abort | Remove | Set Value
Example:
port trans : Task Error -> Task Error ()
port trans =
transaction
( \maybeVal -> case maybeVal of
Just value ->
case Json.Decode.decodeValue Json.Decode.int value of
Ok counter -> Set (Json.Encode.int (counter + 1))
_ -> Abort
Nothing ->
Set (Json.Encode.int 1)
) location False
`andThen`
(\(committed, snapshot) -> ... )
Querying
once : Query -> Location -> Task Error Snapshot
subscribe : (Snapshot -> Task x a) ->
(Cancellation -> Task y b) ->
Query ->
Location ->
Task Error Subscription
unsubscribe : Subscription -> Task Error ()
Use once
to listen to exactly one event of the given type.
The first parameter specifies the event to listen to: valueChanged
, childAdded
, childChanged
, childRemoved
or childMoved
.
Additionally, this parameter may also specify ordering, filtering and limiting of the query (see below).
If you don't need these options a simple query specification is valueChanged noOrder
.
The second parameter references the queried location.
Use subscribe
to start a continuing query of the specified events.
Subscription queries return a arbitrary number of data messages,
which are reported via running a supplied task.
The first parameter of subscribe
is a function used to construct that task from a data message.
The second parameter is a function used to construct a task that is run when the query gets canceled.
The third and fourth parameter of subscribe
are the same as the first two of once
.
On success the subscribe
task returns a Subscription, an identifier that can be used to match the corresponding responses and to cancel the query.
type alias Snapshot =
{ subscription: Subscription
, key: String
, reference: Reference
, existing: Bool
, value: Value
, prevKey: Maybe String
, priority: Priority
}
type Cancellation
= Unsubscribed Subscription
| QueryError Subscription Error
A Snapshot
carries the resulting Value
(as Json) among other information,
e.g. the corresponding Subscription
identifier.
In queries of type valueChanged
the result may be that there is no value at the queried location.
In this case existing
will be False
and value will be the Json value of null
.
key
corresponds to the last part of the path.
It is the empty string for the root.
Keys are relevant notably for child queries.
Example:
responses : Signal.Mailbox (Maybe Snapshot)
responses = Signal.mailbox Nothing
port query : Task Error Subscription
port query =
subscribe
(Signal.send responses.address << Just)
(always (Task.succeed ()))
(childAdded noOrder noLimit)
(fromUrl "https:...firebaseio.com/...")
... = Signal.map
(\response -> case response of
Nothing -> ...
Just snapshot -> ...
)
responses.signal
Ordering, Filtering and Limiting Queries
Query results can be ordered (by value, by a child's value, by key or by priority), and then filtered by giving a start and/or end value within that order, and limited to the first or last certain number of children.
Example queries to be used in once
and subscribe
:
childAdded noOrder
childAdded (orderByValue noRange noLimit)
childAdded (orderByChild "size" noRange noLimit)
childAdded (orderByKey noRange noLimit)
childAdded (orderByPriority noRange (limitToFirst 2))
childAdded (orderByValue (startAt (Json.Encode.string "foo")) noLimit)
childAdded (orderByValue (startAt (Json.Encode.string "foo")) (limitToLast 10))
childAdded (orderByChild "size" (equalTo (Json.Encode.int 42)) noLimit)
childAdded (orderByKey (endAt "k") noLimit)
childAdded (orderByPriority (startAt (NumberPriority 17, Just "k")) noLimit)
When doing ordered valuedChanged
queries it may be useful to map the result
to a list to conserve the ordering:
toSnapshotList : Snapshot -> List Snapshot
toValueList : Snapshot -> List JE.Value
toKeyList : Snapshot -> List String
toPairList : Snapshot -> List (String, JE.Value)
Authentication
The sub-module ElmFire.Auth provides all authentication and user management functions that are offered by Firebase.
Some example tasks:
import ElmFire.Auth exposing (..)
-- create a new user-account with email and password
userOperation (createUser "[email protected]" "myPassword")
-- login with with email and password
authenticate loc [rememberSessionOnly] (withPassword "[email protected]" "myPassword")
-- login with with github account
authenticate loc [] (withOAuthPopup "github")
-- watch for logins and logouts
subscribeAuth
(\maybeAuth -> case maybeAuth of
Just auth -> ... auth.uid ...
Nothing -> ... -- not authenticated
)
loc
Offline Capabilities
- Detecting connection state changes:
subscribeConnected
- Manually disconnect and reconnect:
goOffline
,goOnline
- Managing presence:
onDisconnectSet
,onDisconnectSetWithPriority
,onDisconnectUpdate
,onDisconnectRemove
,onDisconnectCancel
- Handling latency:
subscribeServerTimeOffset
,serverTimeStamp
Examples
There is a basic example app in example/src/Example.elm
. To build it:
cd example
make all open
Alternatively without using make
:
cd example
elm make --output Example.html src/Example.elm
TodoMVC
A more extensive example is this implementation of TodoMVC as a collaborative real-time app.
Testing
There is a testing app, living in the directory test
, that covers most of the code.
It runs a given sequence of tasks on the Firebase API and logs these steps along with the several results.
This app uses a small ad-hoc testing framework for task-based code.
There is a Makefile to build the app. On most Unix-like systems a cd test; make all open
should do the trick.
An older, still functional testing app lives in the directory demo
.