How to querying and aggregate multiple collection in Firebase - javascript

I need to retrieve a monthly work orders collection by project and list of users (in the month I could have some project's OdL hours not included in the users list and the users could have some OdL of other projects).
The OdL model contains the strings for project (projectCode) and user (userId).
I send to the service the dates in Timestamp format, the code of the project and an array with users list.
this.loadGantt(from: Timestamp, to: Timestamp, projectCode: string, users: string[]){...}
I tried to execute single queries, one for project code and one for single user.
These are the queries of the service:
this.docs = this.afs.collection<OdlModel>('odl', ref => {
return ref.where('projectCode', '==', project)
.where('date', '>=', from)
.where('date', '<=', to)
.orderBy('date', 'asc');
})
.snapshotChanges().pipe(map(coll => {
return coll.map(doc => ({ id: doc.payload.doc.id, ...doc.payload.doc.data()}));
}));
return this.docs;
this.docs = this.afs.collection<OdlModel>('odl', ref => {
return ref.where('userId', '==', user)
.where('date', '>=', from)
.where('date', '<=', to)
.orderBy('date', 'asc');
})
.snapshotChanges().pipe(map(coll => {
return coll.map(doc => ({ id: doc.payload.doc.id, ...doc.payload.doc.data()}));
}));
return this.docs;
This is the solution that I found:
loadGantt() {
const momentDate: Moment = moment(new Date());
this.firstDay = momentDate.startOf('month').toDate();
this.lastDay = momentDate.endOf('month').toDate();
const from = firebase.firestore.Timestamp.fromDate(this.firstDay);
const to = firebase.firestore.Timestamp.fromDate(this.lastDay);
this.odl = [];
const odlArray = [];
const a$ = this.odlService.getProjectOdlCollection(from, to, 'IGNC0954-IMI-19');
odlArray.push(a$);
this.user.forEach(u => {
const b$ = this.odlService.getUserOdlCollection(from, to, u);
odlArray.push(b$);
});
const result$ = combineLatest([odlArray]);
result$.subscribe(res => {
res.map(r => {
r.subscribe(odl => {
odl.forEach(o => {
const index = this.odl.findIndex(x => x.id === o.id);
if (index === -1) {
this.odl.push(o);
}
});
});
});
});
}
How to I get all data in a single Observable object?

Below is a snippet which shows how to combine two or more Firestore collections from AngularFire into a single Observable object.
Link here
Data Structure
const usersRef = this.afs.collection('users');
const fooPosts = usersRef.doc('userFoo').collection('posts').valueChanges();
const barPosts = usersRef.doc('userBar').collection('posts').valueChanges();
Solution
import { combineLatest } from 'rxjs/observable/combineLatest';
const combinedList = combineLatest<any[]>(fooPosts, barPosts).pipe(
map(arr => arr.reduce((acc, cur) => acc.concat(cur) ) ),
)
Also this example may also help with your issue.
Let me know if this was helpful.

Related

How to access variable inside a function in JavaScript?

I'm almost finishing my first project with React Native which is an expenditure app.
I'm building a chart from all my expenses in 2 currencies, the code is working I just have a small problem with accessing a local variable.
I would like to access the 2 sum variable inside this function, so I can use it in my return for React Native to build a chart.
// Get $ currency From fire base
const q = query(collection(db, "users"),where("selected", "==", "Gasto"), where("moneda", "==", "$"));
const unsubscribe = onSnapshot(q,(querySnapshot) => {
const dolarCurrency = [];
const months =[];
querySnapshot.forEach((doc) =>{
dolarCurrency.push(doc.data().cantidad);
months.push(doc.data().fecha)
})
const hash = months.map((Day) => ({ Day }));
const hashDolar = dolarCurrency.map(( Amount ) => ({ Amount }))
const output = hash.map(({Day},i) => ({Day, ...hashDolar[i]}));
/// I need this variable below
const sum = output.reduce((acc, cur)=> {
const found = acc.find(val => val.Day === cur.Day)
if(found){
found.Amount+=Number(cur.Amount)
}
else{
acc.push({...cur, Amount: Number(cur.Amount)})
}
return acc
}, [])
})
// Get C$ currency from Firebase
const q2 = query(collection(db, "users"), where("selected", "==", "Gasto"),where("moneda", "==", "C$"));
const unsubscribe2 = onSnapshot(q2, (querySnapshot) =>{
const localCurrency = [];
const months2 = [];
querySnapshot.forEach((doc) => {
localCurrency.push(doc.data().cantidad)
months2.push(doc.data().fecha)
})
const hash = months2.map((Day) => ({ Day }));
const hashLocalCurrency = localCurrency.map(( Amount ) => ({ Amount }))
const output = hash.map(({Day},i) => ({Day, ...hashLocalCurrency[i]}));
/// I need this variable below
const sum = output.reduce((acc, cur)=> {
const found = acc.find(val => val.Day === cur.Day)
if(found){
found.Amount+=Number(cur.Amount)
}
else{
acc.push({...cur, Amount: Number(cur.Amount)})
}
return acc
}, [])
})
Any help or advice is welcome and appreciated!
You can either try state or ref, depends on whether UI part is to be updated, if UI is to be updated then state else ref does the job
const [sum,setSum] = useState(0)
or
const sumRef = useRef(0)
Then after that
const sum = output.reduce((acc, cur)=> {
const found = acc.find(val => val.Day === cur.Day)
if(found){
found.Amount+=Number(cur.Amount)
}
else{
acc.push({...cur, Amount: Number(cur.Amount)})
}
return acc
}, [])
})
You can either setSum(sum) or sumRef.current = sum
Hope it helps , feel free for doubts

