How to enrich result list from FirebaseListObservable with FirebaseObjectObservables - firebase

I have a FirebaseListObservable with IDs and I want to look them up using FirebaseObjectObservable and create a Array of the result, but I don't really understand how to do it.
this.af.database.list('/favourites/'+ this.userId)
.flatMap(favouriteId => {
return this.af.database.object('/dhs/en/'+favouriteId.$value)
})
.subscribe(favourite => {
console.log("favourite", favourite);
});
This doesn't work because favouriteId.$value is undefined. What is the right way to do this?
This is a related question: How to merge an observable that is a property of another observable
EDIT: I have created an ugly temporary solution
getAllFavourites(): Observable<any> {
return Observable.create(observer => {
var resArray = [];
this.af.database.list('/favourites/'+ this.userId)
.subscribe(favouriteArray => {
favouriteArray.forEach((favouriteId) => {
this.secretProvider.getDay(favouriteId.$value)`
.subscribe(favourite => {
resArray.push(favourite);
observer.next(resArray);
});
})
})
})
}
Obviously this is not the purpose of observables and in addition the resulting observable array is not updated correctly when for example a favourite ID is removed from this.af.database.list('/favourites/'+ this.userId).
UPDATE: I have found a better solution following this thread: How to move FirebaseObjectObservable into a FirebaseListObservable
getAllFavourites(): Observable<any> {
return this.af.database.list('/favourites/'+ this.userId)
.map((favourites)=>{
return favourites.map((favourite) => {
favourite = this.secretProvider.getDay(favourite.$value)
return favourite;
})
});
}
(and in the HTML using a nested async varibale as described in the other thread)
I still think there must be a better solution though. Something using mergeMap or similar, and avoiding the nested asyncs.

Related

How can I write use previous returned value of an observable with redux-observable?

I don't know my question title is suitable for this question.
I would like to use returned value of someAsync1 (same as v2) as argument of action1 inside of flatMap.
const anEpic = ($action: ActionsObservable<MyAction>, store: Store<MyRootStore>) => {
return $action.ofType(ActionTypes.AN_ASYNC_ACTION)
.switchMap((v1) => someAsync1(v1))
.switchMap((v2) => someAsync2(v2))
.map((v) => applyToUI(v))
.flatMap((v) => Observable.concat(Observable.of(action1(v)), Observable.of(action2(true)))) //
}
I guess I can use that value by injecting v2 to returned value of someAsync2. But that code looks disgusting.
What is clever way to do this with redux-observable?
switchMap technically means, well, switch to another Observable stream. That means there is no way for you to retain the value because your observer is now observing a different source.
There are a few ways to do the so called "retaining" the values from one stream to another, depending on which one you prefer.
1. Behaviour Subject
This is the most preferred way, because the purpose of BehaviourSubject is to retain a value of an Observable:
//initialize a BehaviourSubject
let v2BSubject = new BehaviourSubject<any>(null);
const anEpic = ($action: ActionsObservable<MyAction>, store: Store<MyRootStore>) => {
return $action.ofType(ActionTypes.AN_ASYNC_ACTION)
.switchMap((v1) => someAsync1(v1))
.switchMap((v2) => {
//store your v2 value here!
v2BSubject.next(v2);
return someAsync2(v2)
})
.map((v) => applyToUI(v))
.flatMap((v) => {
//get your v2 values here
let v2Value = v2BSubject.value;
return Observable.concat(Observable.of(action1(v)), Observable.of(action2(true)))
}) //
}
or you can use it as an Observable. That way you can treat it as an observable and use whatever rxjs operator can provide:
.flatMap((v) => {
return Observable.concat(Observable.of(action1(v)), v2BSubject.asObservable())
})
2. Use .map to propagate the value.
This is rather hacky, but gets the job done. However do note it's modifying the stream source. If you have many operations along the pipe, it may blow up quickly and hard to manage:
const anEpic = ($action: ActionsObservable<MyAction>, store: Store<MyRootStore>) => {
return $action.ofType(ActionTypes.AN_ASYNC_ACTION)
.switchMap((v1) => someAsync1(v1))
.switchMap((v2) => {
someAsync2(v2)
.map(afterSomeAsync2 => {
return {
v1Value: v2,
v2Value: afterSomeAsync2
}
})
})
.map(({v1Value, v2Value}) => {
return applyToUI(v1Value).map(v1 => {
return {
v1Value: v1,
v2Value: v2Value
}
})
})
.flatMap(({v1Value, v2Value}) => {
return Observable.concat(Observable.of(action1(v1Value)), Observable.of(v2Value))
})
The easiest solution is to apply your operators directly on the returned inner Observables instead of on the collapsed outer chain. You can then access the values emitted because they're part of the closures.
That's probably confusing, but hopefully this code makes it clear:
const anEpic = ($action: ActionsObservable<MyAction>, store: Store<MyRootStore>) => {
return $action.ofType(ActionTypes.AN_ASYNC_ACTION)
.switchMap((v1) =>
someAsync1(v1)
.switchMap((v2) =>
someAsync2(v2)
.map((v) => applyToUI(v))
.flatMap((v) => Observable.of(action1(v, v1, v2), action2(true))
)
)
}
This pattern is also what you would have to use if you wanted to catch any errors by someAsync1 or someAsync2 because if you let the error propagate to the top-level chain the epic will have stopped listening for future actions.
e.g. if your epic looks like this:
const somethingEpic = (action$, store) => {
return action$.ofType(SOMETHING)
.switchMap(action => someAsync1(v1))
.map(() => ({ type: SOMETHING_FULFILLED }))
.catch(error => Observable.of({
type: SOMETHING_REJECTED,
error
}));
}
When the error reaches the catch operator it's too late, your epic is no longer listening for future actions. You could "restart" it, but this can have unexpected consequences so its best to avoid this pattern.
Instead, catch the error before it propagates out
const somethingEpic = (action$, store) => {
return action$.ofType(SOMETHING)
.switchMap(action =>
someAsync1(v1)
.map(() => ({ type: SOMETHING_FULFILLED }))
.catch(error => Observable.of({
type: SOMETHING_REJECTED,
error
}))
);
}
Some people refer to this as "isolating your observer chains".
Also notice how I didn't need to use concat with multiple of because of supports any number of arguments.

Convert multiple FirebaseObjectObservable into a FirebaseListObservable

The snippet is part of a bigger code. Generally I have an object on firebase database called users (it's not a list). I need to get some of them and then convert into Array or FirebaseListObservable.
Observable.merge(...[
this.db.object('users/user1'),
this.db.object('users/user2'),
this.db.object('users/user3'),
this.db.object('users/user4'),
this.db.object('users/user5')
]).subscribe(user => {
console.log(user);
});
This return me user by user, however I need to get all users together. I need to do it in sync. Any ideas?
I have a similar problem and this is how I'm solving it for the moment:
getUsers(): Observable<any> {
let observables = [];
for (let user of users) {
observables.push(this.db.object(user))
}
return Observable.combineLatest(...observables, (...results) => { return results });
}
What I did not manage to do is to return it as FirebaseListObservable.

RxJs Compose a list of Observables and then combine (Firebase)

I'm attempting to combine a list of observables, but haven't had luck with zip or other maps.
What I want to do is get a list of Genres for a given Artist Id. I'm using firebase, so it's a nosql database. I have a 'genresPerArtist' list that is a list of Artist keys, and each Artist key has a list of Genre keys.
Structure:
{
genresPerArtist: {
artist1: {
genre1: true,
genre2: true,
genre3: true
}
},
genres: {
genre1: {
name: 'Alternative'
},
genre2: {
name: 'Jazz'
}
}
}
Here's the start of the function that I have:
getGenresByArtistId(artistId: string): Observable<any> {
return this.db.list(`/genresPerArtist/${artistId}`)
.flatMap((genres: IDictionary[]) => {
return genres.map(genre => this.db.list(`/genres/${genre.$key}`));
// return Observable.zip(obvs);
});
I'm not really sure what do do with the list that I get back from genresPerArtist. What I want to do, is take each genre.key (genre1, genre2, genre3), and use each one to fetch the value from 'genres'. I thought I could return a .zip, and then map over that, but I was not getting a response from zip.
#cartant, thanks, that included what I needed, forkJoin.
// Compose an observable based on the genresPerArtist:
return this.db.list(`/genresPerArtist/${artistId}`)
// Each time the genresPerArtist emits, switch to unsubscribe/ignore any pending user queries:
.switchMap(genres => {
// Map the genres to the array of observables that are to be joined.
let genreObservables = genres.map(genre => this.db.object(`/genres/${genre.$key}`).first());
return Observable.forkJoin(...genreObservables);
});
You could try with the following, asumming that the return of genresPerArtist returns a Map<string, boolean>:
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/mergeMap';
import { zip } from 'rxjs/observable/zip';
getGenresByArtistId(artistId: string): Observable<Genre[]>{
return this.db.list(`/genresPerArtist/${artistId}`)
.mergeMap((genres: Map<string, boolean>) =>
zip(Array.from(genres.keys()).map(genre => this.db.list(`/genres/${genre}`))))
}
EDIT: Another option would be
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/toArray';
getGenresByArtistId(artistId: string): Observable<Genre[]>{
return this.db.list(`/genresPerArtist/${artistId}`)
.mergeMap((genres: Map<string, boolean>) => Array.from(genres.keys())
.mergeMap(genres=> genres)
.mergeMap(genre => this.db.list(`/genres/${genre}`))
.toArray();
}
Basically Im flattening the results into a single array manually. I took the long way to make it easier to follow

Extract data from FirebaseListObservable to Array

sorry for my english, i have this observable:
this.productos = af.database.list('/productos', {
query: {
orderByChild: 'categoria',
equalTo: this.catnombre
}
});
I need extract all id from here and set in a array but i dont know how, thanks.
Edit:
I can extract the id but I use de key, now i need extract other data, but snapshot.val, dont work.
this.productos = af.database.list('/productos/', {
query: {
orderByChild: 'categoria',
equalTo: this.catnombre
}, preserveSnapshot:true
});
this.productos.subscribe(snapshot => {
snapshot.forEach(snapshot => {
console.log(snapshot.key);
this.idproductos.push(snapshot.key);
});
console.log(this.idproductos);
});
All you need to do is
this.productos = af.database.list('/productos/', {
query: {
orderByChild: 'categoria',
equalTo: this.catnombre
})
.map(products => products.map(product => product.$key));
The result will be an observable of arrays of keys. Now you can subscribe it to or do whatever else you want to.
this.productos.subscribe(keys => console.log("keys are", keys));
If AngularFire, and things like FirebaseListObservable, are used correctly, you don't need to worry about snapshots, or taking their val(), or doing forEach on them, or taking elements and putting them onto your own array. The FirebaseListObservable is an observable of arrays. Simply map it to create other observables, as we have done above to create an observable of arrays of keys, or subscribe to it to get the underlying data.

Tracker.autorun and subscriptions from array

I registered three Tracker.autorun functions with collection from array: ['tags', 'allUsers', 'userGroups']
formObj.collections.forEach(collection => {
Tracker.autorun(() => {
const handle = Meteor.subscribe(collection);
if (handle.ready()) {
dispatch(collectionIsReady(formObj, collection));
console.log(collection);
if (_.isEqual(formObj.collections, formObj.loadedCollections)) {
dispatch(collectionsAreReady(formObj));
dispatch(formIsReady(formObj));
}
}
});
});
If one of these collections changed the Tracker.autorun function is executed but only with the collection name of the last array item (console output: "userGroups"). What should i do to get the correct collection name?
thanks for your help
Edit:
There is no need to know the correct collection name.
Because of correctly executed autorun functions i additionally passed the computation (c) to collectionIsReady().
formObj.collections.forEach(collection => {
Tracker.autorun((c) => {
const handle = Meteor.subscribe(collection);
if (handle.ready()) {
dispatch(collectionIsReady(formObj, collection, c));
if (_.isEqual(formObj.collections, formObj.loadedCollections)) {
dispatch(collectionsAreReady(formObj));
dispatch(formIsReady(formObj));
}
}
});
});

Resources