Creating API factory with SwaggerJS - javascript

So I have this long standing APIService factory that creates functions to pass through the swagger functions to the UI. Here's a snippet of the factory:
'use strict';
angular.module('myApp').factory('APIService', function ($http, $window, $q, swaggerClient, $mdToast) {
var ApiDoc = {};
ApiDoc.getAllBookmarks = function () {
return $q(function (resolve, reject) {
$http.get('client/components/api/Schema.json')
.success(function (data) {
var schema = data;
_.each(schema.apis, function (b) {
b.apiDeclaration.basePath = $window.location.origin;
})
var api = swaggerClient(schema);
api = api.apiBookmarks.getAll();
resolve(api);
});
});
}
return ApiDoc;
});
And here is a snippet of it's use case in a controller:
$scope.getAllDashboards = function () {
APIService.getAllBookmarks().then(function(data){
if (data.length > 0){
$scope.dashboardsList = data;
$scope.emptyDash = false;
} else {
$scope.emptyDash = true;
}
})
}
$scope.getAllDashboards();
The inherent problem herein, is that if I have 30 API function calls in a controller, then there are 30 $http requests for schema.json that are un-needed really. Problem is that I can't figure out how to request/store that json and call on the functions with swagger the same way as they are now (or else I have to change 200+ methods in controllers, urgh). I tried this:
// var api = null;
// $http.get('client/components/api/Schema.json')
// .success(function (data) {
// var schema = data;
// _.each(schema.apis, function (b) {
// b.apiDeclaration.basePath = $window.location.origin;
// })
// api = swaggerClient(schema);
// });
But couldn't get a function after that to read it properly, or return the result of the function call in a promise like the controllers expect.
I have no other JS developers here so I need help from you all! Thanks much!

That is ugly. If you upgrade your swagger-client to something more modern, you have some options.
First off, you can cache your schema as an object, and supply it in your swaggerClient constructor using the argument spec. You'll still need to pass the URL of the target host to the client when constructing it. With that, there won't need to be any need to call anything remotely.
Next, you can see about keeping a proper swaggerClient instance around, and use it in each of your calls.

Related

Angular - Best practice for retrieving data from a Factory method