It is not possible to make a loop through the array

There is a request to Firebase, using then I add new elements to the array. If you output console.log, then there are elements, but lenght = 0 and loops do not work.
export const useLastMessageDialogs = (messagesStatus: StatusDialogType): UserMessageType[] => {
const messages: string[] = [];
useEffect(() => {
const querySideDialogs = query(
collection(firestoreDb, 'questions'),
where('status', '==', messagesStatus)
);
onSnapshot(querySideDialogs, (dialogs) => {
dialogs.docChanges().forEach((dialog) => {
getDocs(
query(
collection(firestoreDb, 'questions', dialog.doc.id, 'messages'),
orderBy('timestamp', 'desc'),
limit(1)
)
).then((response) => {
response.forEach((message) => {
messages.push('bebebe');
});
});
});
});
}, [messagesStatus]);
console.log(messages); // [0: "bebebe", 1: "bebebe" length: 2]
console.log(messages.length); // 0
return [];
};
I took it to a separate service and connected it via redux-saga

How to set specific query parameters for docs in a sub collection in firestroe 9?

How to set a specific quarry for a sub collection when using a snapshot ?
I am tring to sort sub collection resutls by createdAt with the limit of last 5
useEffect(() => {
//const querydata = query(docRefComm, orderBy('createdAt'), limit(5));
let collectionRef = collection(db, 'Comm', docId, 'messages');
const unsub = onSnapshot(collectionRef , (doc) => {
doc.forEach((el) => {
console.log(el.data());
});
});
return () => {
unsub();
};
}
}, []);
You can build a query() using CollectionReference as shown below:
let collectionRef = collection(db, 'Comm', docId, 'messages');
const q = query(collectionRef, orderBy('createdAt'), limit(5))
const unsub = onSnapshot(q , (qSnap) => {
})
Checkout Listen to multiple documents in a collection in the documentation.

Array.map() doesn't render anything in React

