I am building an app with Angular 2. I have a service, and I am trying to filter the data before getting it from the service. I want a function where I can just ask one project element instead of the whole array.
This is the code I tried, but this approach doesn't work:
getProject(id: number): Observable<Project> {
return this.http.get(this.url).map(this.extractData).filter(project => (<Project>project).id == id).catch(this.handleError2);
// return this.getProjects().filter(project => project.id === id);
//return this.http.get(this.url).toPromise().then(x => x.json().data.filter(project => project.id === id)[0]).catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
return body.data || { };
}
private handleError2 (error: any) {
// In a real world app, we might use a remote logging infrastructure
// We'd also dig deeper into the error to get a better message
let errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : 'Server error';
console.error(errMsg); // log to console instead
return Observable.throw(errMsg);
}
Its better to filter data in component from where you have called the service.
So your service is like :
getProject(id: number): Observable<Project> {
return this.http.get(this.url).map(this.extractData);
}
private extractData(res: Response) {
let body = res.json();
return body.data || []; // return value must be array to use filter function in component
}
in your component you can do as below :
service.getProject(projectId).subscribe((res) => {
let filtered = [];
if(res.length){
filtered = res.filter((item) => {
return item.id === projectId;
});
}
});
To me, it looks like you're mixing sync and async code, and return doesn't work like that. You can return an object (function) who's properties change later (aka a Promise)
I doubt that http.get() provides an array for you to map
.toPromise() seems like a hack, but you should return a Promise chain
return this.http.get(this.url).then(data => data.map(this.extractData).filter(project => (<Project>project).id == id).catch(this.handleError2));
If this.http.get() does not return a Promise, but takes a callback, you can construct one:
return new Promise (resolve => this.http.get(this.url, resolve)).then(data => data.map(this.extractData).filter(project => (<Project>project).id == id).catch(this.handleError2));
And whatever is calling getProject() can chain with getProject().then()
If this part of code works :
getProjects() {
return this.http.get(this.url).map(this.extractData).catch(this.handleError2);
}
Then you can call a single project with :
getProject(id: number) {
return this.getProjects()
.then(projects => projects.filter(project => (<Project>project).id == id);
}
Related
I have a method that should return me a boolean value, the problem is that this is asynchronus and I want to avoid race condition..
So I have some method for example:
someMethod(data: any): boolean{
let isOccupied = false;
firstValueFrom(this.checkIfOccupied('102')).then(toilet=> isOccupied = toilet.name === data.name);
this.someObject.isOccupied = isOccupied;
return isOccupied;
}
So before I proceed to the line with this.someObject... I want to wait for things that are happening inside of then( )
And the checkIfOccupied looks like this:
checkIfOccupied(toiletName: string): Observable<Toilet> {
return this.store$
.select(selectAlarmsForToilet(toiletName))
.pipe(
filter(res => !!res),
take(1),
switchMap((alarms: AlarmsObject[]) => {
alarms.forEach(alarm => {
if (Object.keys(alarm)[0].includes('occupied')) {
const toiletId = this.getToiletIdFromAlarm(toiletName, alarm); <= this method only finds needed ID
if (toiletId ) {
return this.toiletService.getToiletForId(toiletId ); <= this is API call
}
}
});
return of({} as SomeObject);
}));
}
I have tried to make it async and then use await in the someMethod but it doesn't work. Probably I made some mistake in the code (I dont want to make someMethod async - is it even possible?)
You are very close in your thinking.
Consider the words you stated, which I will paraphase:
"Before I proceed to the next line, I want to wait for something"
So change your code like so:
async someMethod(data: any): Promise<boolean>{
let isOccupied = false;
data = await firstValueFrom(this.checkIfOccupied('102'));
this.someObject.isOccupied = toilet.name === data.name;
return toilet.name === data.name;
}
You are using the Arrays.prototype.forEach inside your switchMap, which does not know anything about asynchronousity.
To keep up with race conditions, you can use the forkJoin Operator, which waits for Observables in arrays to complete such as:
someMethod(data: any): boolean {
let isOccupied = false;
firstValueFrom(this.checkIfOccupied('102')).then(toilet => isOccupied =
toilet.name === data.name);
this.someObject.isOccupied = isOccupied;
return isOccupied;
}
checkIfOccupied(toiletName: string): Observable<any> {
return this.store$
.select(selectAlarmsForToilet(toiletName))
.pipe(
filter(res => !!res),
take(1),
switchMap((alarms: AlarmsObject[]) => {
return alarms.length ? forkJoin(alarms
.filter(alarm => Object.keys(alarm)[0].includes('occupied'))
.map((alarm: any) => this.getToiletIdFromAlarm(toiletName, alarm))
.filter(toiletId => !!toiledId)
.map((toiletId: any) => {
return this.toiletService.getToiletForId(toiletId); // api call
})) : of([]);
}));
}
I want to implement a logic of retrying an API call to another origin if the call to current origin failed.
I want it to be handled in the service layer, for not to implement this logic in each component.
For example, I have such function in endpoint service
getAll(): Observable<IssueListItem[]> {
let endpointUrl = `${this.apiConnectionService.getApiUrl()}api/Issues`;
return this.http.get<IssueListItem[]>(endpointUrl, { headers: this.dataService.requestHeaders })
.pipe(retryOtherApi(this.apiConnectionService),
catchError(error => this.handleError(error)));
}
The consumer of this function looks like:
ngOnInit() {
this.issuesEndpointService.getAll()
.subscribe(_ => this.issues = _);
}
And I whant it know nothing about retry logic.
So, I tried to create an operator 'retryOtherApi' where I switch an origin to another one.
export function retryOtherApi(apiConnectionService: ApiConnectionService) {
const maxRetry = apiConnectionService.apiOriginsCount;
return (src: Observable<any>) => src.pipe(
retryWhen(_ => {
return interval().pipe(
flatMap(count => {
console.log('switch to: ' + apiConnectionService.getApiUrl())
apiConnectionService.switchToOther();
return count === maxRetry ? throwError("Giving up") : of(count);
})
);
})
);
}
Switching works, but unfortunately, the whole getAll function is not called and it retries n times with the same old URL.
So the question is how to implement common retry to other API logic if the current API become unavailable.
If to rephrase the question to the more common case, it becomes like how to recall HTTP endpoint with other params if the current one failed.
public getAll(): Observable<IssueListItem[]> {
let call = () => {
let endpointUrl = `${this.apiConnectionService.getApiUrl()}api/Issues`;
return this.http.get<IssueListItem[]>(endpointUrl, { headers: this.dataService.requestHeaders })
};
return <Observable<IssueListItem[]>>call()
.pipe(
retryOtherApi(this.apiConnectionService, () => call()),
catchError(error => this.handleError(error))
);
}
And here is a retry operator:
export function retryOtherApi(apiConnectionService: ApiConnectionService, recall: () => Observable<any>) {
const maxRetry = apiConnectionService.apiOriginsCount;
let count = 0;
let retryLogic = (src: Observable<any>) => src.pipe(
catchError(e => {
if (e.status === 0) {
apiConnectionService.switchToOther();
console.log('switch to: ' + apiConnectionService.getApiUrl())
return count++ === maxRetry ? throwError(e) : retryLogic(recall());
} else {
return throwError(e);
}
}));
return retryLogic;
}
It works, but:
Now I need to implement logic for other threads not to switch api
simultaneously.
Have some problems with TS typecasting, that is
why I hardcoded type before return.
I have a service in which it either stores an object so I can access it in other components or requests data from a web api and returns it in a map.
Example get for the objectService
get(): any {
if (this.object == undefined || this.object == null) {
return this.http.get(this.baseUrl + "/companies/get)
.map((data: any) => {
this.object = data;
return this.object;
});
} else {
return this.object;
}
}
Using the service to set page title
ngOnInit() {
this.objectService.get().subscribe((data: any) => {
this.title.setTitle(data.name);
});
}
So this works when I first load the page, but upon navigating to the page without a reload I get the following error ...subscribe is not a function. Which makes sense since I am returning an object and not an http request.
So I tried to return just the object and not a http request.
Example get for the objectService
get(): any {
if (this.object == undefined || this.object == null) {
return this.http.get(this.baseUrl + "/companies/get)
.map((data: any) => {
this.object = data;
});
}
return this.object;
}
Using the service to set page title
ngOnInit() {
this.title.setTitle(this.objectService.get().name);
}
This now flips the issue where page refresh gives me an error saying this.objectService.get().name is undefined and when I navigate to my component without a page refresh it works just fine.
Any help on trying to get this to work would be appreciated. Thanks in advance!
Actually you are mixing the observable and normal object!
I would say stick to the first aproach but while returning just the object, convert it to an observable so that it will work in your onNgInit
So your service will
get(): Observable<any> {
if (this.object == undefined || this.object == null) {
return this.http.get(this.baseUrl + "/companies/get)
.map((data: any) => {
this.object = data;
return this.object;
});
} else {
return Observable.of(this.object);
}
}
And your consumption will be still the normal and work for both the cases.
ngOnInit() {
this.objectService.get().subscribe(
(data: any) => { // this for response data
this.title.setTitle(data.name);
},
(error: any) => { // this for error
console.log('error: %o', error);
},
);
}
Im trying to add a helper method to my service that will call another method in the same service which returns an Observable. The helper method needs to return a Boolean. The problem is the logic required to determine whether the return value is true or false is done inside the subscribe block which is obviously async. So even through the value is true I still always get a false returned. Maybe trying to mix synchronous and asynchronous programming like this is not a good approach.
In my component I am trying to make the following call to the service:
this.isAdmin = this.teamsService.isAdmin(this.teamId, this.user['userId'])
Service:
getAssociations(id, postfix) {
return this.httpService.get(this.base, id, postfix)
.map((response) => {
return this.responseHandler.handleData(response);
})
.catch((error) => {
return this.responseHandler.handleError(error);
}
);
}
isAdmin(teamId, userId) {
let isAdmin = false;
this.getAssociations(teamId, '/teamMembers')
.subscribe(
_data => {
if (_data['teamMembers']) {
for (let i = 0; i < _data['teamMembers'].length; i++) {
if (_data['teamMembers'][i]['id'] === userId) {
isAdmin = true;
break;
}
}
}
}
);
return isAdmin;
}
So the isAdmin method always return false for obvious reasons. I initially was dealing with this logic inside the component by doing the following:
private checkIsAdmin() {
this.teamsService.getAssociations(this.teamId, '/teamMembers')
.subscribe(
_data => {
if (_data['teamMembers'] && _data['teamMembers'].length) {
_data['teamMembers'].map(member => {
if (member['id'] === this.user['userId']) {
this.isAdmin = true;
}
});
}
this.spinner.hide();
this.loading = false;
},
_error => {
this.spinner.hide();
}
);
}
This worked fine until I had to repeat this same logic on other components. So I am trying to come up with a way to abstract that logic into the service itself returning a boolean since that is really all I need from the function.
I have tried a few ideas but none of them worked. Any help would be great. Thanks.
You can also move all control logic up into the Rx stream like so:
isAdmin(teamId, userId) {
return this.getAssociations(teamId, '/teamMembers')
.mergeMap(data => data['teamMembers'] != null ? Rx.Observable.empty() : Rx.Observable.from(data['teamMembers']))
.filter(user => user['id'] === userId)
.mapTo(true) // found admin user
.take(1)
.concat(Rx.Observable.of(false));//default state; no admin user
}
Depending on the usage of getAssociations() you can also change its return type signature from Observable<ResponseTeamWrapperSomething> towards Observable<TeamMember> so you can lose the mergeMap operator in this function.
By moving this logic and replacing your custom control flow with platform supplied function you reduce the cognitive overload of what your code is doing. This also reduces the testing burden on you because you do not have to test framework supplied logic.
I changed the call in the component to:
this.teamsService.isAdmin(this.teamId, this.user['userId'])
.subscribe(response => {
this.isAdmin = response;
}
);
And updated the method inside the service to:
isAdmin(teamId, userId) {
return this.getAssociations(teamId, '/teamMembers')
.map(_data => {
let isAdmin = false;
if (_data['teamMembers']) {
for (let i = 0; i < _data['teamMembers'].length; i++) {
if (_data['teamMembers'][i]['id'] === userId) {
isAdmin = true;
break;
}
}
}
return isAdmin;
});
}
What am I misunderstanding about how to use the .subscribe function in combination with the .do function?
Here is my observable sequence:
lookupSubscriber = (text$: Observable<string>) =>
text$.debounceTime(300)
.distinctUntilChanged()
.do(() => this.searching = true)
.switchMap(term => {
var data = this._callApi(this.lookupSubscriberAPI, term)
.do(() => {
this.searchFailed = false;
this.searching = false;
})
.catch(() => {
this.searchFailed = true;
this.searching = false;
return Observable.of([]);
})
return data;
})
.do(() => this.searching = false);
If my _callApi function is as follows, it works:
_callApi(url: string, term: string) {
if (term === '') {
return of.call([]);
}
return map.call(this.dataService.get(url + term), response => {
var data = this._transformSubscriberInfo(response);
return data;
})
}
However, when I try to rewrite it with a subscribe function like this:
_callApi = (url: string, term: string) => {
return this.dataService.get(url + term)
.subscribe(
response => { this._transformSubscriberInfo(response) },
error => error.text(),
() => {
if (Logging.isEnabled.light) {
console.log('%c API Call Complete', Logging.normal.orange);
}
))
}
... then the data call succeeds, but I receive error: Property 'do' does not exist on type 'Subscription'.
Basically I am trying to catch errors and run an "always" function after the api call, as shown in the second version of _callApi.
The first version of _callApi seems to return an Observable while the second one returns a Subscription object. And Subscription does not expose do, exactly as the error message states.
What you may want to try is to use a version of do that accepts error and complete callbacks in addition to the next callback:
return this.dataService.get(url + term)
.map(response => this._transformSubscriberInfo(response))
.do(
response => { /* log response */ },
error => { /* log error */ },
() => { /* log completion */ }
);
It worth mentioning that do is not capable of transforming the source stream, the observable it returns contains the same values as the observable it is called on. That's why we need the line .map(response => this._transformSubscriberInfo(response)).
Also complete callback should not be confused for an "always" function: it gets called only when the source observable completes and it does not get called when the observable produces an error or gets unsubscribed.