Rxjs: add data to elements of array returned from http response - http

Following this question: Add data to http response using rxjs
I've tried to adapt this code to my use case where the result of the first http call yields an array instead of a value... but I can't get my head around it.
How do I write in rxjs (Typescript) the following pseudo code?
call my server
obtain an array of objects with the following properties: (external id, name)
for each object, call another server passing the external id
for each response from the external server, obtain another object and merge some of its properties into the object from my server with the same id
finally, subscribe and obtain an array of augmented objects with the following structure: (external id, name, augmented prop1, augmented prop2, ...)
So far the only thing I was able to do is:
this._appService
.getUserGames()
.subscribe(games => {
this._userGames = _.map(games, game => ({ id: game.id, externalGameId: game.externalGameId, name: game.name }));
_.forEach(this._userGames, game => {
this._externalService
.getExternalGameById(game.externalGameId)
.subscribe(externalThing => {
(<any>game).thumbnail = externalThing.thumbnail;
(<any>game).name = externalThing.name;
});
});
});
Thanks in advance

I found a way to make it work. I'll comment the code to better explain what it does, especially to myself :D
this._appService
.getUserGames() // Here we have an observable that emits only 1 value: an any[]
.mergeMap(games => _.map(games, game => this._externalService.augmentGame(game))) // Here we map the any[] to an Observable<any>[]. The external service takes the object and enriches it with more properties
.concatAll() // This will take n observables (the Observable<any>[]) and return an Observable<any> that emits n values
.toArray() // But I want a single emission of an any[], so I turn that n emissions to a single emission of an array
.subscribe(games => { ... }); // TA-DAAAAA!

Don't use subscribe. Use map instead.
Can't test it, but should look more like this:
this._appService
.getUserGames()
.map(games => {
this._userGames = _.map(games, game => ({ id: game.id, externalGameId: game.externalGameId, name: game.name }));
return this._userGames.map(game => { /* this should return an array of observables.. */
return this._externalService
.getExternalGameById(game.externalGameId)
.map(externalThing => {
(<any>game).thumbnail = externalThing.thumbnail;
(<any>game).name = externalThing.name;
return game;
});
});
})
.mergeAll()
.subscribe(xx => ...); // here you can subscribe..

Related

NGRX - How to subscribe to multiple requests that update a list, but prevent it from loading multiple times on the first load?

I'm working with Angular-NGRX and i have 3 components that can modify the data of a list. In the main component I listen for all the changes to update the listing and it works. The problem is that the first load subscribes me 3 times.
This is the ngOnInit of the main component:
ngOnInit(): void {
// Change order
this.store.select(state => state.shared.orderBy).pipe(
takeUntil(this.onDestroy$),
tap(resp => {
this.orderBy = resp;
this._cmsFunctions.deselectAll('Resource');
}),
switchMap(resp => this._dynamicComponentsService.loadResources(this.itemsToShow, this.tabCounter, this.orderBy, this.filters, this.searchTerm, this.company, this.workgroup, (this.paramCode) ? this.paramCode : 0))
).subscribe();
// Change filters
this.store.select(state => state.shared.filters).pipe(
takeUntil(this.onDestroy$),
tap(resp => {
this.filters = resp;
this._cmsFunctions.deselectAll('Resource');
}),
switchMap(resp => this._dynamicComponentsService.loadResources(this.itemsToShow, this.tabCounter, this.orderBy, this.filters, this.searchTerm, this.company, this.workgroup, (this.paramCode) ? this.paramCode : 0))
).subscribe();
// Change search term
this.store.select(state => state.shared.searchTerm).pipe(
takeUntil(this.onDestroy$),
tap(resp => {
this.searchTerm = resp;
this._cmsFunctions.deselectAll('Resource');
}),
switchMap(resp => this._dynamicComponentsService.loadResources(this.itemsToShow, this.tabCounter, this.orderBy, this.filters, this.searchTerm, this.company, this.workgroup, (this.paramCode) ? this.paramCode : 0))
).subscribe();
}
And all i need is that when starting it only subscribes once:
enter image description here
How can I improve this code?
Thanks!
You shouldn't be making calls to a data service from the component, and certainly not based on selector calls. You should be dispatching an Action which is captured and handled by an Effect. You can use dataLoading and dataLoaded flags in your store, for example, to prevent multiple loads.

Lookup items in firebase then lookup docId from the first returned items then merge