I'm trying to make a list in my react app. I have retrieved data from my database, and pushed it into a list. I have doublechecked that the data shows up correctly in the console, and it does, but array.map() returns nothing. I think the problem might be that array.map() runs two times. I don't know why it runs two times.
function Dashboard() {
const user = firebase.auth().currentUser;
const [teams, setTeams] = useState([])
const history = useHistory();
useEffect(() => {
getTeams()
if (user) {
} else {
history.push("/")
}
}, [])
function Welcome() {
if (user) {
return <h1>Welcome, {user.displayName}</h1>
} else {
}
}
const getTeams = () => {
firebase.firestore().collectionGroup('members').where('user', '==', user.uid).get().then((snapshot) => {
const docList = []
snapshot.forEach((doc) => {
docList.push({
teamId: doc.data().teamId,
})
})
const teamslist = []
docList.forEach((data) => {
firebase.firestore().collection('teams').doc(data.teamId).get().then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
})
})
})
setTeams(teamslist)
})
}
const openTeam = (data) => {
console.log(data.teamId)
}
return (
<div>
<Welcome />
<div>
<ul>
{console.log(teams)}
{teams.map((data) => {
return (
<li onClick={() => openTeam(data)} key={data.teamId}>
<h1>{data.name}</h1>
<p>{data.teamId}</p>
</li>
)
})}
</ul>
</div>
</div>
)
}
export default Dashboard
The getTeams function has a bug where it isn't waiting for the firebase.firestore().collection('teams').doc(data.teamId).get().then promises to finish before calling setTeams, so it is called with an empty array, causing React to trigger a render with the empty array.
As the promises for fetching each team resolve they will be pushed to the same array reference, but this won't trigger a rerender in React since you're not calling setTeams again when the array changes.
Try this code, which won't call setTeams until each team promise generated from docList has been resolved.
const getTeams = () => {
firebase.firestore().collectionGroup('members').where('user', '==', user.uid).get().then((snapshot) => {
const docList = []
snapshot.forEach((doc) => {
docList.push({
teamId: doc.data().teamId,
})
})
const teamslist = [];
Promise.all(docList.map((data) => {
return firebase
.firestore()
.collection('teams')
.doc(data.teamId)
.get()
.then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
})
})
}))
.then(() => setTeams(teamslist));
})
}
A smaller edit would be to call setTeams after each separate team promise resolves, which will trigger a React render each time a new team is resolved:
.then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
});
// create a new array, since using the same array
// reference won't cause react to rerender
setTeams([...teamslist]);
})
Many thanks to #martinstark who provided you an answer while I was unavailable.
However, there are some more things that need to be covered.
User State
In your current component, you pull the current user from Firebase Authentication, but don't handle the state changes of that user - signing in, signing out, switching user. If a user is signed in and they were to navigate directly to your dashboard, firebase.auth().currentUser could be momentarily null while it resolves the user's login state, which would incorrectly send them off to your login page.
This can be added using:
const [user, setUser] = useState(() => firebase.auth().currentUser || undefined);
const userLoading = user === undefined;
useEffect(() => firebase.auth().onAuthStateChanged(setUser), []);
Next, in your first useEffect call, you call getTeams() whether the user is signed in or not - but it should depend on the current user.
useEffect(() => {
if (userLoading) {
return; // do nothing (yet)
} else if (user === null) {
history.push("/");
return;
}
getTeams()
.catch(setError);
}, [user]);
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const docList = []
membersQuerySnapshot.forEach((doc) => {
docList.push({
teamId: doc.get("teamId"), // better perfomance than `doc.data().teamId`
});
});
const teamDataList = [];
await Promise.all(docList.map((data) => {
return firebase.firestore()
.collection('teams')
.doc(data.teamId)
.get()
.then(doc => teamDataList.push({
name: doc.get("name"),
teamId: doc.id
}));
}));
setTeams(teamDataList);
}
Optimizing getTeams() - Network Calls
The getTeams function in your question calls setTeams with the array [], which will be empty at the time of calling it as covered in #martinstark's answer. The "get team data" operations are asyncronous and you aren't waiting for them to resolve before updating your state and triggering a new render. While you are pushing data to them after the component has rendered, modifying the array won't trigger a new render.
While you could fetch the data for each team using db.collection("teams").doc(teamId).get(), each of these is requests is a network call, and you can only make a limited number of these in parallel. So instead of fetching 1 team per network call, you could fetch up to 10 teams per network call instead using the in operator and FieldPath.documentId().
Assuming the collectionGroup("members") targets the collections of documents at /teams/{aTeamId}/members which contain (at least):
"/teams/{aTeamId}/members/{memberUserId}": {
teamId: aTeamId,
user: memberUserId, // if storing an ID here, call it "uid" or "userId" instead
/* ... */
}
// this utility function lives outside of your component near the top/bottom of the file
function chunkArr(arr, n) {
if (n <= 0) throw new Error("n must be greater than 0");
return Array
.from({length: Math.ceil(arr.length/n)})
.map((_, i) => arr.slice(n*i, n*(i+1)))
}
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const chunkedTeamIDList = chunkArr(teamIDList, 10) // split into batches of 10
const teamsColRef = firebase.firestore().collection('teams');
const documentId = firebase.firestore.FieldPath.documentId(); // used with where() to target the document's ID
const foundTeamDocList = await Promise
.all(chunkedTeamIDList.map((chunkOfTeamIDs) => {
// fetch each batch of IDs
return teamsColRef
.where(documentId, 'in', chunkOfTeamIDs)
.get();
}))
.then((arrayOfQuerySnapshots) => {
// flatten results into a single array
const allDocsList = [];
arrayOfQuerySnapshots.forEach(qs => allDocsList.push(...qs.docs));
return allDocsList;
});
const teamDataList = foundTeamDocList
.map((doc) => ({ name: doc.get("name"), teamId: doc.id }));
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// update state & trigger render
setTeams(teamDataList);
}
You can also make use of this utility function to simplify & optimize the code a bit. Which gives:
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const teamsColRef = firebase.firestore().collection('teams');
const teamDataList = [];
await fetchDocumentsWithId(
teamsColRef,
teamIDList,
(doc) => teamDataList.push({ name: doc.get("name"), teamId: doc.id })
);
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// update state & trigger render
setTeams(teamDataList);
}
Optimizing getTeams() - Function Definition
As part of the last optimization, you could pull it out of your component or place it in its own file so that it's not redefined with every render:
// define at top/bottom of the file outside your component
// This getTeams() is a (userId: string) => Promise<{ name: string, teamId: string}[]>
async function getTeams(userId) => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', userId)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const teamsColRef = firebase.firestore().collection('teams');
const teamDataList = [];
await fetchDocumentsWithId(
teamsColRef,
teamIDList,
(doc) => teamDataList.push({ name: doc.get("name"), teamId: doc.id })
);
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// return the sorted teams
return teamDataList
}
and update how you use it:
useEffect(() => {
if (userLoading) {
return; // do nothing
} else if (user === null) {
history.push("/");
return;
}
getTeams(user.uid)
.then(setTeams)
.catch(setError);
}, [user]);