I'm looking for some information on the best way to retrieve data from a local JSON file and handle the response. After browsing through Stack Overflow, I have some mixed thoughts as I've seen multiple ways of doing the same thing (although no explanation on why one may or may not be preferred).
Essentially, I have an Angular app that is utilising a factory to retrieve data from a JSON file; I'm then waiting for the response to resolve in my controller before using it in my html file, similar to the below:
Option 1
Factory:
comparison.factory('Info', ['$http', function($http) {
var retrievalFile = 'retrievalFile.json';
return {
retrieveInfo: function() {
return $http.get(retrievalFile);
}
}
}]);
Controller:
comparison.controller('comparisonController', ['$scope', 'Info', function($scope, Info) {
Info.retrieveInfo().then(function(response) {
$scope.info = response.data;
});
}]);
My main point of contention is figuring out when it's best to wait for the response to resolve, or if it even matters. I'm toying with the idea of having the factory return the fulfilled promise, and wait for the controller to retrieve the data also. In my view, it's best to abstract all data retrieval out of the controller and into the factory, but I'm not sure if this extends to waiting for the actual data to be returned within the factory itself. With this in mind, I'm confused about whether to opt for option 1 or option 2 and would really appreciate some feedback from more experienced/qualified developers!
Option 2
Factory:
comparison.factory('Info', ['$http', function($http) {
var retrievalFile = 'retrievalFile.json';
return {
retrieveInfo: function() {
return $http.get(retrievalFile).then(function(response) {
return response.data;
});
}
}
}]);
Controller:
comparison.controller('comparisonController', ['$scope', 'Info', function($scope, Info) {
Info.retrieveInfo().then(function(response) {
$scope.info = response;
});
}]);
Thank you for any input/suggestions in advance!
It depends on what your controller is expecting and how you set up your application. Generally, I always go with the second option. Its because I usually have global error or success handlers in all api requests and I have a shared api service. Something like below.
var app = angular.module('app', []);
app.service('ApiService', ['$http', function($http) {
var get = function(url, params) {
$http.get(url, { params: params })
.then(handleSuccess, handleError);
};
// handle your global errors here
// implementation will vary based upon how you handle error
var handleError = function(response) {
return $q.reject(response);
};
// handle your success here
// you can return response.data or response based upon what you want
var handleSuccess = function(response) {
return response.data;
};
}]);
app.service('InfoService', ['ApiService', function(ApiService) {
var retrieveInfo = function() {
return ApiService.get(retrievalFile);
/**
// or return custom object that your controller is expecting
return ApiService.get.then(function(data) {
return new Person(data);
});
**//
};
// I prefer returning public functions this way
// as I can just scroll down to the bottom of service
// to see all public functions at one place rather than
// to scroll through the large file
return { retrieveInfo: retrieveInfo };
}]);
app.controller('InfoController', ['InfoService', function(InfoService) {
InfoService.retrieveInfo().then(function(info) {
$scope.info = info;
});
}])
Or if you are using router you can resolve the data into the controller. Both ngRouter and uiRouter support resolves:
$stateProvider.state({
name: 'info',
url: '/info',
controller: 'InfoController',
template: 'some template',
resolve: {
// this injects a variable called info in your controller
// with a resolved promise that you return here
info: ['InfoService', function(InfoService) {
return InfoService.retrieveInfo();
}]
}
});
// and your controller will be like
// much cleaner right
app.controller('InfoController', ['info', function(info) {
$scope.info = info;
}]);
It's really just preference. I like to think of it in terms of API. What is the API you want to expose? Do you want your controller to receive the entire response or do you want your controller to just have the data the response wraps? If you're only ever going to use response.data then option 2 works great as you never have to deal with anything but the data you're interested in.
A good example is the app we just wrote where I work. We have two apps: a back-end API and our front-end Angular application. We created an API wrapper service in the front-end application. In the service itself we place a .catch for any of the API endpoints that have documented error codes (we used Swagger to document and define our API). In that .catch we handle those error codes and return a proper error. When our controllers/directives consume the service they get back a much stricter set of data. If an error occurs then the UI is usually safe to just display the error message sent from the wrapper service and won't have to worry about looking at error codes.
Likewise for successful responses we do much of what you're doing in option 2. In many cases we refine the data down to what is minimally useful in the actual app. In this way we keep a lot of the data churning and formatting in the service and the rest of the app has a lot less to do. For instance, if we need to create an object based on that data we'll just do that in return the object to the promise chain so that controllers aren't doing that all over the place.
I would choose option two, as it your options are really mostly the same. But let see when we add a model structure like a Person suppose.
comparison.factory('Info', ['$http', function($http) {
var retrievalFile = 'retrievalFile.json';
return {
retrieveInfo: function() {
return $http.get(retrievalFile).then(function(response) {
//we will return a Person...
var data = response.data;
return new Person(data.name, data.age, data.gender);
});
}
}
}]);
This is really simple, but if you have to map more complex data into object models (you retrieve a list of people with their own items... etc), that's when things get more complicated, you will probably want to add a service to handle the mapping between data and models. Well you have another service DataMapper(example), if you choose your first option you will have to inject DataMapper into your controller and you will have to make your request through your factory, and map the response with the injected service. And then you probably say, Should I have all this code here? ... Well probably no.
That is an hypothetical case, something that count a lot is how you feel structuring your code, won't architecture it in a way you won't understand. And at the end take a look at this: https://en.wikipedia.org/wiki/SOLID_(object-oriented_design) and research more information about this principles but focused to javascript.
Good question. A couple of points:
Controllers should be view centric versus data centric therefore you
want remove data logic from the controller and rather have it focus
on business logic.
Models (M in MVC) are a data representation of your application and
will house the data logic. In Angular case this would be a service
or factory class as you rightfully pointed out. Why is that well for
example:
2.1 AccountsController (might have multiple data models injected)
2.1.1 UserModel
2.1.2 AuthModel
2.1.3 SubscriptionModel
2.1.4 SettingsModel
There are numerous ways to approach the data model approach, but I would say your service class should be the data REST model i.e. getting, storing, caching, validating, etc. I've included a basic example, but suggest you investigate JavaScript OOP as that will help point you in the right direction as to how to build data models, collections, etc.
Below is an example of service class to manage your data.Note I have not tested this code but it should give you a start.
EXAMPLE:
(function () {
'use strict';
ArticleController.$inject = ['$scope', 'Article'];
function ArticleController($scope, Article) {
var vm = this,
getArticles = function () {
return Article.getArticles()
.then(function (result) {
if (result) {
return vm.articles = result;
}
});
};
vm.getArticles = getArticles;
vm.articles = {};
// OR replace vm.articles with $scope if you prefer e.g.
$scope.articles = {};
$scope.userNgClickToInit = function () {
vm.getArticles();
};
// OR an init on document ready
// BUT to honest I would put all init logic in service class so all in calling is init in ctrl and model does the rest
function initArticles() {
vm.getArticles();
// OR chain
vm.getArticles()
.then(getCategories); // doesn't here, just an example
}
initArticles();
}
ArticleModel.$inject = ['$scope', '$http', '$q'];
function ArticleModel($scope, $http, $q) {
var model = this,
URLS = {
FETCH: 'data/articles.json'
},
articles;
function extract(result) {
return result.data;
}
function cacheArticles(result) {
articles = extract(result);
return articles;
}
function findArticle(id) {
return _.find(articles, function (article) {
return article.id === parseInt(id, 10);
})
}
model.getArticles = function () {
return (articles) ? $q.when(articles) : $http.get(URLS.FETCH).then(cacheArticles);
};
model.getArticleById = function (id) {
var deferred = $q.defer();
if (articles) {
deferred.resolve(findArticle(id))
} else {
model.getBookmarks().then(function () {
deferred.resolve(findArticle(id))
})
}
return deferred.promise;
};
model.createArticle = function (article) {
article.id = articles.length;
articles.push(article);
};
model.updateArticle = function (bookmark) {
var index = _.findIndex(articles, function (a) {
return a.id == article.id
});
articles[index] = article;
};
model.deleteArticle = function (article) {
_.remove(articles, function (a) {
return a.id == article.id;
});
};
}
angular.module('app.article.model', [])
.controller('ArticleController', ArticleController)
.service('Article', ArticleModel);
})()

