I've been working with Firebase for a little while, and like their suggestion to keep data denormalized. My only problem is figuring out the best way to query across multiple collections. For example, I have an Identity object which holds info on my users:
identities: {
$identity: {
name: string,
studio: $studioID
}}
That corresponds to a Studio object:
studios: {
$studio: {
name: string,
owner: $identityID,
location: $locationID
}}
This object references the owner, and also their location. The location object references Classes, which references Students.... on and on. Right now, in order to fetch a referenced object, I'm doing something like this:
Auth.loginUser(email, password, (success) => {
const identityRef = firebase.database().ref('/identity');
identityRef.child(success.uid).on("value", function(identitySnapshot) {
const identity = identitySnapshot.val();
const studioRef = firebase.database().ref('/studios');
dispatch({type: 'UPDATE_IDENTITY', identity})
studioRef.child(identity.studioID).on("value", function(studioSnapshot) {
const studio = studioSnapshot.val();
dispatch({type: 'UPDATE_STUDIO', studio});
})}); });
I would continue nesting my calls for Location, Classes, Students, etc. Is there a better way to do this?
Consider the following structures:
identities: {
$identityKey: {
name: string
}
}
identities_studios: {
$identityKey: {
$studioKey: {
name: string
}
}
}
identities_studios_locations: {
$identityKey: {
$studioKey: {
$locationKey: {
lat: string,
lng: string
}
}
}
}
The first, identities, only stores info about the identities as usual.
The second, identities_studios, only stores info about the studios, but the studios are grouped by $identityKey.
The third, identities_studios_locations, only stores info about the locations, but they are grouped firstly by $studioKey and secondly by $identityKey.
Now you can do this:
const db = firebase.database()
Auth.loginUser(email, password, success => {
db.ref(`/identities/${success.uid}`).on("value", snap => { ... }
db.ref(`/identities_studios/${success.uid}`).on("value", snap => { ... }
db.ref(`/identities_studios_locations/${success.uid}`).on("value", snap => { ... }
}
Instead of making multiple requests one after the other, we get them to run simultaneously.
If you want, after getting all this info from the database, you can transform the data structure to whatever you want: one array for identities, another for studios, another for locations, etc.; or a single array of identities with nested studios which in turn have nested locations etc.
Related
I'm looking for a way to fetch data from multiple collections in a subscription.
I have two collections: inspection_processes and inspections. Each inspection belongs to exact one inspection_process. Each inspection contains data (foreign keys) from another collection called devices.
My goal is to get a list of all inspection_processes and fetch their respective inspections and the devices specified in each inspection.
Here's the data from Firestore (displayed JSON-like for better readability:
inspection_processes = [{
id: 'AAA',
process_name: 'P-001'
}]
inspections = [{
inspection_id: 'BBB',
inspection_process_id: 'AAA',
device_id: 'CCC'
}]
devices = [{
device_id: 'DDD',
device_name: 'Computer'
}]
My current (simplified) approach:
fetchInspectionProcesses() {
let inspecion_processes_data = [];
// Get inspection processes
this.getInspectionProcesses().subscribe(inspecion_processes => {
for (let inspection_process of inspecion_processes) {
// Get inspections of inspection process
this.getInspectionsByProcessId(inspection_process['id']).subscribe(inspections => {
for (let inspection of inspections) {
// Get device info
this.getDeviceById(inspection['device_id']).subscribe(device => {
// ... push data to inspecion_processes_data array
})
}
})
}
})
}
I know that this approach isn't recommended since it leads to different problems (duplicate output in the UI, memory leaks, ...).
I did my best trying to solve it by using RxJS (e.g. by piping) but I didn't manage to use the right operators. The problem is not limited to having two or three collections to combine, I'm looking for a general approach on how to do this.
Working with duplicate data (e.g. putting the device's name in the inspections document isn't an option since my example is just simplified, there are many nestings I have to deal with.
EDIT: Screenshot of the Firebase Console
Can anyone help, please? Thank you in advance!
What you're talking about is basically a join in firestore. You should really duplicate your data so you don't have to do this. I wrote some functions to do this in my package
https://github.com/jdgamble555/adv-firestore-functions#Aggregation
But you could also do SQL like joins (but you will incur more reads):
https://fireship.io/lessons/firestore-joins-similar-to-sql/
J
This is how you might remove your nested subscriptions
fetchInspectionProcesses() {
let inspecion_processes_data = [];
this.getInspectionProcesses().pipe(
map(inspecion_processes => inspecion_processes.map(
inspection_process => this.getInspectionsByProcessId(inspection_process['id'])
)),
mergeMap(processes => merge(...processes)),
map(inspections => inspections.map(
inspection => this.getDeviceById(inspection['device_id'])
)),
mergeMap(devices => merge(...devices)),
toArray()
).subscribe(deviceArray => {
// ... This is your inspecion_processes_data array
});
}
Or, a little bit more compactly by combining map and mergeMap together:
fetchInspectionProcesses() {
let inspecion_processes_data = [];
this.getInspectionProcesses().pipe(
mergeMap(inspecion_processes => merge(...inspecion_processes.map(
inspection_process => this.getInspectionsByProcessId(inspection_process['id'])
))),
mergeMap(inspections => merge(...inspections.map(
inspection => this.getDeviceById(inspection['device_id'])
))),
toArray()
).subscribe(deviceArray => {
// ... Do something with deviceArray
});
}
I have this query in my code which allows me to build a tag cloud for this blog front page
tagCloud:allContentfulBlogPost {
group(field: tags, limit: 8) {
fieldValue
}
}
It's passing data that I map in my component using {data.tagCloud.group.map(tag => (...))};. The code works nicely, but it won't be limited by the filter I'm passing above in the group(fields: tags, limit: 8) in my query. It renders all the tags and not only the first eight.
I've unsuccessfully tried the skip filter as well for the sake of seeing if it works.
Is this the proper way to limit the count to my mapping component in Gatsby?
The Contentful source plugin doesn't define arguments on any of the nodes it creates, unfortunately. Instead you would need to create these yourself. The easiest way to do that is through the createResolvers API.
Here's a similar example from a project of mine:
// in gatsby-node.js
exports.createResolvers = ({ createResolvers }) => {
createResolvers({
SourceArticleCollection: {
// Add articles from the selected section(s)
articles: {
type: ["SourceArticle"],
args: {
// here's where the `limit` argument is added
limit: {
type: "Int",
},
},
resolve: async (source, args, context, info) => {
// this function just needs to return the data for the field;
// in this case, I'm able to fetch a list of the top-level
// entries that match a particular condition, but in your case
// you might want to instead use the existing data in your
// `source` and just slice it in JS.
const articles = await context.nodeModel.runQuery({
query: {
filter: {
category: {
section: {
id: {
in: source.sections.map((s) => s._ref),
},
},
},
},
},
type: "SourceArticle",
})
return (articles || []).slice(0, args.limit || source.limit || 20)
},
},
},
})
}
Because resolvers run as part of the data-fetching routines that support the GraphQL API, this will run server-side at build-time and only the truncated/prepared data will be sent down to the client at request time.
I am trying to follow the example of cursor-based paginating with React Apollo (https://www.apollographql.com/docs/react/data/pagination/#cursor-based) but am struggling with how my component that rendered the original data gets the new (appended) data.
This is how we get the original data and pass it to the component:
const { data: { comments, cursor }, loading, fetchMore } = useQuery(
MORE_COMMENTS_QUERY
);
<Comments
entries={comments || []}
onLoadMore={...}
/>
What I'm unsure of is how the fetchMore function works.
onLoadMore={() =>
fetchMore({
query: MORE_COMMENTS_QUERY,
variables: { cursor: cursor },
updateQuery: (previousResult, { fetchMoreResult }) => {
const previousEntry = previousResult.entry;
const newComments = fetchMoreResult.moreComments.comments;
const newCursor = fetchMoreResult.moreComments.cursor;
return {
// By returning `cursor` here, we update the `fetchMore` function
// to the new cursor.
cursor: newCursor,
entry: {
// Put the new comments in the front of the list
comments: [...newComments, ...previousEntry.comments]
},
__typename: previousEntry.__typename
};
}
})
}
From what I understand, yes, once my component will cal this onLoadMore function (using a button's onClick for example), it will fetch the data based on a new cursor.
My question is this. I'm sorry if this is too simple and I'm not understanding something basic.
How does the component get the new data?
I know the data is there, because I console logged the newComments (in my case, it wasn't newComments, but you get the idea.) And I saw the new data! But those new comments, how are they returned to the component that needs the data? And if I click the button again, it is still stuck on the same cursor as before.
What am I missing here?
In the updateQuery function lets you modify (override) the result for the current query. At the same time your component is subscribed to the query and will get the new result. Let's play this through:
Your component is rendered for the first time, component will subscribe to the query and receive the current result of the query from the cache if there is any. If not the query starts fetching from the GraphQL server and your component gets notified about the loading state.
If the query was fetched your component will get the data once the result came in. It now shows the first x results. In the cache an entry for your query field is created. This might look something like this:
{
"Query": {
"cursor": "cursor1",
"entry": { "comments": [{ ... }, { ... }] }
}
}
// normalised
{
"Query": {
"cursor": "cursor1",
"entry": Ref("Entry:1"),
}
"Entry:1": {
comments: [Ref("Comment:1"), Ref("Comment:2")],
},
"Comment:1": { ... },
"Comment:2": { ... }
}
User clicks on load more and your query is fetched again but with the cursor value. The cursor tells the API from which entry it should start returning values. In our example after Comment with id 2.
Query result comes in and you use the updateQuery function to manually update the result of the query in the cache. The idea here is that we want to merge the old result (list) with the new result list. We already fetched 2 comments and now we want to add the two new comments. You have to return a result that is the combined result from two queries. For this we need to update the cursor value (so that we can click "load more" again and also concat the lists of comments. The value is written to the cache and our normalised cache now looks like this:
{
"Query": {
"cursor": "cursor2",
"entry": { "comments": [{ ... }, { ... }, { ... }, { ... }] }
}
}
// normalised
{
"Query": {
"cursor": "cursor2",
"entry": Ref("Entry:1"),
}
"Entry:1": {
comments: [Ref("Comment:1"), Ref("Comment:2"), Ref("Comment:3"), Ref("Comment:4")],
},
"Comment:1": { ... },
"Comment:2": { ... },
"Comment:3": { ... },
"Comment:4": { ... }
}
Since your component is subscribed to the query it will get rerendered with the new query result from the cache! The data is displayed in the UI because we merged the query so that the component gets new data just as if the result had all four comments in the first place.
It depends on how you handle the offset. I'll try to simplify an example for you.
This is a simplified component that I use successfully:
const PlayerStats = () => {
const { data, loading, fetchMore } = useQuery(CUMULATIVE_STATS, {
variables: sortVars,
})
const players = data.GetCumulativeStats
const loadMore = () => {
fetchMore({
variables: { offset: players.length },
updateQuery: (prevResult, { fetchMoreResult }) => {
if (!fetchMoreResult) return prevResult
return {
...prevResult,
GetCumulativeStats: [
...prevResult.GetCumulativeStats,
...fetchMoreResult.GetCumulativeStats,
],
}
},
})
}
My CUMULATIVE_STATS query returns 50 rows by default. I pass the length of that result array to my fetchMore query as offset. So when I execute CUMULATIVE_STATS with fetchMore, the variables of the query are both sortVars and offset.
My resolver in the backend handles the offset so that if it is, for example, 50, it ignores the first 50 results of the query and returns the next 50 from there (ie. rows 51-100).
Then in the updateQuery I have two objects available: prevResult and fetchMoreResult. At this point I just combine them using spread operator. If no new results are returned, I return the previous results.
When I have fetched more once, the results of players.length becomes 100 instead of 50. And that is my new offset and new data will be queried the next time I call fetchMore.
Pop quiz, hotshot:
You're building a react native app. You set some values to firebase as an object at the root of your app, like this:
firebase
.database()
.ref("/companies/")
.set({
awesomeCompany: {
name: "Awesome Company",
owner: "Joe Awesome",
gmail: "joeawesome#gmail.com",
fleetSize: 2
},
badCompany: {
name: "Bad Company",
owner: "Joe Bad",
gmail: "joebad#gmail.com",
fleetSize: 3
}
You want to give the current user a text input field through which they may change the fleetSize of a company if they are the owner of that company.
You have your firebase auth working properly, so you know that firebase.auth().currentUser.email will work to check against to determine if they are an owner.
Your database values have been set - they look like this:
{
"companies": {
"awesomeCompany": {
"fleetSize": 2,
"gmail": "joeawesome#gmail.com",
"name": "Awesome Company",
"owner": "Joe Awesome"
},
"badCompany": {
"fleetSize": 3,
"gmail": "joebad#gmail.com",
"name": "Bad Company",
"owner": "Joe Bad"
}
}
}
How would you render the initial information to the screen, and how would you set up the text input logic so that the user input changes data at the database?
To understand the brain I have, and how I'm failing, I'm including my own code below as a starting point. If there's a way to show me how I could take my basic strategy and make it work - even if it isn't elegant - I'd appreciate that. But overall I'm just really struggling with how to get data path references using Data Snapshot and keep them available to use elsewhere.
Thanks for your help, anyone!
// my crummy half baked code below
import React, { Component } from "react";
import { View, Text, TextInput, Button } from "react-native";
import { styles } from "../styles";
import * as firebase from "firebase";
export default class OwnerProfileScreen extends React.Component {
constructor(props) {
super(props);
this.state = {
gmail: null,
name: null,
fleetSize: null
};
}
componentDidMount() {
this.getData();
}
getData = () => {
const rootRef = firebase.database().ref(); // firebase reference
const authEmail = firebase.auth().currentUser.email; // current user
return rootRef.once("value").then(
function(snapshot) {
const idArray = Object.keys(snapshot.child("companies/").val()); // array of Ids
const companyData = idArray.map(id =>
snapshot.child("companies/" + id).val()
); // values of contained in objects at each key
const ownersCompany = companyData.filter(
obj => obj.gmail === authEmail
); // an array containing one object if the gmail address in the object is the same as the currentUser logged in
// what is the path of fleetSize?
// how do I define it to keep it available to use later
// with a Text Input event?
this.setState({
name: ownersCompany[0].name,
gmail: ownersCompany[0].gmail,
fleetSize: ownersCompany[0].fleetSize
});
}.bind(this)
);
};
changeFleetSize = userInput => {
//in order to set the user input to the database, I need the path
//of the fleetSize of the current user (who has been verified as an
// owner by comparing firebase auth to gmail addresses of company)
};
render() {
return (
<View style={styles.container}>
<Text>minPrice = {this.state.name}</Text>
<Text>gmail = {this.state.gmail}</Text>
<Text>fleetSize = {this.state.fleetSize}</Text>
<TextInput
style={{ height: 40, borderColor: "gray", borderWidth: 1 }}
//onChangeText currently does nothing since I don't know how
// to get the particular path of particular fleetSize
onChangeText={userInput => this.changeFleetSize(userInput)}
/>
</View>
);
}
}
The code is quite messy so it's hard to say what you're trying to accomplish. But let me make a guess here:
You want to load a single company from your database.
You know the email address of the owner of the company.
If that is correct, you can use a query to accomplish the goal. Something like:
var query = rootRef.child("companies").orderByChild("gmail").equalTo(authEmail);
var self = this;
query.once("value").then(function(result) {
result.forEach(function(snapshot) { // loop over result snapshots, since there may be multiple
const companyData = snapshot.val();
self.setState({
name: companyData.name,
gmail: companyData.gmail,
fleetSize: companyData.fleetSize
});
})
);
The changes here:
Use a query to only select the companies with the correct gmail address.
Loop over the results, since (on an API level at least) there could be multiple companies with that value for their gmail property.
Get rid of the whole iterating over Object.keys and filtering, since that made it hard to read. This result is also more idiomatic for Firebase Realtime Database code.
Use self to track the this instance, just because I didn't feel like counting bind calls.
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)