I need to lookup the location for each product.
First searchCollection returns array of object with one of the property anotherTblDocIs as a json string.
In this json string, which is firebase docId, I need to lookup the name. Then I need to merge it back.
How can I achieve this properly?
this.firebaseService
.searchCollection('product', 'shopId', this.shopId)
.pipe(
mergeMap((products: any) => {
let observableBatch = [];
products.map(product => {
const docIds = JSON.parse(product['anotherTblDocIs']);
docIds.forEach(doc =>{
observableBatch.push(this.firebaseService.readCollectionByDocId('doc', doc ).pipe(map(u => {return { doc, ...u}})))
});
});
return forkJoin(observableBatch);
})
)
.subscribe(products => {
});
When you write a stream it's often better to break things into small steps, and you keep transforming the stream until what you need.
Maybe this won't be exactly equivalent in terms of orders of subscriptions, but I think it would look better with something like:
this.firebaseService.searchCollection('product', 'shopId', this.shopId).pipe(
// Here we have Observable<Product[]>
mergeMap(products => products)
// Now we have Observable<Product>
mergeMap(product => JSON.parse(product['anotherTblDocIds']))
// Now we have Observable<DocIds> (for each product)
mergeMap(docId => this.firebaseService.readCollectionByDocId('doc', doc).pipe(
// We need to this map inside this stream to have the reference to `docId`
map(u => ({ doc: docId, ...u }))
))
)
mergeMap will flatten both Observables and arrays, so it can be used this way.

Firebase query with Vue js

i am new in Vue JS and in Firebase. My target is get all 'eventos' that has same category. I mean, let's i have two eventos, an eventos category "SMIX" and another has "DAM". Now i want to get the eventos has category 'SMIX'
My data structure is here :
created() {
var datos = []
firebase.database().ref('usuarios').on("value", data => {
data.forEach(function(user){
user.child("eventos").orderByChild("categoria").equalTo("SMIX")
.forEach(function(evento){
datos.push(evento.val())
});
});
this.eventos = datos;
});
}[My data Structure][1]
There are several errors and points to be noted in your code:
Firstly, if you receive the error user.child(...).orderByChild is not a function
it is because with data.forEach(function(user) {...}), user is a DataSnapshot (see the forEach() doc), and by calling the child() method on this DataSnapshot you get another DataSnapshot... which does not have a orderByChild() method.
The orderByChild() method is a method of a Reference, so you need to do
user.child(...).ref.orderByChild()
using the ref property of the DataSnapshot
Secondly, you cannot do
user.ref.child("eventos").orderByChild("categoria").equalTo("SMIX")
.forEach()
because you need to use the once() or on() methods to get the data at a database location represented by a Reference.
Thirdly, Since you are going to execute several queries within a loop, you need to use the once() method instead of the on() method. The on() method set a listener that continuously "listens for data changes at a particular location."
Finally, note that you need to use Promise.all() to manage the parallel asynchronous queries to the database.
So, having noted all the points above, the following code will do the trick (to put in created()):
var datos = []
firebase.database().ref('usuarios').once('value')
.then(dataSnapshot => {
var promises = [];
dataSnapshot.forEach(user => {
promises.push(user.ref.child("eventos").orderByChild("categoria").equalTo("SMIX").once('value'));
});
return Promise.all(promises);
})
.then(results => {
//console.log(results);
results.forEach(dataSnapshot => {
dataSnapshot.forEach(evento => {
datos.push(evento.val());
});
});
this.eventos = datos;
});

How do I return an entire paged set from the Jira API using Ramda?