firestore < query mixed with orderBy Query

I am using firestore to query documents with compound queries, my code is:
let query = firestore.collection( 'market' )
let res = []
let newChkPt = false
// states
query = query.where('deListTime', '==', false)
query = query.where('tradeTime' , '==', false)
query = query.where('expirationTime', '>', Date.now())
// FIFO ordering
query = query.orderBy('originationTime', 'desc')
query = query.limit(3)
if (chkPt) {
await query
.startAfter(chkPt)
.get()
.then(snap => {
snap.forEach(doc => {
res.push(doc.data());
newChkPt = doc.data()['originationTime']
})
})
.catch(e => { console.log(e); return false})
} else {
await query
.get()
.then(snap => {
snap.forEach(doc => {
res.push(doc.data());
newChkPt = doc.data()['originationTime']
})
})
.catch(e => { console.log(e); return false})
}
In the console I have every combination composite query indices possible specified amongst the fields deListTime, tradeTime, expirationTime, and originationTime. And yet this compound query I specified refuse to fetch data as intended. If I comment out
query = query.orderBy('originationTime', 'desc')
I get the data, and if I comment the '>' out whilst leaving everything else un-commmented:
query = query.where('expirationTime', '>', now)
I also get the desired data. Is it the > that's messing it up?
The indexes:
Are you initializing 'now' somewhere? Did you mean Date.now()?

Categories

Resources