Keeping the data in a service and displaying it on a view

I want to clean up my controller and move all the data fetching logic to the service, so that there wouldn't be the following code in my controller:
myService.fetchData().then(funtion(response) {
self.data = response.data;
});
This is my achievement so far: LINK
Is this a good practice? What problems could rise from this?
Also how can I remove the app.users() call in my template and leave only app.users like a variable?
It is a good practice to move the logic to the service, but you should assign the users to a property (model) instead of relinking to the service from the controller:
var self = this;
this.fetchUsers = function() {
// assign result to 'users' property, which can be used
// in the view
UserService.fetchUsers().then(function(data){
// success
self.users = data;
// or, because you remember the users in your service:
// self.users = UserService.getUsers();
});
}
and your view:
<button ng-click="app.fetchUsers()">Get users</button>
<github-table columns="app.columns" rows="app.users"></github-table>
EDIT
the service:
function fetchUsers () {
// return the $http call (which is a promise) so the
// .then can be used
return $http
.get('https://api.github.com/users')
.success(function (response) {
users = response;
});
}
I think the following code should be in the controller logic because it increases extensibility.
For an example you might call this service method in two different controllers and wants to handle the response differently. And also if you have a function that needs to execute after all the data has been retrieved, then this logic is the best.
myService.fetchData().then(funtion(response) {
self.data = response.data;
});

Variable returning null from Angular Service?