I'm using the Nodejs library for talking to Jira called jira-connector. I can get all of the boards on my jira instance by calling
jira.board.getAllBoards({ type: "scrum"})
.then(boards => { ...not important stuff... }
the return set looks something like the following:
{
maxResults: 50,
startAt: 0,
isLast: false,
values:
[ { id: ... } ]
}
then while isLast === false I keep calling like so:
jira.board.getAllBoards({ type: "scrum", startAt: XXX })
until isLast is true. then I can organize all of my returns from promises and be done with it.
I'm trying to reason out how I can get all of the data on pages with Ramda, I have a feeling it's possible I just can't seem to sort out the how of it.
Any help? Is this possible using Ramda?
Here's my Rx attempt to make this better:
const pagedCalls = new Subject();
pagedCalls.subscribe(value => {
jira.board.getAllBoards({ type:"scrum", startAt: value })
.then(boards => {
console.log('calling: ' + value);
allBoards.push(boards.values);
if (boards.isLast) {
pagedCalls.complete()
} else {
pagedCalls.next(boards.startAt + 50);
}
});
})
pagedCalls.next(0);
Seems pretty terrible. Here's the simplest solution I have so far with a do/while loop:
let returnResult = [];
let result;
let startAt = -50;
do {
result = await jira.board.getAllBoards( { type: "scrum", startAt: startAt += 50 })
returnResult.push(result.values); // there's an array of results under the values prop.
} while (!result.isLast)
Many of the interactions with Jira use this model and I am trying to avoid writing this kind of loop every time I make a call.
I had to do something similar today, calling the Gitlab API repeatedly until I had retrieved the entire folder/file structure of the project. I did it with a recursive call inside a .then, and it seems to work all right. I have not tried to convert the code to handle your case.
Here's what I wrote, if it will help:
const getAll = (project, perPage = 10, page = 1, res = []) =>
fetch(`https://gitlab.com/api/v4/projects/${encodeURIComponent(project)}/repository/tree?recursive=true&per_page=${perPage}&page=${page}`)
.then(resp => resp.json())
.then(xs => xs.length < perPage
? res.concat(xs)
: getAll(project, perPage, page + 1, res.concat(xs))
)
getAll('gitlab-examples/nodejs')
.then(console.log)
.catch(console.warn)
The technique is pretty simple: Our function accepts whatever parameters are necessary to be able to fetch a particular page and an additional one to hold the results, defaulting it to an empty array. We make the asynchronous call to fetch the page, and in the then, we use the result to see if we need to make another call. If we do, we call the function again, passing in the other parameters needed, the incremented page number, and the merge of the current results and the ones just received. If we don't need to make another call, then we just return that merged list.
Here, the repository contains 21 files and folders. Calling for ten at a time, we make three fetches and when the third one is complete, we resolve our returned Promise with that list of 21 items.
This recursive method definitely feels more functional than your versions above. There is no assignment except for the parameter defaulting, and nothing is mutated along the way.
I think it should be relatively easy to adapt this to your needs.
Here is a way to get all the boards using rubico:
import { pipe, fork, switchCase, get } from 'rubico'
const getAllBoards = boards => pipe([
fork({
type: () => 'scrum',
startAt: get('startAt'),
}),
jira.board.getAllBoards,
switchCase([
get('isLast'),
response => boards.concat(response.values),
response => getAllBoards(boards.concat(response.values))({
startAt: response.startAt + response.values.length,
})
]),
])
getAllBoards([])({ startAt: 0 }) // => [...boards]
getAllBoards will recursively get more boards and append to boards until isLast is true, then it will return the aggregated boards.

Data validation using RxJS

I have the following function that validates that rangeFrom is not superior to rangeTo and that the rangeFrom does not already exist in the list of ranges.
How can rewrite this using RxJS?
const isTagAlreadyExist = (tags, currentTag) => _(tags)
.filter(x => x.id !== currentTag.id)
.some(x => _.inRange(currentTag.rangeTo, x.rangeFrom, x.rangeTo))
.value();
const validateRangeFrom = (tags, currentTag) => {
const errors = {};
if (isNumeric(currentTag.rangeFrom)) {
if (!_.inRange(currentTag.rangeFrom, 0, currentTag.rangeTo)) {
errors.rangeFrom = 'FROM_TAG_CANNOT_BE_GREATER_THAN_TO_TAG';
} else if (isTagAlreadyExist(tags, currentTag)) {
errors.rangeFrom ='TAG_ALREADY_EXISTS';
}
}
return {
errors
};
};
The question is: what parts do you want to rewrite to rxjs? Those are two pure functions that run synchronously from what I can see, I do not really see much a usecase for rxjs here - of course you could always utilize your functions within an rxjs stream:
const validateRangeFrom$ = (tags, currentTag) => {
return Observable.of(currentTag)
.map(tag => validateRangeFrom(tags, tag));
}
validateRangeFrom$(myTags, currentTag)
.subscribe(errors => console.log(errors));
But as you can see, this does not make much sense if you simply wrap it inside a stream, the essence of useful reactive programming is, that everything is reactive, not just some small parts, so for your example, you should start with having tags$ and currentTag$ as observables - let's assume that you have that, then you could do something like:
const tags$: Observable<ITag[]>... // is set somewhere, and emits a new array whenever it is changed
const currentTag$: Observable<ITag>... // is set somewhere and emits the tag whenever a new currentTag is set
const validateRangeFrom$ = Observable
.combineLatest(tags$, currentTag$, (tags, tag) => ({tags, tag}))
.map(({tags, tag}) => validateRangeFrom(tags, tag));
validateRangeFrom$.subscribe(errors => console.log(errors));
This will automatically trigger the validation for you whenever a new tags-array is emitted or a new currentTag is selected/set - but again: your validation-method is kept the same - as even in reactive programming you have to do validation and logic-operations at some point, the reactive part usually just concerns the flow of the data (see: tags$ and currentTag$)

Resources