Cloud Firestore deep get with subcollection - database

Let's say we have a root collection named 'todos'.
Every document in this collection has:
title: String
subcollection named todo_items
Every document in the subcollection todo_items has
title: String
completed: Boolean
I know that querying in Cloud Firestore is shallow by default, which is great, but is there a way to query the todos and get results that include the subcollection todo_items automatically?
In other words, how do I make the following query include the todo_items subcollection?
db.collection('todos').onSnapshot((snapshot) => {
snapshot.docChanges.forEach((change) => {
// ...
});
});

This type of query isn't supported, although it is something we may consider in the future.

If anyone is still interested in knowing how to do deep query in firestore, here's a version of cloud function getAllTodos that I've come up with, that returns all the 'todos' which has 'todo_items' subcollection.
exports.getAllTodos = function (req, res) {
getTodos().
then((todos) => {
console.log("All Todos " + todos) // All Todos with its todo_items sub collection.
return res.json(todos);
})
.catch((err) => {
console.log('Error getting documents', err);
return res.status(500).json({ message: "Error getting the all Todos" + err });
});
}
function getTodos(){
var todosRef = db.collection('todos');
return todosRef.get()
.then((snapshot) => {
let todos = [];
return Promise.all(
snapshot.docs.map(doc => {
let todo = {};
todo.id = doc.id;
todo.todo = doc.data(); // will have 'todo.title'
var todoItemsPromise = getTodoItemsById(todo.id);
return todoItemsPromise.then((todoItems) => {
todo.todo_items = todoItems;
todos.push(todo);
return todos;
})
})
)
.then(todos => {
return todos.length > 0 ? todos[todos.length - 1] : [];
})
})
}
function getTodoItemsById(id){
var todoItemsRef = db.collection('todos').doc(id).collection('todo_items');
let todo_items = [];
return todoItemsRef.get()
.then(snapshot => {
snapshot.forEach(item => {
let todo_item = {};
todo_item.id = item.id;
todo_item.todo_item = item.data(); // will have 'todo_item.title' and 'todo_item.completed'
todo_items.push(todo_item);
})
return todo_items;
})
}

I have faced the same issue but with IOS, any way if i get your question and if you use auto-ID for to-do collection document its will be easy if your store the document ID as afield with the title field
in my case :
let ref = self.db.collection("collectionName").document()
let data = ["docID": ref.documentID,"title" :"some title"]
So when you retrieve lets say an array of to-do's and when click on any item you can navigate so easy by the path
ref = db.collection("docID/\(todo_items)")
I wish i could give you the exact code but i'm not familiar with Javascript

I used AngularFirestore (afs) and Typescript:
import { map, flatMap } from 'rxjs/operators';
import { combineLatest } from 'rxjs';
interface DocWithId {
id: string;
}
convertSnapshots<T>(snaps) {
return <T[]>snaps.map(snap => {
return {
id: snap.payload.doc.id,
...snap.payload.doc.data()
};
});
}
getDocumentsWithSubcollection<T extends DocWithId>(
collection: string,
subCollection: string
) {
return this.afs
.collection(collection)
.snapshotChanges()
.pipe(
map(this.convertSnapshots),
map((documents: T[]) =>
documents.map(document => {
return this.afs
.collection(`${collection}/${document.id}/${subCollection}`)
.snapshotChanges()
.pipe(
map(this.convertSnapshots),
map(subdocuments =>
Object.assign(document, { [subCollection]: subdocuments })
)
);
})
),
flatMap(combined => combineLatest(combined))
);
}

As pointed out in other answers, you cannot request deep queries.
My recommendation: Duplicate your data as minimally as possible.
I'm running into this same problem with "pet ownership". In my search results, I need to display each pet a user owns, but I also need to be able to search for pets on their own. I ended up duplicated the data. I'm going to have a pets array property on each user AS WELL AS a pets subcollection. I think that's the best we can do with these kinds of scenarios.

According to docs, you needs to make 2 calls to the firestore.. one to fetch doc and second to fetch subcollection.
The best you can do to reduce overall time is to make these two calls parallelly using promise.All or promise.allSettled instead of sequentially.

You could try something like this:
db.collection('coll').doc('doc').collection('subcoll').doc('subdoc')

Related

How could I write this function so it doesn't setState within the foreach everytime