I have a SharePoint App that is built with AngularJS.
I have a controller that is calling a function in a service and it is not returning a value. I am pretty new at angularJS so I am a bit lost.
My Service:
App.factory('uploadAppService', function () {
return {
currentUserPic: function (myProfileProp) {
GetUserProfileInfo(myProfileProp).done(function (data) {
//The large thumbnail pic.
var picUrl = data.d.PictureUrl;
var largePicUrl = picUrl.replace('MThumb', 'LThumb');
console.log(largePicUrl) //the log here is correct. I want to return the largePicUrl back to my controller.
return largePicUrl;
});
}
My Controller call, I want to populate .imageUrl with the url from the service:
$scope.imageUrl = uploadAppService.currentUserPic("PictureUrl");
Thank you in advance.
To me, your currentUserPicfunction doesn't seem to return a value. GetUserProfileInfo indeed returns your largePicUrl, but this value is not used anywhere (if I correctly understand your code).
Shouldn't you use return GetUserProfileInfo(myProfileProp).done(...); ?
Edit: But as RaviH pointed, if the call is asynchronous, you'll still have to handle it in your controller.
I don't see implementation of your GetUserProfileInfo service, but i suppose it's a Deffered object.
So after code
$scope.imageUrl = uploadAppService.currentUserPic("PictureUrl");
finished working - you don't have anything in you variable $scope.imageUrl because your factory function does not return anything.
So, at first you need to modify your factory:
App.factory('uploadAppService', function () {
return {
currentUserPic: function (myProfileProp) {
return GetUserProfileInfo(myProfileProp).done(function (data) {
// ^
// Here - #ababashka's edit
//The large thumbnail pic.
var picUrl = data.d.PictureUrl;
var largePicUrl = picUrl.replace('MThumb', 'LThumb');
console.log(largePicUrl) //the log here is correct. I want to return the largePicUrl back to my controller.
return largePicUrl;
});
}
Return Deffered Object, so after it finished working, you could save your image URL by getting it in the response.
After you need to write next code:
uploadAppService.currentUserPic("PictureUrl").done(
function (response) {
$scope.imageUrl = response;
}
);
to store your URL in $scope's variable.

Angular Fire: undefined is not a function ($on method does not exist)

I'm trying to log my Firebase data to the console. But I keep getting an error of undefined is not a function. Full error below:
TypeError: undefined is not a function
at Object.childAdded (http://localhost:9000/scripts/services/ProviderService.js:11:22)
at new <anonymous> (http://localhost:9000/scripts/controllers/main.js:118:19)
This is what my code currently looks like:
ProviderService.js
angular.module('outcomesApp').service('ProviderService', function(FBURL, $q, $firebase) {
var providerRef = new Firebase(FBURL).child('providers');
var fireProvider = $firebase(providerRef).$asArray();
return {
childAdded: function childAdded(cb) {
fireProvider.$on('child_added', function(data) {
console.log(data);
});
},
...
main.js
app.controller('MainCtrl', function ($scope, $filter, $timeout, $firebase, FBURL, ProviderService) {
ProviderService.childAdded(function(addedChild) {
$scope.providers.push(addedChild);
});
...
The error is occurring on this line: fireProvider.$on('child_added', function(data) {
You're using an outdated version of AngularFire. That version looks like 0.7 and since 0.8, there has been a lot of changes. The .$on method no longer exists, so that's why you're receiving the undefined error. Now with the new version of Firebase you can refactor your code to just a few short lines using the new API.
When needing to manage a collection, use the new .$asArray() method.
With the 0.8 update, .$on() has been removed and replaced with using either .$asArray() or .$asObject.
Previously we'd have to use .$on() to listen to children being added to the collection. Now using .$asArray() we get automatic updates to a collection.
angular.module('example').controller('MainCtrl', function($scope, $firebase) {
var ref = new Firebase('<your-firebase>');
// using $asArray we automatically have child_added handled for us
$scope.items = $firebase(ref).$asArray();
});
You can easily put this data in a Factory as well.
angular.module('example').factory('Items', function($firebase) {
var ref = new Firebase('<your-firebase>');
return $firebase(ref).$asArray();
});
angular.module('example').controller('MainCtrl', function($scope, Items) {
$scope.items = Items;
});
Are you trying to get promise data?, I don't know much about firebase, let's try this. You have $q but not being used. If this throw error, it has to do with the service provider.
function providerReference(){
var deferred = $q.defer(),
providerRef = new Firebase(FBURL).child('providers');
providerRef.then(function(d) {
var fireProvider = $firebase(d).$asArray();
deferred.resolve(fireProvider);
}, function(err) {
console.log('error')
});
return deferred.promise
}
return {
childAdded: function() {
return providerReference();
}
}
In main.js
$scope.providers = [];
ProviderService.childAdded().then(function(addedChild) {
$scope.providers.push(addedChild);
})

How to use data from http request in angularjs controller?

I'm a code newbie and trying to learn angularjs. My project is simple: get JSON data from an API and display it on a web page. I found functions to make the http request and then use the data:
app.factory('myService', function($http) {
var myService = {
async: function() {
var promise = $http.get('<URL to api>').then(function(response) {
console.log(response);
return response.data;
});
return promise;
}
};
return myService;
});
app.controller('getRealTimeBusDataCtrl', function(myService, $scope) {
myService.async().then(function(d) {
$scope.data = d;
});
});
I can then access and display the whole JSON data chunk or parts of it.
What I'd like to do is set multiple $scope variables instead of just one, but as soon as I try that, the code breaks.
What I'm trying to do:
if (d.ResponseData.Buses[1].JourneyDirection = 1) {
$scope.timeToSollentuna = d.ResponseData.Buses[1].DisplayTime;
else if (d.ResponseData.Buses[1].JourneyDirection = 2) {
$scope.timeToVallingby = d.ResponseData.Buses[1].DisplayTime;
} else if (d.ResponseData.Buses[2].JourneyDirection = 1) {
$scope.timeToSollentuna = d.ResponseData.Buses[2].DisplayTime;
} else {
$scope.timeToVallingby = d.ResponseData.Buses[2].DisplayTime;
}
}
I'm guessing the problem is how the function is set up: that it somehow limits the number of variables I can set or the number of things I can "do" after .then, but I haven't been able to figure out another way to do it.
Sorry if the answer to this question is SO obvious, but I've really tried to find it and failed.
Best regards,
The way that service is written is unnecessarily verbose. Instead, I would rewrite it like this (especially if you're starting out and learning the basics--it's good to learn good habits when starting).
app.factory('myService', ["$http", function($http){
return {
getData: function() {
return $http.get('path/to/api');
}
};
}]);
app.controller('MainCtrl', ["$scope", "myService", function($scope, myService) {
//declare your data model first
$scope.busData = undefined;
//use the service to get data
myService.getData().then(function(response) {
//success... got a response
//this is where you can apply your data
$scope.busData = response.data;
//call a function to apply if you'd like
applyBusData(response.data);
}).catch(function(response) {
//error has occurred
});
function applyBusData(data) {
if(data.Buses[1].JourneyDirection === 1) {
//etcc... etc...
} else {
//etc.. etc...
}
}
}]);

Categories