Flatten/Chain multiple nested firebase Observables - firebase

I'm trying to use AngularFire2. I am querying and everything works fine below.
I want to combine all/most of the observables into one:
getTournamentWithRounds(key):Observable<Tournament> {
return this.af.database
.object(`/tournaments/${key}`)
.map(tourney => {
let t = Tournament.fromJson(tourney);
this.af.database.list('players', {
query: {
orderByChild: 'tournament_key',
equalTo: key
}
})
.map(Player.fromJsonList)
.subscribe(ps => { t.players = ps; });
this.af.database.list('rounds', {
query: {
orderByChild: 'tournament_key',
equalTo: key
}
})
.map(Round.fromJsonList)
.subscribe(rs => { t.rounds= rs; })
return t;
})
}
I was wondering if I could join all the observables and get the output with a single subscribe function.
I would like to know when all the initial data has been loaded and perform additional computation in the controller before outputting it to the view.
Also, how could this be extended to include the matches for each round?
My extension to the above code would be:
...
this.af.database.list('rounds', {
query: {
orderByChild: 'tournament_key',
equalTo: key
}
})
.map(rounds => {
return rounds.map((round) => {
let r = Round.fromJson(round);
this.af.database.list('matches', {
query: {
orderByChild: 'round_key',
equalTo: round.$key
}
})
.map(Match.fromJsonList)
.subscribe(matches => { r.matches = matches; })
return r;
})
})
.subscribe(rs => { t.rounds= rs; })
...

You could use the combineLatest operator to combine the players and rounds with the tournament:
getTournamentWithRounds(key): Observable<Tournament> {
return this.af.database
.object(`/tournaments/${key}`)
.combineLatest(
this.af.database.list('players', {
query: {
orderByChild:'tournament_key',
equalTo: key
}
}),
this.af.database.list('rounds', {
query: {
orderByChild:'tournament_key',
equalTo: key
}
})
)
.map(([tourney, players, rounds]) => {
let t = Tournament.fromJson(tourney);
t.players = Player.fromJsonList(players);
t.rounds = Round.fromJsonList(rounds);
return t;
});
}
Whenever any of the observables emits, the latest values will be re-combined and a new Tournament will be emitted.
Extending this to include each round's matches is a little more complicated, as each round's key is needed for the matches query.
The emitted rounds can be mapped to an array of list observables for the matches and forkJoin can be used to join the observables, with the forkJoin selector function being used to combine the matches with the rounds. switchMap is then used to emit the rounds.
getTournamentWithRounds(key): Observable<Tournament> {
return this.af.database
.object(`/tournaments/${key}`)
.combineLatest(
this.af.database.list('players', {
query: {
orderByChild:'tournament_key',
equalTo: key
}
}),
this.af.database.list('rounds', {
query: {
orderByChild:'tournament_key',
equalTo: key
}
})
.switchMap(rounds => {
Observable.forkJoin(
rounds.map(round => this.af.database.list('matches', {
query: {
orderByChild: 'round_key',
equalTo: round.$key
}
}).first()),
(...lists) => rounds.map((round, index) => {
let r = Round.fromJson(round);
r.matches = Match.fromJsonList(lists[index]);
return r;
})
)
})
)
.map(([tourney, players, rounds]) => {
let t = Tournament.fromJson(tourney);
t.players = Player.fromJsonList(players);
t.rounds = rounds;
return t;
});
}

Related

How do I combine multiple http requests and merge results

