emberfire-utils
v1.5.1
Published
Useful utilities on top of EmberFire
Downloads
32
Readme
EmberFire Utilities
This addon provides some useful utilities on top of EmberFire.
Installation
ember install emberfire-utils
Your app needs to have EmberFire installed for this addon to work.
Features
Configuration
You can optionally specify what libraries you'd want to exclude in your build within your ember-cli-build.js
.
Here's how:
var app = new EmberApp(defaults, {
'emberfire-utils': {
exclude: [ 'firebase-flex', 'firebase-util', 'firebase-ui' ],
},
});
Possible exclusions are
firebase-flex
,firebase-util
, andfirebase-ui
.
Flexible Adapter and Serializer
This is a standard Ember Data adapter that supports: createRecord()
, destroyRecord()
, findRecord()
, findAll()
, queryRecord()
, and query()
. However, its extended to allow some power features that's available to Firebase users.
Usage
Setup your application adapter like this:
// app/adapters/application.js
import FirebaseFlexAdapter from 'emberfire-utils/adapters/firebase-flex';
export default FirebaseFlexAdapter.extend();
Save and delete records with fan-out
// Saving a new record with fan-out
this.get('store').createRecord('post', {
title: 'Foo',
message: 'Bar'
}).save({
adapterOptions: {
include: {
'/userFeeds/user_a/$id': true,
'/userFeeds/user_b/$id': true,
}
}
});
// Deleting a record with fan-out
this.get('store').findRecord('post', 'post_a').then((post) => {
post.deleteRecord();
post.save({
adapterOptions: {
include: {
'/userFeeds/user_a/post_a': null,
'/userFeeds/user_b/post_a': null,
}
}
});
});
// Alternatively, you can use `destroyRecord` with fan-out too
this.get('store').findRecord('post', 'post_a').then((post) => {
post.destroyRecord({
adapterOptions: {
include: {
'/userFeeds/user_a/post_a': null,
'/userFeeds/user_b/post_a': null,
}
}
});
});
Notice the
$id
. It's a keyword that will be replaced by the model's ID.
Save records with path
this.get('store').createRecord('comment', {
title: 'Foo',
message: 'Bar'
}).save({
adapterOptions: { path: 'comments/post_a' }
});
Update only the changed attributes of a record
By default, only the changed attributes will be updated in Firebase whenever we call save()
. This way, we can now have rules that doesn't allow some attributes to be edited.
Query records with path and infinite scrolling
The query params here uses the same format as the one in EmberFire with the addition of supporting the following:
orderBy: '.value'
.path
to query the data fromisReference
to know if thepath
is just a reference to a model in a different node (see example below)cacheId
to prevent duplicate listeners and make the query result array update in realtime- Without
cacheId
, the query result array won't listen forchild_added
orchild_removed
changes. However, the models that are already inside of it will still update in realtime. cacheId
isn't available inqueryRecord
.
- Without
With path
Let's assume the following data structure.
{
"chats": {
"one": {
"title": "Historical Tech Pioneers",
"lastMessage": "ghopper: Relay malfunction found. Cause: moth.",
"timestamp": 1459361875666
},
"two": { ... },
"three": { ... }
},
"members": {
"one": {
"ghopper": true,
"alovelace": true,
"eclarke": true
},
"two": { ... },
"three": { ... }
},
"messages": {
"one": {
"m1": {
"name": "eclarke",
"message": "The relay seems to be malfunctioning.",
"timestamp": 1459361875337
},
"m2": { ... },
"m3": { ... }
},
"two": { ... },
"three": { ... }
},
"users": {
"ghopper": { ... },
"alovelace": { ... },
"eclarke": { ... }
}
}
To fetch the chat members, you need to set the path
and isReference
. The isReference
boolean indicates that the nodes under members/one
are simply references to the user
model which is represented by the users
node.
this.get('store').query('user', {
path: 'members/one',
isReference: true,
limitToFirst: 10
});
To fetch the chat messages, you just need to set the path
and leave out the isReference
. Without the isReference
boolean, it indicates that the messages/one/m1
, messages/one/m2
, etc. are a direct representation of the message
model.
this.get('store').query('message', {
path: 'messages/one',
limitToFirst: 10
});
With cacheId
this.get('store').query('post', {
cacheId: 'my_cache_id',
limitToFirst: 10
});
Infinite scrolling
this.get('store').query('post', {
limitToFirst: 10
}).then((posts) => {
posts.get('firebase').next(10);
});
Caveats
Relationship won't get updated when firing save()
As explained above, only the changed attributes will be saved when we call it. Ember Data currently doesn't provide a way to check if a relationship has changed. As a workaround, we need to fan-out the relationship to save it.
e.g.
const store = this.get('store');
store.findRecord('comment', 'another_comment').then((comment) => {
store.findRecord('post', 'post_a').then((post) => {
post.get('comments').addObject(comment);
post.save({
adapterOptions: {
include: {
'posts/post_a/comments/another_comment': '<some_value_here>'
}
}
});
});
});
However, there's a good side to this. Now we can provide different values to those relationships rather than the default true
value in EmberFire.
hasFiltered
relationship (not really a relationship)
Most of the time, we don't want to use the hasMany()
relationship in our models because:
- It's not flexible enough to fetch from paths we want.
- It loads all the data when we access it.
- Even if we don't access it, those array of IDs are still taking up internet data usage.
To solve those 2 problems above, use hasFiltered()
relationship. It has the same parameters as store.query()
and it also works with infinite scrolling as explained above.
// app/models/post
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import hasFiltered from 'emberfire-utils/utils/has-filtered';
export default Model.extend({
title: attr('string'),
_innerReferencePath: attr('string'),
comments: hasFiltered('comment', {
cacheId: '$id_comments',
path: '/comments/$innerReferencePath/$id',
limitToFirst: 10,
})
});
Notice the following:
$id
- This is a keyword that will be replaced by the model's ID.
- This works for both
cacheId
andpath
.
_innerReferencePath
- This will be replaced by the inner Firebase reference path of the model.
- If
post
model lives in/posts/forum_a/post_a
, the value would beforum_a
.- Another example,
/posts/foo/bar/post_a
->foo/bar
.
- Another example,
- This is a client-side only property. It won't be persisted in the DB when you save the record.
$innerReferencePath
- This is a keyword that will be replaced by
_innerReferencePath
. - This only works for
path
. - Won't work when
_innerReferencePath
isn't defined. - This is useful for when let's say your comments lives in
/comments/<forum_id>/<post_id>
and you know the value of the<post_id>
through$id
but don't know the value of<forum_id>
.
- This is a keyword that will be replaced by
hasFiltered()
are read only.
Utility Service
Usage
Simply inject the firebase-util
service.
Multi-path updates
To write on multiple paths atomically in Firebase, call update()
.
const fanoutObject = {};
fanoutObject['users/foo/firstName'] = 'Foo';
fanoutObject['users/bar/firstName'] = 'Bar';
this.get('firebaseUtil').update(fanoutObject).then(() => {
// Do something after a succesful update
}).catch(error => {
// Do something with `error`
});
Generate Firebase push ID
Should you need to generate a Firebase push ID for your multi-path updates, you can use generateIdForRecord()
. This returns a unique ID generated by Firebase's push()
method.
const pushId = this.get('firebaseUtil').generateIdForRecord();
Storage manipulations
Uploading a file to Firebase Storage
To upload files in Firebase storage, call uploadFile()
.
function onStateChange(snapshot) {
const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
console.log('Upload is ' + progress + '% done');
}
this.get('firebaseUtil').uploadFile('images/foo.jpg', file, metadata, onStateChange).then(downloadURL => {
// Do something with `downloadURL`
}).catch(error => {
// Do something with `error`
});
file
should be aBlob
or aUint8Array
.metadata
andonStateChange
are optional params.
Deleting a file in Firebase Storage
To delete files in Firebase storage, call deleteFile()
.
this.get('firebaseUtil').deleteFile(url).then(() => {
// Do something on success
}).catch(error => {
// Do something with `error`
});
url
should be the HTTPS URL representation of the file. e.g. https://firebasestorage.googleapis.com/b/bucket/o/images%20stars.jpg
Queries for non-model data
For the examples below, assume we have the following Firebase data:
{
"users" : {
"foo" : {
"photoURL" : "foo.jpg",
"username" : "bar"
},
"hello" : {
"photoURL" : "hello.jpg",
"username" : "world"
}
}
}
Query a single record
To query a single record, call queryRecord()
. This will return a promise that fulfills with the requested record in a plain object format.
this.get('firebaseUtil').queryRecord('users', { equalTo: 'foo' }).then((record) => {
// Do something with `record`
}).catch(error => {
// Do something with `error`
});
Params:
path
- Firebase pathoptions
- An object that can contain the following:cacheId
- Prevents duplicate listeners and returns cached record if it already exists. When not provided, Firebase won't listen for changes returned by this function.- EmberFire queries with the addition of
.value
fororderBy
and forcing oflimitToFirst
orlimitToLast
to 1.
limitToFirst
andlimitToLast
is forced to 1 because this method will only return a single record. If you provided an option oflimitToFirst
, it will set it to 1 regardless of the value that you've set. Same goes forlimitToLast
respectively.
Query multiple records
To query for multiple records, call query()
. This will return a promise that fulfills with the requested records; each one in a plain object format.
this.get('firebaseUtil').query('users', { limitToFirst: 10 }).then((records) => {
// Do something with `records`
}).catch(error => {
// Do something with `error`
});
Params:
path
- Firebase pathoptions
- An object that can contain the following:cacheId
- Prevents duplicate listeners and returns cached record if it already exists. When not provided, Firebase won't listen for changes returned by this function.- EmberFire queries with the addition of
.value
fororderBy
.
Serialized to plain objects
For queryRecord()
and query()
, the records are serialized in plain object. For the queryRecord()
example
above, the record will be serialized to:
record = {
id: 'foo',
photoURL: 'foo.jpg',
username: 'bar'
};
For query()
:
records = [{
id: 'foo',
photoURL: 'foo.jpg',
username: 'bar'
}, {
id: 'hello',
photoURL: 'hello.jpg',
username: 'world'
}];
Should we retrieve a record who's value isn't an object (e.g. users/foo/username
), the record will be
serialized to:
record = {
id: 'username',
value: 'bar'
};
Loading more records for query()
To load more records in the query()
result, call next()
.
const firebaseUtil = this.get('firebaseUtil');
firebaseUtil.query('users', {
cacheId: 'cache_id',
limitToFirst: 10,
}).then(() => {
firebaseUtil.next('cache_id', 10);
});
Checking if record exists
To check if a record exists, call isRecordExisting()
. This returns a promise that fulfills to true
if the record exists. Otherwise, false
.
this.get('firebaseUtil').isRecordExisting('users/foo').then((result) => {
// Do something with `result`
}).catch(error => {
// Do something with `error`
});
FirebaseUI
Auth
A component is provided for rendering FirebaseUI Auth. Here's how:
First setup your uiConfig
which is exactly the same with Firebase UI Auth.
import firebase from 'firebase';
import firebaseui from 'firebaseui';
let uiConfig = {
credentialHelper: firebaseui.auth.CredentialHelper.NONE,
signInSuccessUrl: '<url-to-redirect-to-on-success>',
signInOptions: [
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
firebase.auth.FacebookAuthProvider.PROVIDER_ID,
firebase.auth.TwitterAuthProvider.PROVIDER_ID,
firebase.auth.GithubAuthProvider.PROVIDER_ID,
firebase.auth.EmailAuthProvider.PROVIDER_ID
],
};
Then pass that uiConfig
into the firebase-ui-auth
component.
{{firebase-ui-auth uiConfig=uiConfig}}
Compatibility
This addon is compatible with EmberFire 2.0.x.
Contributing
Installation
git clone <repository-url>
this repositorycd emberfire-utils
npm install
Running
ember serve
- Visit your app at http://localhost:4200.
Running Tests
npm test
(Runsember try:each
to test your addon against multiple Ember versions)ember test
ember test --server
Building
ember build
For more information on using ember-cli, visit https://ember-cli.com/.