I am building an Angular2 app and one of the components needs to make multiple API calls which are dependent on the previous ones.
I currently have a service which makes an API call to get a list of TV shows. For each show, I then need to call a different API multiple times to step through the structure to determine if the show exists on a Plex server.
The API documentation is here
For each show, I need to make the following calls and get the correct data to determine if it exists: (Assume we have variables <TVShow>, <Season>, <Episode>)
http://baseURL/library/sections/?X-Plex-Token=xyz will tell me:
title="TV Shows" key="2"
http://baseURL/library/sections/2/all?X-Plex-Token=xyz&title=<TVShow> will tell me: key="/library/metadata/2622/children"
http://baseURL/library/metadata/2622/children?X-Plex-Token=xyz will tell me: index="<Season>" key="/library/metadata/14365/children"
http://baseURL/library/metadata/14365/children?X-Plex-Token=xyz will tell me: index="<Episode>" which implies that the episode I have exists.
The responses are in json, I have removed a lot of the excess text. At each stage I need to check that the right fields exist (<TVShow>, <Season>, <Episode>) so that they can be used for the next call. If not, I need to return that the show does not exist. If it does, I will probably want to return an id for the show.
I have looked at lots of examples including promise, async & flatmap, but am not sure how to solve this based on the other examples I have seen.
How to chain Http calls in Angular2
Angular 2.0 And Http
Angular 2 - What to do when an Http request depends on result of another Http request
Angular 2 chained Http Get Requests with Iterable Array
nodejs async: multiple dependant HTTP API calls
How to gather the result of Web APIs on nodeJS with 'request' and 'async'
Here is what I have for getting the list of shows. (shows.service.ts)
export class ShowsHttpService {
getShows(): Observable<Show[]> {
let shows$ = this._http
.get(this._showHistoryUrl)
.map(mapShows)
.catch(this.handleError);
return shows$;
}
}
function mapShows(response:Response): Show[] {
return response.json().data.map(toShow);
}
function toShow(r:any): Show {
let show = <Show>({
episode: r.episode,
show_name: r.show_name,
season: r.season,
available : false, // I need to fill in this variable if the show is available when querying the Plex API mentioned above.
});
// My best guess is here would be the right spot to call the Plex API as we are dealing with a single show at a time at this point, but I cannot see how.
return show;
}
Here is the relevant code from the component (shows.component.ts)
public getShows():any {
this._ShowsHttpService
.getShows()
.subscribe(w => this.shows = w);
console.log(this.shows);
}
Bonus points
Here are the obvious next questions that are interesting, but not necessary:
The first API query will be much faster than waiting for all of the other queries to take place (4 queries * ~10 shows). Can the initial list be returned and then updated with the available status when it is ready.
The first Plex call to get the key="2" only needs to be performed once. It could be hard coded, but instead, can it be performmed once and remembered?
Is there a way to reduce the number of API calls? I can see that I could remove the show filter, and search through the results on the client, but this doesn't seam ideal either.
The 4 calls for each show must be done sequentially, but each show can be queried in parallel for speed. Is this achievable?
Any thoughts would be much appreciated!
Not sure if I totally understand your question, but here is what I do:
I make the first http call, then when the subscribe fires, it calls completeLogin. I could then fire another http call with its own complete function and repeat the chain.
Here is the component code. The user has filled in the login information and pressed login:
onSubmit() {
console.log(' in on submit');
this.localUser.email = this.loginForm.controls["email"].value;
this.localUser.password = this.loginForm.controls["password"].value;
this.loginMessage = "";
this.checkUserValidation();
}
checkUserValidation() {
this.loginService.getLoggedIn()
.subscribe(loggedIn => {
console.log("in logged in user validation")
if(loggedIn.error != null || loggedIn.error != undefined || loggedIn.error != "") {
this.loginMessage = loggedIn.error;
}
});
this.loginService.validateUser(this.localUser);
}
This calls the loginservice ValidateUser method
validateUser(localUser: LocalUser) {
this.errorMessage = "";
this.email.email = localUser.email;
var parm = "validate~~~" + localUser.email + "/"
var creds = JSON.stringify(this.email);
var headers = new Headers();
headers.append("content-type", this.constants.jsonContentType);
console.log("making call to validate");
this.http.post(this.constants.taskLocalUrl + parm, { headers: headers })
.map((response: Response) => {
console.log("json = " + response.json());
var res = response.json();
var result = <AdminResponseObject>response.json();
console.log(" result: " + result);
return result;
})
.subscribe(
aro => {
this.aro = aro
},
error => {
console.log("in error");
var errorObject = JSON.parse(error._body);
this.errorMessage = errorObject.error_description;
console.log(this.errorMessage);
},
() => this.completeValidateUser(localUser));
console.log("done with post");
}
completeValidateUser(localUser: LocalUser) {
if (this.aro != undefined) {
if (this.aro.errorMessage != null && this.aro.errorMessage != "") {
console.log("aro err " + this.aro.errorMessage);
this.setLoggedIn({ email: localUser.email, password: localUser.password, error: this.aro.errorMessage });
} else {
console.log("log in user");
this.loginUser(localUser);
}
} else {
this.router.navigate(['/verify']);
}
}
In my login service I make a call to the authorization service which returns an observable of token.
loginUser(localUser: LocalUser) {
this.auth.loginUser(localUser)
.subscribe(
token => {
console.log('token = ' + token)
this.token = token
},
error => {
var errorObject = JSON.parse(error._body);
this.errorMessage = errorObject.error_description;
console.log(this.errorMessage);
this.setLoggedIn({ email: "", password: "", error: this.errorMessage });
},
() => this.completeLogin(localUser));
}
In the authorization service:
loginUser(localUser: LocalUser): Observable<Token> {
var email = localUser.email;
var password = localUser.password;
var headers = new Headers();
headers.append("content-type", this.constants.formEncodedContentType);
var creds:string = this.constants.grantString + email + this.constants.passwordString + password;
return this.http.post(this.constants.tokenLocalUrl, creds, { headers: headers })
.map(res => res.json())
}
The point here in this code, is to first call the validateUser method of the login service, upon response, based on the return information, if its valid, I call the loginUser method on the login service. This chain could continue as long as you need it to. You can set class level variables to hold the information that you need in each method of the chain to make decisions on what to do next.
Notice also that you can subscribe to the return in the service and process it there, it doesn't have to return to the component.
Okay, Here goes:
public getShows():any {
this._ShowsHttpService
.getShows()
.subscribe(
w => this.shows = w,
error => this.errorMessage = error,
() => this.completeGetShows());
}
completeGetShow() {
//any logic here to deal with previous get;
this.http.get#2()
.subscribe(
w => this.??? = w),
error => this.error = error,
() => this.completeGet#2);
}
completeGet#2() {
//any logic here to deal with previous get;
this.http.get#3()
.subscribe(
w => this.??? = w),
error => this.error = error,
() => this.completeGet#3);
}
completeGet#3() {
//any logic here to deal with previous get;
//another http: call like above to infinity....
}
Related
In a Vue component controlling users subsciption to newsletters, I have the fellowing code:
async newSubscriber(event) {
// Validate email
//---------------
if (!this.isEmailValid(this.subscriber_email))
this.subscribeResult = "Email not valid";
else {
// If valid, check if email is not already recorded
//-------------------------------------------------
let alreadyRecorded = false;
let recordedEmails = await this.$apollo.query({ query: gql`query { newslettersEmails { email } }` });
console.log('length ' + recordedEmails.data.newslettersEmails.length);
console.log(recordedEmails.data.newslettersEmails);
for (let i = 0; !alreadyRecorded && i < recordedEmails.data.newslettersEmails.length; i++)
alreadyRecorded = this.subscriber_email === recordedEmails.data.newslettersEmails[i].email;
if (alreadyRecorded)
this.subscribeResult = "Email already recorded";
else {
// If not, record it and warn the user
//------------------------------------
this.$apollo.mutate({
mutation: gql`mutation ($subscriber_email: String!){
createNewslettersEmail(input: { data: { email: $subscriber_email } }) {
newslettersEmail {
email
}
}
}`,
variables: {
subscriber_email: this.subscriber_email,
}
})
.then((data) => { this.subscribeResult = "Email recorded"; })
.catch((error) => { this.subscribeResult = "Error recording the email: " + error.graphQLErrors[0].message; });
}
}
}
At the very first email subscription test, $apollo.query returns me the correct number of emails already recorded (let's say, 10) and record the new subscriber email. But if I try to record a second email without hard refreshing (F5) the browser, $apollo.query returns me the exact same result than the first time (10), EVEN IF the first test email has been correctly recorded by strapi (graphql palyground showns me the added email with the very same query!). Even if I add ten emails, apollo will always return me what it got during its first call (10 recorded emails), as if it uses a buffered result. Of course, that allows Vue to record several times the same email, which I obviously want to avoid!
Does it speaks to anyone ?
After a lot of Google digging (giving the desired results by simply changing in my requests, at the end, "buffering" by "caching" !), I understood that Apollo cache its queries by default (at least, in the configuration of the Vue project I received). To solve the problem I just added "fetchPolicy: 'network-only'" to the query I make:
let recordedEmails = await this.$apollo.query({
query: gql`query { newslettersEmails { email } }`,
});
became
let recordedEmails = await this.$apollo.query({
query: gql`query { newslettersEmails { email } }`,
fetchPolicy: 'network-only'
});
And problem solved ^^
I have a custom validation in VeeValidate for EU Vat Numbers. It connects to our API, which routes it to the VIES webservice. This webservice is very unstable though, and a lot of errors occur, which results in a 500 response. Right now, I return false when an error has occured, but I was wondering if there was a way to warn the user that something went wrong instead of saying the value is invalid?
Validator.extend('vat', {
getMessage: field => 'The ' + field + ' is invalid.',
validate: async (value) => {
let countryCode = value.substr(0, 2)
let number = value.substr(2, value.length - 2)
try {
const {status, data} = await axios.post('/api/euvat', {countryCode: countryCode, vatNumber: number})
return status === 200 ? data.success : false
} catch (e) {
return false
}
},
}, {immediate: false})
EDIT: Changed code with try-catch.
You can use:
try {
your logic
}
catch(error) {
warn user if API brokes (and maybe inform them to try again)
}
finally {
this is optional (you can for example turn of your loader here)
}
In your case try catch finally block would go into validate method
OK, first of all I don't think that informing user about broken API in a form validation error message is a good idea :-| (I'd use snackbar or something like that ;) )
any way, maybe this will help you out:
I imagine you are extending your form validation in created hook so maybe getting message conditionaly to variable would work. Try this:
created() {
+ let errorOccured = false;
Validator.extend('vat', {
- getMessage: field => 'The ' + field + ' is invalid.',
+ getMessage: field => errorOccured ? `Trouble with API` : `The ${field} is invalid.`,
validate: async (value) => {
let countryCode = value.substr(0, 2)
let number = value.substr(2, value.length - 2)
const {status, data} = await axios.post('/api/euvat', {countryCode: countryCode, vatNumber: number})
+ errorOccured = status !== 200;
return status === 200 ? data.success : false;
},
}, {immediate: false})
}
After searching a lot, I found the best approach to do this. You just have to return an object instead of a boolean with these values:
{
valid: false,
data: { message: 'Some error occured.' }
}
It will override the default message. If you want to return an object with the default message, you can just set the data value to undefined.
Here is a veeValidate v3 version for this:
import { extend } from 'vee-validate';
extend('vat', async function(value) {
const {status, data} = await axios.post('/api/validate-vat', {vat: value})
if (status === 200 && data.valid) {
return true;
}
return 'The {_field_} field must be a valid vat number';
});
This assumes your API Endpoint is returning json: { valid: true } or { valid: false }
Suppose we have an observable main object array, and observable data about that array (e.g. suppose we have selectedReports and reportParameters) . Now suppose we emit action to either add report to the array or remove report from that array. How do we run an action to fetch the data for reportParameters, as reaction?
Thus far, my attempt, which isn't working, looks like this:
// report parameters stuff
async fetchAllReportParameters() {
reaction(
() => this.selectedReports,
async (reports) => {
// reset the report parameters
this.reportParameters = {}
// fetch the parameters for all the reports
await reports
.forEach((report) => {
this.fetchReportParameters(report.Id)
})
}
)
}
/**
* fetches report parameters for a reportId
* #param {number} reportId
*/
fetchReportParameters = (reportId) => {
this.reportParameters[reportId] = []
const onSuccess = (reportParameters) => {
this.reportParameters[reportId] = reportParameters
}
this.api.GetReportParameters(reportId)
.then(onSuccess, this.fetchReportParametersError)
}
fetchReportParametersError = (error) => {
// TODO: output some error here
}
Are you ever actually calling fetchAllReportParameters? If you don't, the reaction will never be created. You may instead like to create the reaction from the constructor, assuming you always want it to be run. One example:
class SomeStore {
constructor() {
this.disposeReportsReaction = reaction(
() => this.selectedReports.slice(),
reports => {
// ...
}
)
}
}
Call storeInstanceName.disposeReaction() whenever you're done with the reaction.
Notice that I've used .slice() here. This is because if you simply pass the array reference, the reaction will never be called. See reaction docs: you have to actually use the value in some way.
You also need to tweak the async code a bit. This:
async (reports) => {
await reports.forEach((report) => {
// ...
})
}
won't do what you hope, because forEach returns undefined. Even if you shift the async keyword to the forEach callback, all the API requests will be sent in quick succession. Consider using something like this instead, depending on whether you want to wait for the preceding request before sending the next one:
try {
for (const report of reports) {
await this.fetchReportParameters(report.id)
}
} catch (e) {
// handle error
}
This isn't always the right answer: sometimes it's fine to send a bunch of requests in quick succession (perhaps especially if it's a small batch, and/or in the context of HTTP/2). If that's ok with you, you could use:
reports => {
// ...
reports.forEach(report => this.fetchReportParameters(report.id))
}
I need advise for handling errors in front-end of web application.
When I call a service to get the community according to community in web app, I want it to catch an error. For example for catching errors like 404.
There is a service for getting community according to id provided.
getCommunity(id: number) {
return this.http.get(`${this.api}/communities/` + id + ``);
}
that is called in events.ts file
setCommunityBaseUrl() {
this.listingService.getCommunity(environment.communityId).subscribe((data: any) => {
this.communityUrl = data.url + `/` + data.domain;
});
}
The id is provided in environment. Let's say there are 20 communities in total. When I provide id = 1 the events according to community = 1 appears.
export const environment = {
production: ..,
version: 'v2',
apiUrl: '...',
organization: '...',
websiteTitle: '...',
communityId: 1,
googleMapsApiKey: '...'
};
The problem is that when I provide id = null all community events are occurring | all events list in the backend is occurring.
Please, help ^^
When you subscribe you subscribe with an Observer pattern. So the first function you pass in
.subscribe(() => {} );
fires when the Observable calls .next(...)
and after that you can provide another function which will fire whenever the Observable calls .error(...)
so
.subscribe(() => {}, (error) => { handleTheError(error); } );
The this.http.get(...); returns an Observable which will fire the .error(...) on http error
We also know that this.http.get(...) completes or "errors" and it's not an endless one (a one that never completes). So you can make it a promise and manipulate on it promise like.
async getMeSomething(...) {
try {
this.mydata = await this.http.get(...).toPromise();
}
catch(error) {
handleTheError(error)
}
}
But what I really recommend is to use Swagger for your backend and then generate the API Client class with NSwagStudio so you don't have to write the client manually or adjust it or deal with error catching. I use it all the time and it saves us an enormous amount of time
Because you are using ".subscribe" you can create your own error handler and catch the errors like this, directly on the method.
This is an example on how you can use this:
constructor(
private _suiteAPIService: SuitesAPIService,
private _testcaseService: TestcaseService,
public _tfsApiService: TfsApiService,
private _notificationService: NotificationService) { }
errorHandler(error: HttpErrorResponse) {
return observableThrowError(error.message || "Server Error")
}
public something = "something";
GetTestcasesFromSuiteSubscriber(Project, BuildNumber, SuiteId) {
this._suiteAPIService.GetTestResults(Project, BuildNumber, SuiteId).subscribe(
data => {
console.log(data);
this._testcaseService.ListOfTestcases = data;
//Notofication service to get data.
this._notificationService.TestcasesLoaded();
},
error => {
//Here we write som error
return this.something;
}
);
}
I am trying to do a put request to an api from array that I have. The post wants an object, and I have an array of objects. What I do is a loop itereting the length of my array of objects calling the method into my service. The problem is that just works the first one and the rest are not working. Do I should something like return promise and then call it recursively?
Here I let my method to call the api:
onUpdate() {
for (var i = 0; i < this.conditionsToUpdate.length; i++) {
this.ruleService.updateConditionsFromRule(this.rule.id, this.conditionsToUpdate[i])
.then(_ => {
this.notificationService.addToast('Condition Updated!', '', 2)
})
.catch(err => this.notificationService.handleError("Could not update the
condition!"))
}
}
Finally, on my Service I have my request:
updateConditionsFromRule(idRule: number, condition: ConditionUpdate):Promise<any> {
return this.http.post(`${this.organizationId}/rules/${idRule}/conditions`, condition)
.toPromise()
.then(res => {
const response = <{ id: String, error: IError[] }>res.json();
if (!!response && !!response.error) {
return Promise.reject(response.error)
} else {
return Promise.resolve(response)
}
}).catch(err => Promise.reject(err));
}
And as I said, it just returns me the first post we do, the rest are not being created.
Thank you a lot!
You can use Observable for this, promises will be too limited.
given your array updateConditionsFromRule, this is how to implement such a thing:
let requests:Observable<Response>[] = [];
updateConditionsFromRule.forEach( updateCondition => {
requests.push(this.http.post(`${this.organizationId}/rules/${idRule}/conditions`, condition));
});
// After our loop, requests is an array of Observables, not triggered at the moment.
//Now we use combineLatest to convert our Observable<Response>[] to a Observable<Response[]>.
//This means that the promise will resolve once the last request of the array has finished.
Observable.combineLatest(requests).toPromise()
.then(res => {
const response = <{ id: String, error: IError[] }>res.json();
if (!!response && !!response.error) {
return Promise.reject(response.error)
} else {
return Promise.resolve(response)
}
}).catch(err => Promise.reject(err));
}