I need to handle a situation where I have 3 endpoints to call and would like to get the data in the most convenient/efficient way. The first call can be handled independently and returns a single result. The second endpoint returns a collection but will need to initiate 0-* subsequent calls, where a given key is present.
Ideally would like to receive the collection (from the 2nd endpoint call) as a mutated/new collection that includes the result from the 3rd endpoint call.
I am currently using forkJoin(observableA$, observableB$) to handle the first 2 calls in parallel but I cannot work out how to include the sequential calls and have the data included in observableB$
//Customer observable
const customer$ = this._customerManagementService.getCustomer(
accountNumber
);
return forkJoin({
customer: customer$,
saleCycles: saleCyclesWithVehicle$
}).pipe(finalize(() => this._loaderFactoryService.hide()));
getSalesWithVehicle(accountNumber: string, dealerKey: string) {
return this._salesCycleService
.getCyclesForCustomer({
customerNumber: accountNumber,
dealerKey: dealerKey
})
.pipe(
concatMap((results: ISaleCycle[]) => {
return results.map(cycle => {
return this._purchaseVehicleService.getPurchaseVehicle(
cycle.vehicleKey
);
});
})
);
}
I expect the collection to include further data as a new property on the original collection
UPDATE
After a bit more thought maybe I should be using reduce somewhere in the solution. This way I can be in control of what's getting push into the array and it could be dynamic?
getSalesWithVehicle(accountNumber: string, dealerKey: string) {
return this._salesCycleService
.getCyclesForCustomer({
customerNumber: accountNumber,
dealerKey: dealerKey
})
.pipe(
switchMap((results: ISaleCycle[]) => {
return results.map(cycle => {
if (cycle.vehicleKey) {
return this._purchaseVehicleService
.getPurchaseVehicle(cycle.vehicleKey)
.pipe(
reduce((acc, vehicle) => {
return { cycle: cycle, vehicle: vehicle };
}, []),
toArray()
);
}
else {
///No extra data to be had
}
});
}),
concatAll()
);
}
I would use concatMap() to merge the responses of HTTP requests 2 and 3.
import { of } from 'rxjs';
import { map, concatMap } from 'rxjs/operators';
const pretendGetCustomer = of({accountNumber: 123, name:"John Doe"});
const pretendGetVehiculeHttpRequest = (customerNumber) => {
return of([{custNum: 123, vehicleId:"2"}, {custNum: 123, vehicleId:"1"}]);
}
const pretendGetCyclesHttpRequest = (cycleIds) => {
return of([{id:"1", name:"yellow bike", retailPrice:"$10"}, {id:"2", name:"red bike", retailPrice:"$20"}]);
}
const yourFunction = () => {
pretendGetCustomer.subscribe(customer => {
// Assuming you do other things here with cust, reason why we are subscribing to this separately
// isHappy(customer)
// Your second & third calls
pretendGetVehiculeHttpRequest(customer.accountNumber).pipe(
// Need to use concatMap() to subscribe to new stream
// Note: use mergeMap() if you don't need the 1st stream to be completed
// before calling the rest
concatMap(purchases => {
const cyclesIds = purchases.map(p => p.vehicleId);
// concatMap() requires an Observable in return
return pretendGetCyclesHttpRequest(cyclesIds).pipe(
// Use map() here because we just need to use the data,
// don't need to subscribe to another stream
map(cycles=>{
// Retrun whatever object you need in your subscription
return {
customerNumber: customer.accountNumber,
customerName: customer.name,
purchases: purchases.map(p => cycles.find(c => p.vehicleId === c.id))
}
})
);
})
).subscribe(resultof2and3 => {
// Do something with the new/mutated Object which is a result of
// your HTTP calls #2 and #3
console.log(resultof2and3);
});
});
}
yourFunction();
I made a stackblitz if you want to see the above run (see console): https://stackblitz.com/edit/rxjs-nqi7f1
This is the solution I eventually came up with. I've taken the advice from BoDeX and used concatMap(). In my mind it was clear that I wanted to use forkJoin and be able to reference the results by object key, I.e customer or saleCycles.
In the scenario where a vehicleKey was present I needed to return the results in a defined data structure, using map(). Likewise, if no vehicle was found then I just needed the outer observable.
const customer$ = this._customerManagementService.getCustomer(accountNumber);
const saleCyclesWithVehicle$ = this.getSalesWithVehicle(accountNumber,dealerKey);
getSalesWithVehicle(accountNumber: string, dealerKey: string) {
return this._salesCycleService
.getCyclesForCustomer({
customerNumber: accountNumber,
dealerKey: dealerKey
})
.pipe(
concatMap(cycles => {
return from(cycles).pipe(
concatMap((cycle: ISaleCycle) => {
if (cycle.vehicleKey) {
return this._purchaseVehicleService
.getPurchaseVehicle(cycle.vehicleKey)
.pipe(
map(vehicle => {
return { cycle: cycle, vehicle: vehicle };
})
);
} else {
return of({ cycle: cycle });
}
}),
toArray()
);
})
);
}
return forkJoin({
customer: customer$,
saleCycles: saleCyclesWithVehicle$
}).pipe(finalize(() => this._loaderFactoryService.hide()));

