I have data stored in firebase in the following structure (figure 1). I followed the guidelines for structuring data and saved it in a flat structure with key-val pairs on events and users to allow for a many to many relationship reference. I want to user a userid to look up events that a user has access to, in pure javascript this is simple (see figure 2) however it is proving difficult with angularfire as I'd like to use a firebaseObject or array. Does anyone know how to perform a query like this?
Figure 1.
{
users: {
user_id1: {
events: {
event_id1: true,
event_id2: true
}
},
user_id2: {
events: {
event_id3: true,
event_id4: true
}
},
user_idN...
},
events: {
event_id1: {
users: {
user_id1: true
}
},
event_id2: {
users: {
user_id1: true
}
},
event_idN...
}
}
Figure 2
// List all of user_id1's events
var ref = new Firebase("https://<<example>>.firebaseio.com/");
// fetch a list of user_id1's events
ref.child("users/user_id1/events").on('child_added', function(snapshot) {
// for each event, fetch it and print it
String groupKey = snapshot.key();
ref.child("events/" + groupKey).once('value', function(snapshot) {
console.log(snapshot.val());
});
});
This is a great case for using $extend in AngularFire.
You're sharing the $event_id key so can load the events after, the user is retrieved.
app.factory("UserFactory", function($firebaseObject) {
return $firebaseObject.$extend({
getEvent: function(eventId) {
var eventRef = new Firebase('<my-firebase-app>/events').child(eventId);
return $firebaseObject(eventRef);
}
});
});
app.controller('MyCtrl', function($scope, UserFactory) {
var userRef = new Firebase('<my-firebase-app>').child("users/user_id1/");
var user = new UserFactory();
$scope.event = user.getEvent(user.events.event_id1);
});
See the API reference for more information.
Related
Edit 11/16/14: Version Information
DEBUG: Ember : 1.7.0 ember-1.7.0.js:14463
DEBUG: Ember Data : 1.0.0-beta.10+canary.30d6bf849b ember-1.7.0.js:14463
DEBUG: Handlebars : 1.1.2 ember-1.7.0.js:14463
DEBUG: jQuery : 1.10.2
I'm beating my head against a wall trying to do something that I think should be fairly straightforward with ember and ember-data, but I haven't had any luck so far.
Essentially, I want to use server data to populate a <select> dropdown menu. When the form is submitted, a model should be created based on the data the user chooses to select. The model is then saved with ember data and forwarded to the server with the following format:
{
"File": {
"fileName":"the_name.txt",
"filePath":"/the/path",
"typeId": 13,
"versionId": 2
}
}
The problem is, the typeId and versionId are left out when the model relationship is defined as async like so:
App.File = DS.Model.extend({
type: DS.belongsTo('type', {async: true}),
version: DS.belongsTo('version', {async: true}),
fileName: DS.attr('string'),
filePath: DS.attr('string')
});
The part that is confusing me, and probably where my mistakes lie, is the controller:
App.FilesNewController = Ember.ObjectController.extend({
needs: ['files'],
uploadError: false,
// These properties will be given by the binding in the view to the
//<select> inputs.
selectedType: null,
selectedVersion: null,
files: Ember.computed.alias('controllers.files'),
actions: {
createFile: function() {
this.createFileHelper();
}
},
createFileHelper: function() {
var selectedType = this.get('selectedType');
var selectedVersion = this.get('selectedVersion');
var file = this.store.createRecord('file', {
fileName: 'the_name.txt',
filePath: '/the/path'
});
var gotDependencies = function(values) {
//////////////////////////////////////
// This only works when async: false
file.set('type', values[0])
.set('version', values[1]);
//////////////////////////////////////
var onSuccess = function() {
this.transitionToRoute('files');
}.bind(this);
var onFail = function() {
this.set('uploadError', true);
}.bind(this);
file.save().then(onSuccess, onFail);
}.bind(this);
Ember.RSVP.all([
selectedType,
selectedVersion
]).then(gotDependencies);
}
});
When async is set to false, ember handles createRecord().save() POST requests correctly.
When async is true, ember handles GET requests perfectly with multiple requests, but does NOT add the belongsTo relationships to the file JSON during createRecord().save(). Only the basic properties are serialized:
{"File":{"fileName":"the_name.txt","filePath":"/the/path"}}
I realize this question has been asked before but I have not found a satisfactory answer thus far and I have not found anything that suits my needs. So, how do I get the belongsTo relationship to serialize properly?
Just to be sure that everything is here, I will add the custom serialization I have so far:
App.ApplicationSerializer = DS.RESTSerializer.extend({
serializeIntoHash: function(data, type, record, options) {
var root = Ember.String.capitalize(type.typeKey);
data[root] = this.serialize(record, options);
},
keyForRelationship: function(key, type){
if (type === 'belongsTo') {
key += "Id";
}
if (type === 'hasMany') {
key += "Ids";
}
return key;
}
});
App.FileSerializer = App.ApplicationSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
type: { serialize: 'id' },
version: { serialize: 'id' }
}
});
And a select:
{{ view Ember.Select
contentBinding="controller.files.versions"
optionValuePath="content"
optionLabelPath="content.versionStr"
valueBinding="controller.selectedVersion"
id="selectVersion"
classNames="form-control"
prompt="-- Select Version --"}}
If necessary I will append the other routes and controllers (FilesRoute, FilesController, VersionsRoute, TypesRoute)
EDIT 11/16/14
I have a working solution (hack?) that I found based on information in two relevant threads:
1) How should async belongsTo relationships be serialized?
2) Does async belongsTo support related model assignment?
Essentially, all I had to do was move the Ember.RSVP.all() to after a get() on the properties:
createFileHelper: function() {
var selectedType = this.get('selectedType');
var selectedVersion = this.get('selectedVersion');
var file = this.store.createRecord('file', {
fileName: 'the_name.txt',
filePath: '/the/path',
type: null,
version: null
});
file.set('type', values[0])
.set('version', values[1]);
Ember.RSVP.all([
file.get('type'),
file.get('version')
]).then(function(values) {
var onSuccess = function() {
this.transitionToRoute('files');
}.bind(this);
var onFail = function() {
alert("failure");
this.set('uploadError', true);
}.bind(this);
file.save().then(onSuccess, onFail);
}.bind(this));
}
So I needed to get() the properties that were belongsTo relationships before I save the model. I don't know is whether this is a bug or not. Maybe someone with more knowledge about emberjs can help shed some light on that.
See the question for more details, but the generic answer that I worked for me when saving a model with a belongsTo relationship (and you specifically need that relationship to be serialized) is to call .get() on the properties and then save() them in then().
It boils down to this:
var file = this.store.createRecord('file', {
fileName: 'the_name.txt',
filePath: '/the/path',
type: null,
version: null
});
// belongsTo set() here
file.set('type', selectedType)
.set('version', selectedVersion);
Ember.RSVP.all([
file.get('type'),
file.get('version')
]).then(function(values) {
var onSuccess = function() {
this.transitionToRoute('files');
}.bind(this);
var onFail = function() {
alert("failure");
this.set('uploadError', true);
}.bind(this);
// Save inside then() after I call get() on promises
file.save().then(onSuccess, onFail);
}.bind(this));
Say I have a collection (of search results, for example) which needs to be populated and a pagination model that needs to take values for current page, total number of pages, etc. In my controller, I make a GET call to an API which returns both search results and pagination information. How, then, can I fetch all this information and parse it into a collection and a separate model? Is this possible?
I am using AirBNB's Rendr, which allows you to use a uniform code base to run Backbone on both the server and the client. Rendr forces me to parse the API response as an array of models, keeping me from being able to access pagination information.
In Rendr, my controller would look like this:
module.exports = {
index: function (params, callback) {
var spec = {
pagination: { model: 'Pagination', params: params },
collection: { collection: 'SearchResults', params: params }
};
this.app.fetch(spec, function (err, result) {
callback(err, result);
});
}
}
I apologize if this is not clear enough. Feel free to ask for more information!
This is super old so you've probably figured it out by now (or abandoned it). This is as much a backbone question as a Rendr one since the API response is non-standard.
Backbone suggests that if you have a non-standard API response then you need to override the parse method for your exact data format.
If you really want to break it up, the way you may want to code it is:
a Pagination Model
a Search Results Collection
a Search Result Model
and most importantly a Search Model with a custom parse function
Controller:
index: function (params, callback) {
var spec = {
model: { model: 'Search', params: params }
};
this.app.fetch(spec, function (err, result) {
callback(err, result);
});
}
Search Model
var Base = require('./base'),
_ = require('underscore');
module.exports = Base.extend({
url: '/api/search',
parse: function(data) {
if (_.isObject(data.paginationInfo)) {
data.paginationInfo = this.app.modelUtils.getModel('PaginationInfo', data.paginationInfo, {
app: this.app
});
}
if (_.isArray(data.results)) {
data.results = this.app.modelUtils.getCollection('SearchResults', data.results, {
app: this.app,
params: {
searchQuery: data.searchQuery // replace with real parameters for client-side caching.
}
});
}
return data;
}
});
module.exports.id = 'Search';
Working on my social app I've found a strange behavior in the collection Meteor.users, this problem does not occur with other Collections using the same methodologies
I would like to have an initial list of users downloading a minimum number of information for everyone and when I open the panel to a specific user I subscribe a different showing more information if the specified user is a friend of mine.
But after subscribe the client collection Meteor.users is not updated!
CLIENT
Meteor.startup(function() {
Meteor.subscribe('usersByIds', Meteor.user().profile.friends, function() {
//... make users list panel using minimal fields
});
//performed when click on a user
function userLoadInfo(userId) {
Meteor.subscribe('userById', userId, function() {
var userProfile = Meteor.users.findOne(userId).profile;
//...
//make template user panel using full or minimal user fields
//...
//BUT NOT WORK!
//HERE Meteor.users.findOne(userId) keep minial user fields!!
//then if userId is my friend!
});
}
});
SERVER
//return minimal user fields
getUsersByIds = function(usersIds) {
return Meteor.users.find({_id: {$in: usersIds} },
{
fields: {
'profile.username':1,
'profile.avatar_url':1
}
});
};
//return all user fields
getFriendById = function(userId) {
return Meteor.users.find({_id: userId},
{
fields: {
'profile.username':1,
'profile.avatar_url':1
//ADDITIONAL FIELDS
'profile.online':1,
'profile.favorites':1,
'profile.friends':1
}
});
};
//Publish all users, with minimal fields
Meteor.publish('usersByIds', function(userId) {
if(!this.userId) return null;
return getUsersByIds( [userId] );
});
//Publish user, IF IS FRIEND full fields
Meteor.publish('userById', function(userId) {
if(!this.userId) return null;
var userCur = getFriendById(userId),
userProfile = userCur.fetch()[0].profile;
if(userProfile.friends.indexOf(this.userId) != -1) //I'm in his friends list
{
console.log('userdById IS FRIEND');
return userCur; //all fields
}
else
return getUsersByIds( [userId] ); //minimal fields
});
This is a limitation or bug in DDP. See this.
A workaround is to move data out of users.profile.
Like this:
//limited publish
Meteor.publish( 'basicData', function( reqId ){
if ( this.userId ) {
return Meteor.users.find({_id: reqId },{
fields: { 'profile.username':1,'profile.avatar_url':1}
});
}
else {
this.ready();
}
});
//friend Publish
Meteor.publish( 'friendData', function( reqId ){
if ( this.userId ) {
return Meteor.users.find( {_id: reqId, 'friendProfile.friends': this.userId }, {
fields: {
'friendProfile.online':1,
'friendProfile.favorites':1,
'friendProfile.friends':1
}
});
}
else {
this.ready();
}
});
//example user
var someUser = {
_id: "abcd",
profile: {
username: "abcd",
avatar_url: "http://pic.jpg"
},
friendProfile: {
friends: ['bcde', 'cdef' ],
online: true,
favorites: ['stuff', 'otherStuff' ]
}
}
As given in a comment, this link reveals your problem. The current DDP Protocol does not allow publishing of subdocuments. One way to get around this is to create a separate collection with your data but a better way would probably to just remove some of the data and make it a direct object off of your user.
The best way to do this is add the data to your user's profile upon insert and then in the onCreateUser move the data onto the user directly:
Accounts.onCreateUser(function(options, user) {
if (options.profile) {
if (options.profile.publicData) {
user.publicData = options.profile.publicData;
delete options.profile.publicData;
}
user.profile = options.profile;
}
return user;
});
If you are allowing clients to perform user inserts make sure you validate the data better though. This way you can have the online, favorites, and friends in the profile and publish that specifically when you want it. You can then have username and avatar_url in the publicData object directly on the user and just always publish all-the-time.
i have rest api based on django rest framework, that include next method of creation object, that takes the data in JSON-format on 'myapp/create_obj/' and if the data is correct object will created, otherwise it returns an error also in JSON-format.
def create_obj(request):
stream = StringIO(request.raw_post_data)
data = JSONParser().parse(stream)
serializer = ObjSerializer(data=data, many=True)
if serializer.is_valid():
serializer.save()
return JSONResponse(serializer.data, status=201)
else:
return JSONResponse(serializer.errors, status=400)
Also i tried to create a module on backbone.js, that post the input in form data to this method. Im very new to js, in particular to backbone and i bad understand how backbone works with server api. i have something like
App.module('Createobj', function(Mod, App, Backbone, Marionette, $, _) {
Mod.id = 'create-obj';
Mod.controllers = {};
Mod.Obj = Backbone.Model.extend({
defaults: {
real_ref : '',
share : ''
}
});
Mod.View = Marionette.ItemView.extend({
id: 'create-obj-page',
template: '#tpl-create-obj-page',
model: Mod.obj,
ui: {
'real_ref': 'input[name=real_ref]',
'share': 'input[name=share]',
'error': 'div.error'
},
hammerEvents: {
'tap button': 'submit:tap'
},
hammerOptions: {
tap: true
},
showError: function(message) {
this.ui.error
.text(message)
.show();
},
hideError: function() {
this.ui.error.hide();
},
});
Mod.Controller = SRClient.PageController.extend({
id: Mod.id + '.main',
ViewClass: Mod.View,
setup: function() {
this.listenTo(this.view, 'submit:tap', this.submit);
},
submit: function() {
var real_ref = this.view.ui.real_ref.val(),
share = this.view.ui.share.val();
if (!real_ref || !share) {
this.view.showError($t('create-obj.error_empty_fields'));
return;
}
App.vent.trigger('loading-screen:show', $t('app.please_wait'));
var obj = new Mod.obj({
real_ref : this.view.ui.real_ref.val(),
share : this.view.ui.share.val()
});
}});
Mod.addInitializer(function() {
Mod.Controllers = {
default: Mod.Controller
};
App.pageControllers[Mod.id] = Mod;
});
});
What i need to do, that data which i input in webform sends to 'myapp/create_obj' in json-format? Thanks!
Backbone expects a RESTful api so instead of being the endpoint an action like create_obj, REST works with Resources and with HTTP methods. In your case you could have a Model like this:
var Obj = Backbone.Model.extend({
defaults: {
real_ref : '',
share : ''
}
});
and a collection like this
var Objects = Backbone.Collection.extend({
url: 'myapp/obj',
model: Obj
});
the collection has a propetry url that specifies the server endpoint. So the operations will be
POST /myapp/obj/ for create a new item
GET /myapp/obj/:id/ if you want to retreive an specific item
GET /myapp/obj/ retreving the whole list
PUT /myapp/obj/:id/ update an item
DELETE /myapp/obj/:id/ delete an item
Tastypie is a good framework to create RESTful api with Django.
Background: I've got a single-page knockout.js app using the mapping plugin. The data is updated via Websocket JSON from the server. I can see that the app is successfully receiving the data (printing data in console) and when there are object removals/additions, the ViewModel updates no problem.
Problem: When a property of an object is updated from the server, it does not change the ViewModel. Do I need to return the object property somehow with each update?
Here are the relevant snippets of code:
var userMapping = {
"users": {
key: function(data) { return ko.utils.unwrapObservable(data.id); },
create: function(options) {
// for sortable ui access
return createUser(options.data);
}
}
};
var jobMapping = {
"jobs": {
key: function(data) { return ko.utils.unwrapObservable(data.id); },
create: function(options) {
// for sortable ui access
return createJob(options.data);
},
update: function(options) {
return createJob(options.data);
}
}
};
var createJob = function(job, user) {
// leaflet init
createJobIcon(job);
// general data mapping
var result = ko.mapping.fromJS(job);
return result;
};
self.engineModel.update = function(data) {
ko.mapping.fromJS(data, userMapping, self.engineModel);
};
And in the ViewModel:
<li data-bind="visible: canceled()==false, attr: {class: 'job-li canceled-'+canceled()+' started-'+started()+' hold-'+hold() }">
Thanks for the insight!
I've been able to fix the behaviour but I remain skeptical about the long-term impact from the decision.
I removed this line:
key: function(data) { return ko.utils.unwrapObservable(data.id); },
Now everything updates as it should. If anyone has thoughts about further processing I'm all ears (for example, does this affect performance?)