I'm currently working with an API that's a bit of a pain :D The API doesn't return full information needed for my app and that means I have to make multiple calls to get the full information required. Also, I'm struggling to keep my head round it so if it's not well explained just let me know!
Main Details of Issue
The current API flow looks a little like this:
Get a list of 'Group ID's' (see response 1).
Using that list, for each Group ID, get the Group Details and Types in the Group (see response 2).
Using the group details, for each type, get the type name (see response 3).
Build a big tree with all the details.
Using a separate end point, get all 'skills' and update the tree accordingly (see response 4).
The problem comes when trying to return the correct values in the correct place which is all out of sync since I'm nesting promises in promises in async promises :O
The main API endpoints and examples can be found at https://esi.tech.ccp.is/latest/.
My current code looks a little like the below (I've tried to list functions in the order they are called).
The problem is, I need to locate the point where:
The list of groups has been returned.
For each group the types included have been returned.
the skillTree object has had a new property added which is under the format below.
Skill Tree Aim:
skillTree = {
"groupName": [
"skillID": {
"level": 0;
},
"skill2ID": {
"level": 0;
},...
],
"group2Name": [
"skillID" {
"level": 0;
},...
],...
};
tab-skills-all.ts (calls main function):
eveESI.buildSkillTree().then(() => {
// Need to add names to all skills in tree...
console.log('Completed skill tree:');
console.log(eveESI.skillTree);
}).catch((error) => {
// Do error handling...
});
eveESI Provider - buildSkillTree():
buildSkillTree(){
return new Promise((resolve, reject) => {
this.getSkillGroups().then((groups) => {
console.log('Success: Fetched groups successfully!');
console.log(groups);
// Process groups. First get group details including types. Then for each group push to main array.
for (var i in groups) {
if (groups.hasOwnProperty(i)) {
this.getSkillsInGroup(groups[i]).then((data) => {
var groupDetails = JSON.parse(data.toString());
var types = groupDetails.types;
var name = groupDetails.name;
console.log('Success: Fetched types for group ' + name + ' successfully!');
// Declare and build temp group object before we push it to main skill object...
var tempGroupObj = [];
// For each skill type in the group add to temporary array...
for (var n in types) {
if (types.hasOwnProperty(n)) {
tempGroupObj[types[n]] = {};
tempGroupObj[types[n]]['level'] = 0;
}
}
console.log(tempGroupObj);
this.skillTree[name] = tempGroupObj;
}).then(() => {
}).catch((error) => {
// Do error handling...
console.log(error);
});
}
}
resolve();
}).catch((error) => {
// Do error handling...
reject();
});
});
}
eveESI Provider - getSkillGroups() - returns [...] of group ID's see response 1:
getSkillGroups(){
return new Promise((resolve, reject) => {
this.http.get(this.apiRoot + 'universe/categories/16/', { }, { Authorization: 'Basic YWUxYmIzZDU4ZmRiNDk1ZDk3ZTE1ZTE0OTIyZDc0ZDk6MnpsVjNLZzVHbTh4OHY5b2lUSENYOHVXR21PYjlHd2Rqc3htQ0NHOA=='})
.then(reqResponse => {
// Returns {} of skill groups from category...
var responseJSON = JSON.parse(reqResponse.data);
resolve(responseJSON.groups);
}).catch(reqError => {
// Error. Return error message...
reject();
});
});
}
eveESI Provider - getSkillsInGroup(id) - returns {...} of group details see response 2:
getSkillsInGroup(id){
return new Promise((resolve, reject) => {
this.http.get(this.apiRoot + 'universe/groups/' + id + '/', { }, { Authorization: 'Basic YWUxYmIzZDU4ZmRiNDk1ZDk3ZTE1ZTE0OTIyZDc0ZDk6MnpsVjNLZzVHbTh4OHY5b2lUSENYOHVXR21PYjlHd2Rqc3htQ0NHOA=='})
.then(reqResponse => {
resolve(reqResponse.data);
}).catch(reqError => {
// Error. Return error message...
reject();
});
});
}
Response 1 (Lists Group ID's):
{
"category_id": 16,
"name": "Skill",
"published": true,
"groups": [
255,
256,
257,
258,
266,
268,
269,
270,
272,
273,
274,
275,
278,
505,
1209,
1210,
1213,
1216,
1217,
1218,
1220,
1240,
1241,
1545
]
}
Response 2 (returns group details and types in the group):
{
"group_id": 255,
"name": "Gunnery",
"published": true,
"category_id": 16,
"types": [
3300,
3301,
3302,
3303,
3304,
3305,
3306,
3307,
3308,
3309,
3310,
3311,
3312,
3315,
3316,
3317,
11082,
11083,
11084,
12201,
12202,
12203,
12204,
12205,
12206,
12207,
12208,
12209,
12210,
12211,
12212,
12213,
12214,
12215,
20327,
21666,
21667,
22043,
24563,
32856,
41403,
41404,
41405,
41406,
41407,
41408,
41537
]
}
Response 3 (returns type details by ID):
{
"type_id": 3300,
"name": "Gunnery",
"description": "Basic turret operation skill. 2% Bonus to weapon turrets' rate of fire per skill level.",
"published": true,
"group_id": 255,
"market_group_id": 364,
"radius": 1,
"volume": 0.01,
"packaged_volume": 0.01,
"icon_id": 33,
"capacity": 0,
"portion_size": 1,
"mass": 0,
"dogma_attributes": [...],
"dogma_effects": [...]
}
Package.json
{
"name": "name",
"version": "0.0.1",
"author": "author",
"homepage": "http://ionicframework.com/",
"private": true,
"scripts": {
"clean": "ionic-app-scripts clean",
"build": "ionic-app-scripts build",
"lint": "ionic-app-scripts lint",
"ionic:build": "ionic-app-scripts build",
"ionic:serve": "ionic-app-scripts serve"
},
"dependencies": {
"@angular/common": "5.0.3",
"@angular/compiler": "5.0.3",
"@angular/compiler-cli": "5.0.3",
"@angular/core": "5.0.3",
"@angular/forms": "5.0.3",
"@angular/http": "5.0.3",
"@angular/platform-browser": "5.0.3",
"@angular/platform-browser-dynamic": "5.0.3",
"@ionic-native/browser-tab": "^4.4.2",
"@ionic-native/core": "4.4.0",
"@ionic-native/deeplinks": "^4.4.2",
"@ionic-native/http": "^4.4.2",
"@ionic-native/secure-storage": "^4.4.2",
"@ionic-native/spinner-dialog": "^4.4.2",
"@ionic-native/splash-screen": "4.4.0",
"@ionic-native/sqlite": "^4.4.2",
"@ionic-native/sqlite-porter": "^4.5.0",
"@ionic-native/status-bar": "4.4.0",
"@ionic/storage": "^2.1.3",
"angular2-natural-sort": "0.0.2",
"angular2-swagger-client-generator": "0.0.22",
"cordova-android": "6.3.0",
"cordova-plugin-advanced-http": "^1.9.0",
"cordova-plugin-browsertab": "^0.2.0",
"cordova-plugin-compat": "^1.2.0",
"cordova-plugin-device": "^1.1.4",
"cordova-plugin-file": "^5.0.0",
"cordova-plugin-ionic-webview": "^1.1.16",
"cordova-plugin-native-spinner": "^1.1.3",
"cordova-plugin-secure-storage": "^2.6.8",
"cordova-plugin-splashscreen": "^4.0.3",
"cordova-plugin-statusbar": "^2.3.0",
"cordova-plugin-whitelist": "^1.3.1",
"cordova-sqlite-storage": "^2.1.2",
"ionic-angular": "3.9.2",
"ionic-plugin-deeplinks": "^1.0.15",
"ionic-plugin-keyboard": "^2.2.1",
"ionicons": "3.0.0",
"ngx-order-pipe": "^1.1.1",
"rxjs": "5.5.2",
"sw-toolbox": "3.6.0",
"swagger-angular-generator": "^1.2.1",
"uk.co.workingedge.cordova.plugin.sqliteporter": "^1.0.2",
"zone.js": "0.8.18"
},
"devDependencies": {
"@ionic/app-scripts": "3.1.4",
"typescript": "2.4.2"
},
"description": "An Ionic project",
"cordova": {
"plugins": {
"ionic-plugin-keyboard": {},
"cordova-plugin-whitelist": {},
"cordova-plugin-device": {},
"cordova-plugin-splashscreen": {},
"cordova-plugin-ionic-webview": {},
"cordova-plugin-browsertab": {},
"ionic-plugin-deeplinks": {
"URL_SCHEME": "_CUSTOMURLSCHEME",
"DEEPLINK_SCHEME": "https",
"DEEPLINK_HOST": "localhost",
"ANDROID_PATH_PREFIX": "/",
"ANDROID_2_PATH_PREFIX": "/",
"ANDROID_3_PATH_PREFIX": "/",
"ANDROID_4_PATH_PREFIX": "/",
"ANDROID_5_PATH_PREFIX": "/",
"DEEPLINK_2_SCHEME": " ",
"DEEPLINK_2_HOST": " ",
"DEEPLINK_3_SCHEME": " ",
"DEEPLINK_3_HOST": " ",
"DEEPLINK_4_SCHEME": " ",
"DEEPLINK_4_HOST": " ",
"DEEPLINK_5_SCHEME": " ",
"DEEPLINK_5_HOST": " "
},
"cordova-plugin-secure-storage": {},
"cordova-plugin-native-spinner": {},
"cordova-plugin-advanced-http": {},
"cordova-sqlite-storage": {},
"cordova-plugin-statusbar": {},
"uk.co.workingedge.cordova.plugin.sqliteporter": {}
},
"platforms": [
"android"
]
}
}
You should make use of Observables, because Observables are cold, so you can create them, store them in an array, and then combine the results so your http requests are fired at the same time, gathering details for everything you need.
Here is, in pseudo-code (as I won't copy the whole implementation you have), an Observable chain solving your problem:
getSkillGroups() {
this.http.get(this.apiRoot + 'universe/categories/16/', {}, {Authorization: 'Basic YWUxYmIzZDU4ZmRiNDk1ZDk3ZTE1ZTE0OTIyZDc0ZDk6MnpsVjNLZzVHbTh4OHY5b2lUSENYOHVXR21PYjlHd2Rqc3htQ0NHOA=='})
.switchMap(response1 => {
const groupsDetails: Observable<any>[] = [];
response1.groups.forEach(group => {
groupsDetails.push(this.getSkillsInGroup(group));
});
//This will return an array of requests ready to be fired right when you call subscribe on it
// When fired, the requests will be parallels, not sync.
return Observable.combineLatest(groupsDetails);
});
}
getSkillsInGroup(id){
return this.http.get(this.apiRoot + 'universe/groups/' + id + '/', { }, { Authorization: 'Basic YWUxYmIzZDU4ZmRiNDk1ZDk3ZTE1ZTE0OTIyZDc0ZDk6MnpsVjNLZzVHbTh4OHY5b2lUSENYOHVXR21PYjlHd2Rqc3htQ0NHOA=='})
.map(response2 => response2.data);
}
Once you call getSkillGroups()
, you get an array of response2
-typed data, meaning that you can add a new map
operator to this, creating an array of Observables to add details to the skills. Then you can subscribe on it and you get the skills.
I strongly recommend using Observables instead of promises for big cases like this one, as Observable allow you to do way more things, and easy debugging without altering data using do
operator.
More details on rxjs officiel documentation website.