Populate the cloud firestore document containing an array of document references

I have a collection named campgrounds in which every document contains an array of document reference to the documents in the comments collections.
It looks like this Campground
I'm trying to figure out a way to populate this comments array before sending it to my ejs template.
My code looks like this
app.get("/campgrounds/:docId", function(req, res) {
var docRef = firestore.collection("campgrounds").doc(req.params.docId);
try {
docRef.get().then(doc => {
if (!doc.exists) {
res.send("no such document");
} else {
// res.send(doc.data());
res.render("campground", {
doc: doc.data(),
title: doc.data().title,
id: req.params.docId
});
}
});
} catch (error) {
res.send(error);
}
});
In your array you store DocumentReferences. If you want to get the data of the corresponding documents in order to include this data in your object you should use Promise.all() to execute the variable number (1 or more) of get() asynchronous operations.
The following should work (not tested at all however):
app.get("/campgrounds/:docId", function(req, res) {
var docRef = firestore.collection("campgrounds").doc(req.params.docId);
try {
var campground = {};
docRef.get()
.then(doc => {
if (!doc.exists) {
res.send("no such document");
} else {
campground = {
doc: doc.data(),
title: doc.data().title,
id: req.params.docId
};
var promises = [];
doc.data().comments.forEach((element, index) => {
promises.push(firestore.doc(element).get());
});
return Promise.all(promises);
}
})
.then(results => {
var comments = {};
results.forEach((element, index) => {
comments[index] = element.data().title //Let's imagine a comment has a title property
});
campground.comments = comments;
res.render("campground", campground);
})
} catch (error) {
res.send(error);
}
});
Note that with this code you are doing 1 + N queries (N being the length of the comments array). You could denormalize your data and directly store in the campground doc the data of the comments: you would then need only one query.

Firestore query with multiple where clauses based on parameters

I want to query a Firestore database with multiple where clauses based on the parameters that are passed in. The following block of code works:
getProducts2(accountId: string, manufacturer?: string, materialType?: string): Promise<Product[]> {
return new Promise<Product[]>((resolve, reject) => {
const productCollection2: AngularFirestoreCollection<FreightRule> = this.afs.collection('products');
const query = productCollection2.ref
.where('materialType', '==', materialType)
.where('manufacturer', '==', manufacturer);
query.get().then(querySnapshot => {
if (querySnapshot.size > 0) {
const data = querySnapshot.docs.map(documentSnapshot => {
return documentSnapshot.data();
}) as Product[];
resolve(data);
} //todo else...
});
});
}
But what I really want to do is conditionally include the where clauses based on the optional parameters. The following is what I want, but it doesn't work:
getProducts2(accountId: string, manufacturer?: string, materialType?: string): Promise<Product[]> {
return new Promise<Product[]>((resolve, reject) => {
const productCollection2: AngularFirestoreCollection<FreightRule> = this.afs.collection('products');
const query = productCollection2.ref;
if (manufacturer) {
query.where('manufacturer', '==', manufacturer);
}
if (materialType) {
query.where('materialType', '==', materialType);
}
query.get().then(querySnapshot => {
if (querySnapshot.size > 0) {
const data = querySnapshot.docs.map(documentSnapshot => {
return documentSnapshot.data();
}) as Product[];
resolve(data);
} //todo else...
});
});
}
While valid, this code just returns all of the products with no filtering.
Is there a way to structure this so I can filter based on the optional parameters?
edit: I realize I can do something like:
let query;
if (manufacturer && materialType) {
query = productCollection2.ref.where(....).where(....)
} else if (manufacturer) {
query = productCollection2.ref.where(....)
} else if (materialType) {
query = productCollection2.ref.where(....)
}
I was just hoping for something a little more elegant.
Build upon the prior query, don't repeat the prior query:
let query = collection // initial query, no filters
if (condition1) {
// add a filter for condition1
query = query.where(...)
}
if (condition2) {
// add a filter for condition2
query = query.where(...)
}
// etc
If using different query structure, You can try below ways:
db.collection("subscriptions").where("email", '==', req.body.email,"&&","subscription_type","==","free").get();
OR
db.collection("subscriptions").where("email", '==', req.body.email).where("subscription_type", '==', 'free111').get();