The function collects role Assignment PrincipalIds on an item in SPO. I then use a foreach to populate state with the Title's of these PrincipalIds. This all works fine but it's inefficient and I'm sure there is a better way to do it than rendering multiple times.
private _permJeChange = async () => {
if(this.state.userNames){
this.setState({
userNames: []
});
}
var theId = this.state.SelPermJEDD;
var theId2 = theId.replace('JE','');
var info = await sp.web.lists.getByTitle('MyList').items.getById(theId2).roleAssignments();
console.log(info, 'info');
var newArr = info.map(a => a.PrincipalId);
console.log(newArr, 'newArr');
// const userIds = [];
// const userNames = [];
// const userNameState = this.state.userNames;
newArr.forEach(async el => {
try {
await sp.web.siteUsers.getById(el).get().then(u => {
this.setState(prevState => ({
userNames: [...prevState.userNames, u.Title]
}));
// userNames.push(u.Title);
// userIds.push(el);
});
} catch (err) {
console.error("This JEForm contains a group");
}
});
}
I've left old code in there to give you an idea of what I've tried. I initially tried using a local variable array const userNames = [] but declaring it locally or even globally would clear the array everytime the array was populated! So that was no good.
PS. The reason there is a try catch is to handle any SPO item that has a permissions group assigned to it. The RoleAssignments() request can't handle groups, only users.
Create an array of Promises and await them all to resolve and then do a single state update.
const requests = info.map(({ PrincipalId }) =>
sp.web.siteUsers.getById(PrincipalId).get().then(u => u.Title)
);
try {
const titles = await Promise.all(requests);
this.setState(prevState => ({
userNames: prevState.userNames.concat(titles),
}));
} catch (err) {
console.error("This JEForm contains a group");
}

Firestore: calling collections.get() inside promise()

useEffect(() => {
if (!stop) {
// get current user profile
db.collection('events').get(eventId).then((doc) => {
doc.forEach((doc) => {
if (doc.exists) {
let temp = doc.data()
let tempDivisions = []
temp["id"] = doc.ref.id
doc.ref.collection('divisions').get().then((docs) => {
docs.forEach(doc => {
let temp = doc.data()
temp["ref"] = doc.ref.path
tempDivisions.push(temp)
});
})
temp['divisions'] = tempDivisions
setEvent(temp)
setStop(true)
// setLoading(false);
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
<Redirect to="/page-not-found" />
}
})
})
}
}, [stop, eventId]);
I am curious if this is the properly way to extract nested data from Cloud Firestore.
Data model:
Collection(Events) -> Doc(A) -> Collection(Divisions) -> Docs(B, C, D, ...)
Pretty much I'm looking to get metadata from Doc(A), then get all the sub-collections which contain Docs(B, C, D, ...)
Current Problem: I am able to get meta data for Doc(A) and its subcollections(Divisions), but the front-end on renders metadata of Doc(A). Front-End doesn't RE-RENDER the sub-collections even though. However, react devtools show that subcollections(Divisions) are available in the state.
EDIT 2:
const [entries, setEntries] = useState([])
useEffect(() => {
let active = true
let temp = []
if (active) {
divisions.forEach((division) => {
let teams = []
let tempDivision = division
db.collection(`${division.ref}/teams`).get().then((docs) => {
docs.forEach((doc, index) => {
teams.push(doc.data())
})
tempDivision['teams'] = teams
})
setEntries(oldArray => [...oldArray, temp])
})
}
return () => {
active = false;
};
}, [divisions]);
is there any reason why this is not detecting new array and trigger a new state and render? From what I can see here, it should be updating and re-render.
Your inner query doc.ref.collection('divisions').get() doesn't do anything to force the current component to re-render. Simply pushing elements into an array isn't going to tell the component that it needs to render what's in that array.
You're going to have to use a state hook to tell the component to render again with new data, similar to what you're already doing with setEvent() and setStop().

Parsing firebase query data in reactjs

