collectionGroup within a certain path - firebase

I'd like to do a collection group query but within a certain path, meaning that I would like to target collections not only with the collectionId but also with where the collection is.
Let's use the example used in the doc to explain it better. We would have landmarks in cities and "general" landmarks in their own collection :
let citiesRef = db.collection('cities');
let landmarks = Promise.all([
citiesRef.doc('SF').collection('landmarks').doc().set({
name: 'Golden Gate Bridge',
type: 'bridge'
}),
citiesRef.doc('SF').collection('landmarks').doc().set({
name: 'Legion of Honor',
type: 'museum'
}),
citiesRef.doc('LA').collection('landmarks').doc().set({
name: 'Griffith Park',
type: 'park'
}),
citiesRef.doc('LA').collection('landmarks').doc().set({
name: 'The Getty',
type: 'museum'
}),
citiesRef.doc('DC').collection('landmarks').doc().set({
name: 'Lincoln Memorial',
type: 'memorial'
})
]);
let generalLandmarks = Promise.all([
db.collection('landmarks').doc().set({
name: 'National Air and Space Museum',
type: 'museum'
}),
db.collection('landmarks').doc().set({
name: 'Ueno Park',
type: 'park'
}),
db.collection('landmarks').doc().set({
name: 'National Museum of Nature and Science',
type: 'museum'
}),
db.collection('landmarks').doc().set({
name: 'Jingshan Park',
type: 'park'
}),
db.collection('landmarks').doc().set({
name: 'Beijing Ancient Observatory',
type: 'museum'
})
]);
Now I would like to query for landmarks that are in a city and not get the general ones. In a simpler way, I would like to do something like this :
let museums = db.collection('cities').collectionGroup('landmarks').where('type', '==', 'museum');
Is it possible ?

This is currently not possible with Cloud Firestore. When you perform a collection group query, it will use all of the collections and subcollections with the given name. You are not able to narrow to scope of the query to specific collections.
What you can do instead is store a field inside the documents in each subcollection that identify which top-level collection they belong to, then use that field to filter your results.

Now I would like to query for landmarks that are in a city and not get the general ones.
No, you cannot. Collection group query will return results from all collections or subcollections with the same name, including the general ones. There is no way you can change this.
The solution for that is to change the name of your collections to be different if you want to limit the scope of your collection group query.
If you app is in productuon and you cannot change the name of your collections, simply try to ignore the results that you get client side. How can you achieve this, just take a look a the path of the document and see from which collections belong. In this way you can only use the result from the collection you need.
Another workaround would be to add a new property of type boolean in your collections named isGeneral and set it tu true in the general collection and false in the others. To get only the particular items and not the general once, add to your query a where call:
whereEqualTo("isGeneral", false);

Related

Firebase - snapshot based on ids

I've been looking everywhere for the logic to build a specific request, but I don't completely get the firebase philosophy. This is my data structure:
`users:{
u1_id:{...}
u2_id:{...}
...
},
contacts:{
u1_id:{
contact1_id,
contact2_id,
...
}
}`
My first option would be a request that gets the data regarding the user id, the second one (the actual one) is to store the data I need from the contacts when they're added but I would need to run a big update for each contacts of each user and that doesn't seem to be the right solution. (I've check questions, firebase doc and videos)
The closest solution I found is this (from their youtube):
function getUserContacts(key, cb){
const rootRef = firebase.database().ref()
const contactsRef = rootRef.child('contacts')
const usersRef = rootRef.child('users')
contactsRef.child(key).on('child_added', snap => {
let userRef = usersRef.child(snap.key)
userRef.once('value', cb)
})
}
but it gives the users 1 by 1 and not an object with all my users
Edit: my temporary solution ->
firebase.database().ref('users')
.on('value', snaps => {
firebase.database().ref(`contacts/${currentUser.uid}`)
.on('value', snapshot => {
let userContacts = []
snapshot.forEach(snapc => {
snaps.forEach(snapu => {
let duser = snapu
if(snapc.key == snapu.key)
{
userContacts.push(snapu.val())
}
})
})
dispatch({
type: CONTACTS_FETCH,
payload: userContacts
})
})
})
The question is still a bit unclear but let me present three options:
If we want to populate a list with user_1's contacts, we read in the users contacts node by value. In this case, user_1 has two contacts; user_id2 and user_id3
contacts
user_1
user_id2: true
user_id3: true
Value reads in the parent node (user_1) with all of the child data as well. Because we have the entire node of data (a snapshot) we can iterate over the child values of user_1 to obtain each user_idx (which is the key of each child node). As you iterate over the children, each will present a key, to which you can then read in that specific user node.
A second option is to change the structure.
contacts
user_2
name: "John"
rank: "Captain"
contact_of: "user_1"
user_3
name: "William"
rank: "Second Officer"
contact_of: "user_1"
with this structure, if user 1 wants to load their contacts, a simple query on the contacts node where contact_of = "user_1". That would load each contact and all of their data.
This second example works when a user has specific contacts that are not shared.
If multiple users can have multiple contacts, here's another option.
contacts
user_2
name: "Jean Luc"
rank: "Captain"
contact_of
user_1: true
user_2: true
user_3
name: "William"
rank: "Second Officer"
contact_of:
user_1: true
user_2: true
in this case, perform a deep query on the contacts node where contact_of/user_1 = true, which will present user_2 and user_3/

Where to store Record meta data with Redux and Immutable JS

I switched over to a Redux + Immutable JS project from Ember a few months ago and am overall enjoying the experience.
One problem I still have not found a nice solution for when working with Records is storing meta data for that Record.
For example, let's say I have a User record:
const userRecord = Immutable.Record({
id: null,
name: '',
email: ''
});
For the User, I may also wish to store properties like isLoading or isSaved. The first solution would be to store these in the userRecord. Although this would be the easiest solution by far, this feels wrong to me.
Another solution might be to create a User Map, which contains the User Record, as well as meta data about the User.
Ex.
const userMap = Immutable.Map({
record: Immutable.Record({
id: null,
name: '',
email: ''
}),
isLoading: false,
isSaved: true
});
I think this is more elegant, but I don't like how all the user properties become even more deeply nested, so accessing User properties becomes very verbose.
What I miss most about Ember is being able to access Model properties easily.
Ex. user.get('isSaved') or user.get('name')
Is it possible to recreate something like this with Redux and Immutable? How have you approached this situation before?
I might be misunderstanding the problem, because
What I miss most about Ember is being able to access Model properties easily.
user.get('isSaved') or user.get('name')
This does work for Immutable records.
If you don't want to add too many properties to your record, you could have a single status property and add some getters (assuming your statuses are mutually exclusive):
const STATUS = {
INITIAL: 'INITIAL',
LOADING: 'LOADING',
SAVING: 'SAVING
};
class UserRecord extends Immutable.Record({
id: null,
name: '',
email: '',
status: STATUS.INITIAL}) {
isLoading() {
return this.get('status') === STATUS.LOADING;
}
isSaving() {
return this.get('status') === STATUS.SAVING;
}
}
new UserRecord().isLoading()); // returns false
new UserRecord({status: STATUS.LOADING}).isLoading(); // returns true
new UserRecord().set('status', STATUS.LOADING).isLoading(); // returns true