How do i convert a single json record retrieved from firebase into an array

I am trying to take a single record from firebase to use in vuejs but I cant find out how to convert it to an array, if thats even what i should be doing.
my mutation
GET_CASE(state, caseId) {
state.caseId = caseId;
},
My action
getCase ({ commit, context }, data) {
return axios.get('http' + data + '.json')
.then(res => {
const convertcase = []
convertcase.push({ data: res.data })
//result below of what is returned from the res.data
console.log(convertcase)
// commit('GET_CASE', convertcase)
})
.catch(e => context.error(e));
},
I now get the following returned to {{ myCase }}
[ { "data": { case_name: "Broken laptop", case_status: "live", case_summary: "This is some summary content", contact: "", createdBy: "Paul", createdDate: "2018-06-21T15:20:22.932Z", assessor: "Gould", updates: "" } } ]
when all i want to display is Broken Laptop
Thanks
Example let obj = {a: 1, b: 'a'); let arr = Object.values(obj) // arr = [1, 'a']
async getCase ({ commit, context }, url) {
try {
let { data } = await axios.get(`http${url}.json`)
commit('myMutation', Object.values(data))
} catch (error) {
context.error(error)
}
}
But as I'm reading your post again, I think you don't want array from object. You want array with one object. So, maybe this is what you want:
async getCase ({ commit, context }, url) {
try {
let { data } = await axios.get(`http${url}.json`)
commit('myMutation', [data])
} catch (error) {
context.error(error)
}
}
Put this inside / after your .then
Object.keys(data).forEach(function(k, i) {
console.log(k, i);
});
With a response from Axios, you can get your data as:
res.data.case_name
res.data.case_number
....
Just build JavaScript object holding these properties and pass this object to your mutation. I think it is better than using an array.
const obj = {};
Object.assign(obj, res.data);
commit('GET_CASE', obj)
And in your mutation you do as follows:
mutations: {
GET_CASE (state, payload) {
for (var k in payload) {
if (payload.hasOwnProperty(k)) {
state[k] = payload[k]
}
}
}
}
Alternatively you can code your store as follows:
state: {
case: {},
...
},
getters: {
getCase: state => {
return state.case
},
....
},
mutations: {
GET_CASE (state, payload) {
state.case = payload
}
}
and you call the value of a case field form a component as follows:
const case = this.$store.getters.getCase
..... = case.case_name

Normalizr and server-generated IDs

I am using normalizr to organize my redux-store state.
Let's say that I have normalized todo-list:
{
result: [1, 2],
entities: {
todo: {
1: {
id: 1,
title: 'Do something'
},
2: {
id: 2,
title: 'Second todo'
}
}
}
}
Then I would like to implement addTodo action. I need to have an id in todo object, so I generate a random one:
function todoReducer(state, action) {
if(action.type == ADD_TODO) {
const todoId = generateUUID();
return {
result: [...state.result, todoId],
enitities: {
todos: {
...state.entities.todos,
[todoId]: action.todo
}
}
}
}
//...other handlers...
return state;
}
But the problem is that eventually all data will be saved to server and generated id should be replaced with real server-assigned id. Now I merge them like this:
//somewhere in reducer...
if(action.type === REPLACE_TODO) {
// copy todos map, add new entity, remove old
const todos = {
...state.entities.todos
[action.todo.id]: action.todo
};
delete todos[action.oldId];
// update results array as well
const result = state.result.filter(id => id !== oldId).concat(action.todo.id);
// return new state
return {entities: {todos}, result};
}
It seems to be a working solution, but there also a lot of overhead. Do you know any way to simplify this and don't make REPLACE_TODO operation?

Resources