I am using firebase cloud firestore for storing data. And I am developing a web app using reactjs. I have obtained documents using the following function:
getPeoples() {
let persons = [];
firestore.collection("students")
.get()
.then(function (querySnapshot) {
querySnapshot.forEach((doc) => {
var person = {};
person.name = doc.data().Name;
person.course = doc.data().Course;
persons.push(person);
})
});
console.log(persons);
return persons;
}
I am getting the desired data, but when I am iterating through persons array, it says it has length of 0.
here is the console output when I am displaying complete persons array and its length.
The length should be 14, but it shows 0. Please correct me what is wrong with me?
I want to display the output in the html inside the render() method of react component.
The output of
const peoples = this.getPeoples();
console.log(peoples);
It is:
The complete render method looks like:
render() {
const peoples = this.getPeoples();
console.log(peoples);
return (
<div className="peopleContainer">
<h2>Post-Graduate Students</h2>
{/* <h4>{displayPerson}</h4> */}
</div>
);
}
This is due to the fact the query to the database is asynchronous and you are returning the persons array before this asynchronous task is finished (i.e. before the promise returned by the get() method resolves).
You should return the persons array within the then(), as follows:
getPeoples() {
let persons = [];
return firestore.collection("students")
.get()
.then(function (querySnapshot) {
querySnapshot.forEach((doc) => {
var person = {};
person.name = doc.data().Name;
person.course = doc.data().Course;
persons.push(person);
})
console.log(persons);
return persons;
});
}
And you need to call it as follows, because it will return a promise :
getPeoples().then(result => {
console.log(result);
});
Have a look at what is written to the console if you do:
console.log(getPeoples());
getPeoples().then(result => {
console.log(result);
});
I'm not sure but please try to update your
getPeoples() {
let persons = [];
firestore.collection("students")
.get()
.then(function (querySnapshot) {
querySnapshot.forEach((doc) => {
var person = {};
person.name = doc.data().Name;
person.course = doc.data().Course;
persons.push(person);
})
});
console.log(persons);
return persons;
}
to
getPeoples() {
let persons = [];
firestore.collection("students")
.get()
.then(querySnapshot => {
querySnapshot.forEach((doc) => {
persons.push({name = doc.data().Name,
course = doc.data().Course
})
});
console.log(persons);
return persons;
}
Update
Sorry I thought you have problem with filling persons array in your function. Anyway as Renaud mentioned the query in your function is asynchronous so the result is not quick enough to be displayed on render. I use similar function and I found redux the best way to handle this situations.

ReactJS+FireStore Data mapping issue

Im writing a small program to fetch the categories from the Firestore DB and show in webpage as a list.
My code look like this:
class Category extends Component {
constructor() {
super();
this.state = {'Categories': []}
}
render() {
let categoryList = null;
if (Array.isArray(this.state.Categories)) {
console.log(this.state.Categories);
categoryList = this.state.Categories.map((category) => {
return <li>{category.name}</li>
});
}
return(
<ul>{categoryList}</ul>
);
}
componentWillMount() {
// fetch the data from the Google FireStore for the Category Collection
var CategoryCollection = fire.collection('Category');
let categories = [];
CategoryCollection.get().then((snapshot)=> {
snapshot.forEach ((doc) => {
categories.push(doc.data());
});
}).catch((error) => {
console.log("Error in getting the data")
});
this.setState({'Categories': categories});
}
}
Im able to fetch the data and even populate this.state.Categories, however the map function is not getting executed.
The console.log statement produce an array of values butthe map function in render is not getting executed. Any thoughts?
Console.log output:
You have an error in handling data retrieval. In the last line categories is still empty, so it triggers setState with an empty data set. Should be something lie that
componentWillMount() {
fire.collection('Category').get()
.then(snapshot => {
const categories = snapshot.map(doc => doc.data());
// sorry, but js object should be pascal cased almost always
this.setState({ categories });
})
.catch(error => {
console.log("Error in getting the data")
});
}
Only return the data if the data exists. The simplest way to do this is to replace <ul>{categoryList}</ul> with <ul>{this.state.categories && categoryList}</ul>
I could make it work with a small change (moved this.setState to be inside the callback). Honestly, I still don't understand the difference.
P.S: I come from PHP development and this is my first step into ReactJS.
componentWillMount() {
// fetch the data from the Google FireStore for the Category Collection
var categoryCollection = fire.collection('Category');
let categories = [];
categoryCollection.get().then((snapshot)=> {
snapshot.forEach ((doc) => {
categories.push(doc.data());
});
if (categories.length > 0) {
this.setState({'Categories': categories});
}
}).catch((error) => {
console.log("Error in getting the data");
});
// this.setState({'Categories': categories});
}

how to get id of object from firebase database in reactjs

how to get id of object from firebase database in reactjs
I have an array of list got from firebase database in react-redux , I want to get id of every object of array, How can I get?
Get the snapshot, and iterate through it as a Map, with Object.keys(foo).forEach for example.
Here is a dummy piece of code :
`
const rootRef = firebase.database().ref();
const fooRef = rootRef.child("foo");
fooRef.on("value", snap => {
const foo = snap.val();
if (foo !== null) {
Object.keys(foo).forEach(key => {
// The ID is the key
console.log(key);
// The Object is foo[key]
console.log(foo[key]);
});
}
});
`
Be careful with Arrays in Firebase : they are Maps translated into Arrays if the IDs are consecutive numbers started from '0'. If you remove an item in the middle of your array, it will not change the ID accordingly. Better work with Maps, it's more predictable.
You could try something like this:
export const getAllRooms = () => {
return roomCollection.get().then(function (querySnapshot) {
const rooms = [];
querySnapshot.forEach(function (doc) {
const room = doc.data();
room.id = doc.id;
rooms.push(room);
});
return rooms;
});
};
`

Resources