Meteor Framework Subscribe/Publish according to document variables

I have a game built on Meteor framework. One game document is something like this:
{
...
participants : [
{
"name":"a",
"character":"fighter",
"weapon" : "sword"
},
{
"name":"b",
"character":"wizard",
"weapon" : "book"
},
...
],
...
}
I want Fighter character not to see the character of the "b" user. (and b character not to see the a's) There are about 10 fields like character and weapon and their value can change during the game so as the restrictions.
Right now I am using Session variables not to display that information. However, it is not a very safe idea. How can I subscribe/publish documents according to the values based on characters?
There are 2 possible solutions that come to mind:
1. Publishing all combinations for different field values and subscribing according to the current state of the user. However, I am using Iron Router's waitOn feature to load subscriptions before rendering the page. So I am not very confident that I can change subscriptions during the game. Also because it is a time-sensitive game, I guess changing subscriptions would take time during the game and corrupt the game pleasure.
My problem right now is the user typing
Collection.find({})
to the console and see fields of other users. If I change my collection name into something difficult to find, can somebody discover the collection name? I could not find a command to find collections on the client side.
The way this is usually solved in Meteor is by using two publications. If your game state is represented by a single document you may have problem implementing this easily, so for the sake of an example I will temporarily assume that you have a Participants collection in which you're storing the corresponding data.
So anyway, you should have one subscription with data available to all the players, e.g.
Meteor.publish('players', function (gameId) {
return Participants.find({ gameId: gameId }, { fields: {
// exclude the "character" field from the result
character: 0
}});
});
and another subscription for private player data:
Meteor.publish('myPrivateData', function (gameId) {
// NOTE: not excluding anything, because we are only
// publishing a single document here, whose owner
// is the current user ...
return Participants.find({
userId: this.userId,
gameId: gameId,
});
});
Now, on the client side, the only thing you need to do is subscribe to both datasets, so:
Meteor.subscribe('players', myGameId);
Meteor.subscribe('myPrivateData', myGameId);
Meteor will be clever enough to merge the incoming data into a single Participants collection, in which other players' documents will not contain the character field.
EDIT
If your fields visibility is going to change dynamically I suggest the following approach:
put all the restricted properties in a separated collection that tracks exactly who can view which field
on client side use observe to integrate that collection into your local player representation for easier access to the data
Data model
For example, the collection may look like this:
PlayerProperties = new Mongo.Collection('playerProperties');
/* schema:
userId : String
gameId : String
key : String
value : *
whoCanSee : [String]
*/
Publishing data
First you will need to expose own properties to each player
Meteor.publish('myProperties', function (gameId) {
return PlayerProperties.find({
userId: this.userId,
gameId: gameId
});
});
then the other players properties:
Meteor.publish('otherPlayersProperties', function (gameId) {
if (!this.userId) return [];
return PlayerProperties.find({
gameId: gameId,
whoCanSee: this.userId,
});
});
Now the only thing you need to do during the game is to make sure you add corresponding userId to the whoCanSee array as soon as the user gets ability to see that property.
Improvements
In order to keep your data in order I suggest having a client-side-only collection, e.g. IntegratedPlayerData, which you can use to arrange the player properties into some manageable structure:
var IntegratedPlayerData = new Mongo.Collection(null);
var cache = {};
PlayerProperties.find().observe({
added: function (doc) {
IntegratedPlayerData.upsert({ _id : doc.userId }, {
$set: _.object([ doc.key ], [ doc.value ])
});
},
changed: function (doc) {
IntegratedPlayerData.update({ _id : doc.userId }, {
$set: _.object([ doc.key ], [ doc.value ])
});
},
removed: function (doc) {
IntegratedPlayerData.update({ _id : doc.userId }, {
$unset: _.object([ doc.key ], [ true ])
});
}
});
This data "integration" is only a draft and can be refined in many different ways. It could potentially be done on server-side with a custom publish method.

Retrieving data from firebase Array

My Firebase Array has the following structure:
[
{
'userId': '12345',
'itemOrdered' : 'abc',
'status': 'pending'
...other attributes
},
{
'userId': '6789',
'itemOrdered' : 'def',
'status' : 'pending',
...other attributes
},
{
'userId': '12345',
'itemOrdered' : 'def',
'status' : 'complete',
...other attributes
},
]
I am not able to figure out how to retrieve the following data:
Get records with userId = xxx
Get all records where 'itemOrdered" = 'def'
Firebase docs talk about using orderByChild but that doesn't make much sense.
Assuming you're using the JavaScript SDK to access Firebase:
ref.orderByChild('userId').equalTo('xxx')
ref.orderByChild('itemOrdered').equalTo('def')
If you're trying to build a query that gets order of item def from user xxx, then that's not currently possible with Firebase's querying. The only way to query the value of multiple properties is to combine them in a single property in a way that allows the query you want. E.g.
ref.orderByChild('userId_itemOrdered').equalTo('xxx_def')
There are additional options depending on your platform.
1) Query for the userId and then filter in code for the item you are looking for.
2) Query for the userId and build another query based on those results.
3) Flatten your data!
For example: create a node called user_purchases and each child could be a userId node with that users purchased itemId's (that makes it a snap to know exactly what items a user purchased). Or create an items_purchased node with each child being an item number node and then associated userId's who purchased that item.

