So I have this for example in firebase
clients {
//clients with unique keys {
invoices: {
// Invoices with unique Keys
}
}
}
I'm returning all this with one ref like so:
.controller('singleClientController', function($scope, $firebaseObject, fbUrl, $routeParams) {
var id = $routeParams.id;
var singleRef = new Firebase(fbUrl+'/clients/'+id);
var client = this;
client.details = $firebaseObject(singleRef);
})
so in my html, I'm trying to return the $id for both the client and the invoice. I am able to get the client no problem with {{client.details.$id}} but when I try to do the same thing with the invoice id {{invoice.$id}} I don't get anything.
The invoices are displayed through a foreach like so:
<tr ng-repeat="invoice in client.details.invoices">
<td>
<a href="#/invoices/details/{{invoice.$id}}/{{client.details.$id}}">
{{invoice.settings.number}}
</a>
</td>
...
</tr>
Is it because the invoices are inside the client? If so, how would you return the id for the invoices? It's driving me nuts! Please help!
For a better understanding of my firebase set-up here is a screenshot of what I'm talking about.
tl;dr - In Firebase it is ideal to have a flat data structure.
JSBin Example
In your case you have invoices nested under clients.
{
"clients": {
"1": {
"name": "Alison",
"invoices": {
"0001": {
"amount": 500
"paid": true
}
}
}
}
}
This feels natural because the invoices are nicely grouped underneath the proper client. However, this can lead to bad performance in syncing data with Firebase. Firebase reads all data underneath a node.
This means every time you read /clients/1 you also get the invoices downloaded over the network. Even if you just need the client's name, you'll also get the the invoices.
The solution is to flatten your data structure.
{
"clients": {
"1": {
"name": "Alison"
}
},
"clientInvoices": {
"1": {
"0001": {
"amount": 500
"paid": true
}
}
}
}
The important part to grasp here is the shared key.
In this example the key is 1. This is for simplicity. In reality you'll likely use a .push() id.
By using this pattern you'll still be able to retrieve all of the invoices for the client by simply knowing their key. This also decouples the client from the invoices.
As an added benefit your controller code and ng-repeat will be much easier.
In your case you should switch from a $firebaseObject to a $firebaseArray for the invoices. We can even create a helper factory that gets the invoices by a client's id.
.factory('invoices', function(fbUrl, $firebaseArray) {
return function(clientId) {
var ref = new Firebase(fbUrl).child('clientInvoices').child(clientId);
return $firebaseArray(ref);
}
})
// While we're at it, lets create a helper factory for retrieving a
// client by their id
.factory('clients', function(fbUrl, $firebaseObject) {
return function(clientId) {
var ref = new Firebase(fbUrl).child('clients').child(clientId);
return $firebaseObject(ref);
}
})
Now inject the helper factories into your controller and use the $routeParams.id to retrieve the client's invoices:
.controller('singleClientController', function($scope, $routeParams, invoices, clients) {
$scope.client = clients($routeParams.id);
$scope.clientInvoices = invoices($routeParams.id);
})
Now binding it to your template is a breeze:
<tr ng-repeat="invoice in clientInvoices">
<td>
<a href="#/invoices/details/{{invoice.$id}}/{{client.$id}}">
{{invoice.settings.number}}
</a>
</td>
...
</tr>
Related
I'm working on a simple registration system using Firebase as a backend. I am successfully authenticating users and writing to the database. I have an index of courses and users with the following structure:
{
courses: { // index all the courses available
key1: {
title: "Course 1",
desc: "This is a description string.",
date: { 2018-01-01 12:00:00Z }
members: {
user1: true
...
}
},
key2 { ... },
},
users: { // track individual user registrations
user1: {
key1: true,
...
},
user2: { ... }
}
}
I have a cloud function that watches for the user to add a course and it builds an array with the corresponding courseId that will look at the courses node to return the appropriate items.
exports.listenForUserClasses = functions.database.ref('users/{userId}')
.onWrite(event => {
var userCourses = [];
var ref = functions.database.ref('users/{userId}');
for(var i=0; i<ref.length; i++) {
userCourses.push(ref[i])
}
console.log(userCourses); // an array of ids under the user's node
});
So, my question has two parts:
How can I build the updated object when the page is loaded?
How do I return the function to the client script?
Question 1: From the client side you want to get the reference to the database path. Then you want to call the child_added event. Keep it in-memory, this will be called whenever one is add then you can update your UI.
var ref = db.ref("path/to/courses");
ref.on("child_added", function(snapshot, prevChildKey) {
var newClass = snapshot.val();
});
If you are completely refreshing the page then you can always grab the data again from the database path by using the value option and calling once
Questions 2: You don't. This is considered an asynchronous function. If you wanted a response from the function then you would setup an HTTP trigger and wait for the response from that function.
What I am trying to do
I am creating a social media app with react native and firebase. I am trying to call a function, and have that function return a list of posts from off of my server.
Problem
Using the return method on a firebase query gives me a hard to use object array:
Array [
Object {
"-L2mDBZ6gqY6ANJD6rg1": Object {
//...
},
},
]
I don't like how there is an object inside of an object, and the whole thing is very hard to work with. I created a list inside my app and named it items, and when pushing all of the values to that, I got a much easier to work with object:
Array [
Object {
//...
"key": "-L2mDBZ6gqY6ANJD6rg1",
},
]
This object is also a lot nicer to use because the key is not the name of the object, but inside of it.
I would just return the array I made, but that returns as undefined.
My question
In a function, how can I return an array I created using a firebase query? (to get the objects of an array)
My Code
runQ(group){
var items = [];
//I am returning the entire firebase query...
return firebase.database().ref('posts/'+group).orderByKey().once ('value', (snap) => {
snap.forEach ( (child) => {
items.push({
//post contents
});
});
console.log(items)
//... but all I want to return is the items array. This returns undefined though.
})
}
Please let me know if I'm getting your question correctly. So, the posts table in database looks like this right now:
And you want to return these posts in this manner:
[
{
"key": "-L1ELDwqJqm17iBI4UZu",
"message": "post 1"
},
{
"key": "-L1ELOuuf9hOdydnI3HU",
"message": "post 2"
},
{
"key": "-L1ELqFi7X9lm6ssOd5d",
"message": "post 3"
},
{
"key": "-L1EMH-Co64-RAQ1-AvU",
"message": "post 4"
}
...
]
Is this correct? If so, here's what you're suppose to do:
var items = [];
firebase.database().ref('posts').orderByKey().once('value', (snapshot) => {
snapshot.forEach((child) => {
// 'key' might not be a part of the post, if you do want to
// include the key as well, then use this code instead
//
// const post = child.val();
// const key = child.key;
// items.push({ ...post, key });
//
// Otherwise, the following line is enough
items.push(child.val());
});
// Then, do something with the 'items' array here
})
.catch(() => { });
Off the topics here: I see that you're using firebase.database().... to fetch posts from the database, are you using cloud functions or you're fetching those posts in your App, using users' devices to do so? If it's the latter, you probably would rather use cloud functions and pagination to fetch posts, mainly because of 2 reasons:
There might be too many posts to fetch at one time
This causes security issues, because you're allowing every device to connect to your database (you'd have to come up with real good security rules to keep your database safe)
I aggregated some data and published it, but I'm not sure how/where to access the subscribed data. Would I be able to access WeeklyOrders client collection (which is defined as client-only collection i.e WeeklyOrders = new Mongo.Collection(null);)?
Also, I see "self = this;" being used in several examples online and I just used it here, but not sure why. Appreciate anyone explaining that as well.
Here is publish method:
Meteor.publish('customerOrdersByWeek', function(customerId) {
check(customerId, String);
var self = this;
var pipeline = [
{ $match: {customer_id: customerId} },
{ $group: {
_id : { week: { $week: "$_created_at" }, year: { $year: "$_created_at" } },
weekly_order_value: { $sum: "$order_value" }
}
},
{ $project: { week: "$_id.week", year: "$_id:year" } },
{ $limit: 2 }
];
var result = Orders.aggregate(pipeline);
result.forEach(function(wo) {
self.added('WeeklyOrders', objectToHash(wo._id), {year: wo.year, week: wo.week, order_value: wo.weekly_order_value});
});
self.ready();
});
Here is the route:
Router.route('/customers/:_id', {
name: 'customerOrdersByWeek',
waitOn: function() {
return [
Meteor.subscribe('customerOrdersByWeek', this.params._id)
];
},
data: function() { return Customers.findOne(this.params._id); }
});
Here is my template helper:
Template.customerOrdersByWeek.helpers({
ordersByWeek: function() {
return WeeklyOrders.find({});
}
});
You want var self = this (note the var!) so that call to self.added works. See this question for more details. Alternatively you can use the new es6 arrow functions (again see the linked question).
There may be more than one issue where, but in your call to added you are giving a random id. This presents two problems:
If you subscribe N times, you will get N of the same document sent to the client (each with a different id). See this question for more details.
You can't match the document by id on the client.
On the client, you are doing a Customers.findOne(this.params._id) where this.params._id is, I assume, a customer id... but your WeeklyOrders have random ids. Give this a try:
self.added('WeeklyOrders', customerId, {...});
updated answer
You'll need to add a client-only collection as a sort-of mailbox for your publisher to send WeeklyOrders to:
client/collections/weekly-orders.js
WeeklyOrders = new Meteor.Collection('WeeklyOrders');
Also, because you could have multiple docs for the same user, you'll probably need to:
Forget what I said earlier and just use a random id, but never subscribe more that once. This is an easy solution but somewhat brittle.
Use a compound index (combine the customer id + week, or whatever is necessary to make them unique).
Using (2) and adding a customerId field so you can find the docs on the client, results in something like this:
result.forEach(function (wo) {
var id = customerId + wo.year + wo.week;
self.added('WeeklyOrders', id, {
customerId: customerId,
year: wo.year,
week: wo.week,
order_value: wo.weekly_order_value,
});
});
Now on the client you can find all of the WeeklyOrders by customerId via WeeklyOrders.find({customerId: someCustomerId}).
Also note, that instead of using pub/sub you could also just do all of this in a method call. Both are no-reactive. The pub/sub gets you collection semantics (the ability to call find etc.), but it adds the additional complexity of having to deal with ids.
A few questions about storing user data in MongoDB. What is the best place in mongo to store user specific data, such as User settings, User photo url, User friends, User events?
In Mongo, user data is stored in:
Meteor
/ Collections
/ users
/ _id
/ profile
/ services
Should I add there a new collections? In a following way:
/ events / _id's
/ friends / _id's
/ messages / _id's
/ settings
How should I publish user's private data and manipulate this collections, to be sure it's save and no one else will modify or have access to private data of another person.
You can add data to the users profile field like this:
Meteor.users.update( id, { $set: { 'profile.friends': someValue } } );
To only publish specific fields you can do something like this:
Meteor.publish( 'users', function () {
return Meteor.users.find( {}, { fields: { 'profile.friends': 1 } } );
});
Hope this helps.
Normalization
"Database normalization is the process of organizing the attributes and tables of a relational database to minimize data redundancy."
MongoDB is a non relational database. This makes normalized data hard to query for. This is why in MongoDB we denormalize data. That makes querying for it easier.
It depends on your use-case. The question is basically when to demormalize. It's mostly a matter of opinion. But objective here are some pros and cons:
Pros to demormalization
It's easier to retrieve data (Due to Mongo not beeing a relational DB)
It performs better if you are always getting the data in bulk
Cons to demormalization
It doesn't scale well for things like user.messages (You can't just publicize some messages)
In your case I'd definitly go for seperate collections for events, friends and messages. Setting can't expand infinitly. So I'd put it into the users collection.
Security
I'd use a publications and allow and deny for this. Let me make an example for Messages:
Collection
Messages = new Mongo.Collection('Messages')
Messages.insert({
sender: Meteor.userId,
recipient: Meteor.users.findOne()._id,
message: 'Hello world!'
})
Publication
Meteor.publish('userMessages', function (limit) {
return Messages.subscribe({
$or: [
{sender: this.userId},
{recipient: this.userId}
]
}, {limit: limit})
})
Allow
function ownsMessage (user, msg) {
return msg.sender === user ? true : false
}
Messages.allow({
insert: function (userId, newDoc) {
!!userId
},
update: function (userId, oldDoc, newDoc) {
if(
ownsMessage(userId, oldDoc) &&
ownsMessage(userId, newDoc)
) return true
return false
},
remove: function () {
return false
}
})
This code is untested, so it might contain small errors
I am working with backbone at the moment, I run a fetch on a new model, and get a response from the server, the model I am fetching should have other models and collections within it, the JSON returned supports this and it looks something like this,
{
"id" : 230,
"name" : "A project name",
"briefversion":{
"id":199,
"project_id":230,
"version_number":1,
"name":"Version 1",
"created_at":"2015-05-14 10:22:29",
"updated_at":"2015-05-14 10:22:29",
"briefversionsections":[{
"id":947,
"briefversion_id":199,
"position":1,
"name":"Overview",
"content":"<p>A general description of the project and some background information, also worth including some context, where is the work going to be used? Billboards, online, showroom etc</p><div><img src="//www.sketchup.com/images/case_study/architecture/robertson_walsh_3.jpg"/></div>",
"created_at":"2015-05-14 10:22:29",
"updated_at":"2015-05-14 10:22:29",
"briefsectonattachments":{}
}, {
"id":948,
"briefversion_id":199,
"position":2,
"name":"Scope of work",
"content":"<p>A list of the deliverables, e.g.</p><ul><li>An exterior view</li><li>An interior view</li><li>An animation</li><li>A website</li></ul>",
"created_at":"2015-05-14 10:22:29",
"updated_at":"2015-05-14 10:22:29",
"briefsectonattachments":{}
},{
"id":949,
"briefversion_id":199,
"position":3,
"name":"Target market",
"content":"<p>ASCribe who the work is to appeal to, what are the demographics and end user types.</p>",
"created_at":"2015-05-14 10:22:29",
"updated_at":"2015-05-14 10:22:29",
"briefsectonattachments":{
}
}]
},
"organisations":{
"id":55,
"name":"Jaguar",
"uri_hash":"jaguar",
"slug":"S336e056",
"information":"",
"type":"organisation",
"currency":"USD",
"notifications":"0",
"add_all":"0",
"created_at":"-0001-11-30 00:00:00",
"updated_at":"2015-05-20 09:16:21",
"users":[
{
"id":111,
"email":"xxxxxxxx#gmail.com",
"first_name":"Matty",
"last_name":"Brook",
"display_name":"mattybrook",
"initials":"MB",
"remember_me":null,
"active":"1",
"invite_code":null,
"forgotten_code":null,
"cost_visible":0,
"login_type":"normal",
"api_token":null,
"created_at":"2015-03-16 15:49:58",
"updated_at":"2015-05-15 13:12:45",
"deleted_at":null,
"pivot":{
"organisation_id":55,
"user_id":111,
"is_admin":"0"
}
}
}
So after the fetch, how can I make sure that briefversion becomes a model and within that briefversionsections within that becomes a collection, similarly how do I make sure that the users attribute of the organisation object also become a collection?
You will need to override parse to handle getting the JSON from your server in the right format. Once that's done you can then instantiate your collections for some of the properties in the initialize method.
For example
initialize: function () {
this.briefversionsections = new Backbone.Collection(this.briefversionsections);
this.users = new Backbone.Collection(this.users);
},
parse: function (response, options) {
var myModel = response.briefversion;
myModel.users= response.organisations.users
return myModel;
}