handling complex queries in firebase

I'm developing a shopping cart mobile application using ionic framework and FIREBASE as back-end . I have a requirement that I need to join JOIN, SORT, FILTER and PAGINATE by multiple data attributes.
Sample data structure is given below.
products : {
prod1:{
name: Samsung-s4,
type: pro,
category: Phone,
created_datetime: 1426472828282,
user: user1,
price: 400
},
prod2:{
name: iPhone 5s,
type: pro,
category: Phone,
created_datetime: 1426472635846,
user: user2,
price: 500
},
prod3:{
name: HP Laptop i3,
type: regular,
category: Computer,
created_datetime: 1426472111111,
user: user1,
price: 600
}
}
user_profiles : {
user1:{
name: abc_user,
display_name: ABC,
email: abc#mail.com
},
user2:{
name: xyz_user,
display_name: XYZ,
email: xyz#mail.com
}
}
I need to query the products using multiple ways. two simple examples given below.
1) Get all products with Pagination, where type is "pro" then join with user_profiles and SORT by created date.
2) Get all products with Pagination, filter by category then join with user_profiles and SORT by created date.
Like above there will be more and more filtering options coming in Eg: price.
My main problem is, I couldn't find straight forward way of doing this using FIREBASE query options. Also I referred firebase util documentation but there also I don't see a way of getting this done.
As far as I see only way to get this done is, do the majority of processing in client end by getting all the data (or majority of the data) in to client side and do the SORT / FILTER / PAGINATE in client end.
But we are expecting thousands of records in these schemas, therefore there will be a huge performance impact if we do the processing in client end !!
Appreciate your expertise/support to solve this problem.
Thank you in advance.
UPDATE:
I changed the data structure (as webduvet explained) and tried with firebase-util but failed to achieve what i want. CRITERIA :Products -> Filter By type/pro -> Sort By products.created_date
type: {
pro:{
product1: user1,
product3: user1,
...
},
regular:{
...
}
}
firebase-util - angularfire code
var list = $firebase(new Firebase.util.NormalizedCollection(
ref.child("products").orderByChild('created_datetime'),
ref.child('type').child('pro')
).select(
{key: "products.name" , alias: 'name'}
).ref()).$asArray();
"products" will have hundred thousand records, so we have to make sure we restrict as much possible in firebase end rather than handling in client end.
Please help !
This has to be done not by queries but by database design. You need to store data in de-normalised way for example:
type: {
pro:{
product1: user1,
product3: user1,
...
},
regular:{
...
}
}
the above structure will give you option to query all pro products and retrieve the user id as well. Firebase offer good sorting mechanism so there should not be a problem. The more complex query you require the more complex the data structure will be needed and the more denormalized data you will have.
But as pointed out by #Swordfish0321 sql type of db might suit you much better after all.

Resources