-
-
Save jelbourn/6276338 to your computer and use it in GitHub Desktop.
/** | |
* Example of using an angular provider to build an api service. | |
* @author Jeremy Elbourn (@jelbourn) | |
*/ | |
/** Namespace for the application. */ | |
var app = {}; | |
/******************************************************************************/ | |
/** | |
* Interface for a model objects used with the api service. | |
* @interface | |
*/ | |
app.ApiModel = function() {}; | |
/** | |
* Data transformation done after fetching data from the server. | |
* @type {Function} | |
*/ | |
app.ApiModel.prototype.afterLoad; | |
/** | |
* Data transformation done before posting / putting data to the server. | |
* @type {Function} | |
*/ | |
app.ApiModel.prototype.beforeSave; | |
/******************************************************************************/ | |
/** | |
* Configuration object for an api endpoint. | |
* @constructor | |
*/ | |
app.ApiEndpointConfig = function() { | |
/** | |
* Map of actions for the endpoint, keyed by action name. An action has a HTTP | |
* method (GET, POST, etc.) as well as an optional set of default parameters. | |
* @type {Object.<string, {method: string, params: Object}>} | |
*/ | |
this.actions = {}; | |
/** The default actions defined for every endpoint. */ | |
var defaultActions = { | |
'GET': 'get', | |
'PUT': 'update', | |
'POST': 'save', | |
'PATCH': 'patch', | |
'DELETE': 'remove' | |
}; | |
// Add the default actions to this endpoint. | |
var self = this; | |
angular.forEach(defaultActions, function(alias, method) { | |
self.addHttpAction(method, alias); | |
}); | |
}; | |
/** | |
* Set the route for this endpoint. This is relative to the server's base route. | |
* @param {string} route | |
* @return {app.ApiEndpointConfig} | |
*/ | |
app.ApiEndpointConfig.prototype.route = function(route) { | |
this.route = route; | |
return this; | |
}; | |
/** | |
* Set the route for this endpoint. This is relative to the server's base route. | |
* @param {function(): app.ApiModel} model | |
* @return {app.ApiEndpointConfig} | |
*/ | |
app.ApiEndpointConfig.prototype.model = function(model) { | |
this.model = model; | |
return this; | |
}; | |
/** | |
* Adds an action to the endpoint. | |
* @param {string} method The HTTP method for the action. | |
* @param {string} name The name of the action. | |
* @param {Object=} params The default parameters for the action. | |
*/ | |
app.ApiEndpointConfig.prototype.addHttpAction = function(method, name, params) { | |
this.actions[name] = {method: method.toUpperCase(), params: params}; | |
}; | |
/******************************************************************************/ | |
/** | |
* An api endpoint. | |
* | |
* @constructor | |
* @param {string} baseRoute The server api's base route. | |
* @param {app.ApiEndpointConfig} endpointConfig Configuration object for the | |
* endpoint. | |
* @param {!Object} $injector The angular $injector service. | |
* @param {!Function} $resource The angular $resource service. | |
*/ | |
app.ApiEndpoint = function(baseRoute, endpointConfig, $injector, $resource) { | |
this.config = endpointConfig; | |
this.$injector = $injector; | |
this.resource = $resource(baseRoute + endpointConfig.route, {}, | |
endpointConfig.actions); | |
// Extend this endpoint objects with methods for all of the actions defined | |
// in the configuration object. The action performed depends on whether or | |
// not there is a model defined in the configuration; when there is a model | |
// defined, certain request types must be wrapped in order to apply the | |
// pre/post request transformations defined by the model. | |
var self = this; | |
angular.forEach(endpointConfig.actions, function(action, actionName) { | |
var actionMethod = self.request; | |
if (endpointConfig.model) { | |
if (action.method === 'GET') { | |
actionMethod = self.getRequestWithModel; | |
} else if (action.method === 'PUT' || action.method === 'POST') { | |
actionMethod = self.saveRequestWithModel; | |
} | |
} | |
self[actionName] = angular.bind(self, actionMethod, actionName); | |
}); | |
}; | |
/** | |
* Instantiates a model object from the raw server response data. | |
* @param {Object} data The raw server response data. | |
* @return {app.ApiModel} The server response data wrapped in a model object. | |
*/ | |
app.ApiEndpoint.prototype.instantiateModel = function(data) { | |
var model = this.$injector.instantiate(this.config.model); | |
angular.extend(model, data); | |
model.afterLoad(); | |
return model; | |
}; | |
/** | |
* Perform a standard http request. | |
* | |
* @param {string} action The name of the action. | |
* @param {Object=} params The parameters for the request. | |
* @param {Object=} data The request data (for PUT / POST requests). | |
* @return {angular.$q.Promise} A promise resolved when the http request has | |
* a response. | |
*/ | |
app.ApiEndpoint.prototype.request = function(action, params, data) { | |
return this.resource[action](params, data).$promise; | |
}; | |
/** | |
* Perform an HTTP GET request and performs a post-response transformation | |
* on the data as defined in the model object. | |
* | |
* @param {string} action The name of the action. | |
* @param {Object=} params The parameters for the request. | |
* @return {angular.$q.Promise} A promise resolved when the http request has | |
* a response. | |
*/ | |
app.ApiEndpoint.prototype.getRequestWithModel = function(action, params) { | |
var promise = this.request(action, params); | |
var instantiateModel = this.instantiateModel.bind(this); | |
// Wrap the raw server response data in an instantiated model object | |
// (or multiple, if response data is an array). | |
return promise.then(function(response) { | |
var data = response.data; | |
response.data = angular.isArray(data) ? | |
data.map(instantiateModel) : instantiateModel(data); | |
}); | |
}; | |
/** | |
* Performs an HTTP PUT or POST after performing a pre-request transformation | |
* on the data as defined in the model object. | |
* | |
* @param {string} action The name of the action. | |
* @param {Object=} params The parameters for the request. | |
* @param {Object=} data The request data (for PUT / POST requests). | |
* @return {angular.$q.Promise} A promise resolved when the http request has | |
* a response. | |
*/ | |
app.ApiEndpoint.prototype.saveRequestWithModel = function(action, params, data) { | |
// Copy the given data so that the beforeSave operation doesn't alter the | |
// object state from wherever the request was triggered. | |
var model = angular.copy(data); | |
if (model && model.beforeSave) { | |
model.beforeSave(); | |
} | |
return this.request(action, params, model); | |
}; | |
/******************************************************************************/ | |
/** | |
* Angular provider for configuring and instantiating as api service. | |
* | |
* @constructor | |
*/ | |
app.ApiProvider = function() { | |
this.baseRoute = ''; | |
this.endpoints = {}; | |
}; | |
/** | |
* Sets the base server api route. | |
* @param {string} route The base server route. | |
*/ | |
app.ApiProvider.prototype.setBaseRoute = function(route) { | |
this.baseRoute = route; | |
}; | |
/** | |
* Creates an api endpoint. The endpoint is returned so that configuration of | |
* the endpoint can be chained. | |
* | |
* @param {string} name The name of the endpoint. | |
* @return {app.ApiEndpointConfig} The endpoint configuration object. | |
*/ | |
app.ApiProvider.prototype.endpoint = function(name) { | |
var endpointConfig = new app.ApiEndpointConfig(); | |
this.endpoints[name] = endpointConfig; | |
return endpointConfig; | |
}; | |
/** | |
* Function invoked by angular to get the instance of the api service. | |
* @return {Object.<string, app.ApiEndpoint>} The set of all api endpoints. | |
*/ | |
app.ApiProvider.prototype.$get = ['$injector', function($injector) { | |
var api = {}; | |
var self = this; | |
angular.forEach(this.endpoints, function(endpointConfig, name) { | |
api[name] = $injector.instantiate(app.ApiEndpoint, { | |
baseRoute: self.baseRoute, | |
endpointConfig: endpointConfig | |
}); | |
}); | |
return api; | |
}]; | |
/******************************************************************************/ | |
// Example of creating an angular module to house "core" functionality. It is | |
// here that you add any custom providers. | |
app.core = angular.module('core', ['ngResource']); | |
app.core.config(function($provide) { | |
$provide.provider('api', app.ApiProvider); | |
}); | |
/******************************************************************************/ | |
// Example of creating an angular module for your app or part of your app. | |
// A provider can be injected into a config function, which is run before | |
// normal services are instantiated. | |
app.component = angular.module('component', ['core']); | |
app.component.config(function(apiProvider) { | |
apiProvider.setBaseRoute('my/app/api/'); | |
apiProvider.endpoint('songs') | |
.route('songs/:id') | |
.addHttpAction('POST', 'favorite', {isFavorite: true}) | |
.model(app.Song); | |
apiProvider.endpoint('albums') | |
.route('albums/:id') | |
.model(app.Album); | |
}); | |
/******************************************************************************/ | |
// Example of making a request with the api service in a controller. | |
app.SongController = function($scope, $routeParams, api) { | |
var songsPromise = api.songs.get({id: $routeParams.id}); | |
}; |
Thank you for this. It was a true inspiration.
I was looking at this and I have a newbie question about how to configure and use it. I saw the examples but my problem is that I don't know where in the Angular app the different parts should be placed.
I've started an Angular app with a modified ngbp as boilerplate. Ideally, I would like to configure the basic stuff in the app.js file, like these lines of code:
(function(app) {
app.config(function ($stateProvider, $urlRouterProvider, $provide) {
$urlRouterProvider.otherwise('/home');
});
app.run(function () {});
app.controller('AppController', function ($scope) { });
app.core = angular.module('core', ['ngResource']);
app.core.config(function($provide) {
app.ApiProvider.setBaseRoute('http://localhost:31460/api');
$provide.provider('api', app.ApiProvider);
});
}(angular.module("myApp", [
'templates-app',
'templates-common',
'angular-api-provider',
etc...
)], window);
And then I would like to configure the following code in the controller´s module config:
app.ApiProvider.endpoint('user')
.route('user/:id')
.model(app.Song);
Is this possible or how should I go about it?
Again, sorry for such a trivial question!
Thanks,
I had a problem using this with a model, but resolved it.
This
app.ApiEndpoint.prototype.getRequestWithModel = function(action, params) {
var promise = this.request(action, params);
var instantiateModel = this.instantiateModel.bind(this);
// Wrap the raw server response data in an instantiated model object
// (or multiple, if response data is an array).
return promise.then(function(response) {
var data = response.data;
response.data = angular.isArray(data) ?
data.map(instantiateModel) : instantiateModel(data);
});
};
Becomes
app.ApiEndpoint.prototype.getRequestWithModel = function(action, params) {
var promise = this.request(action, params);
var instantiateModel = this.instantiateModel.bind(this);
// Wrap the raw server response data in an instantiated model object
// (or multiple, if response data is an array).
promise.then(function(response) {
var data = response.data;
response.data = angular.isArray(data) ?
data.map(instantiateModel) : instantiateModel(data);
});
return promise;
};
Also, if you want to use Angular's strict DI mode, you have to use dependency-annotated objects when using $injector.instantiate.
Assuming your model prototype stands alone,
app.ApiEndpoint.prototype.instantiateModel = function(data) {
var model = this.$injector.instantiate(this.config.model);
angular.extend(model, data);
model.afterLoad();
...
app.ApiProvider.prototype.$get = ['$injector', function($injector) {
var api = {};
var self = this;
angular.forEach(this.endpoints, function(endpointConfig, name) {
api[name] = $injector.instantiate(app.ApiEndpoint, {
baseRoute: self.baseRoute,
endpointConfig: endpointConfig
});
Becomes
app.ApiEndpoint.prototype.instantiateModel = function(data) {
var model = this.$injector.instantiate([this.config.model]);
angular.extend(model, data);
model.afterLoad();
...
app.ApiProvider.prototype.$get = ['$injector', function($injector) {
var api = {};
var self = this;
angular.forEach(this.endpoints, function(endpointConfig, name) {
api[name] = $injector.instantiate(['baseRoute', 'endpointConfig', '$injector', '$resource', app.ApiEndpoint], {
baseRoute: self.baseRoute,
endpointConfig: endpointConfig
});
Hopefully that was helpful to some poor sap like me banging his head against his desk somewhere.
Oh, one more thing.
With this
app.ApiEndpoint = function(baseRoute, endpointConfig, $injector, $resource) {
this.config = endpointConfig;
this.$injector = $injector;
this.resource = $resource(baseRoute + endpointConfig.route, {},
endpointConfig.actions);
...
var self = this;
angular.forEach(endpointConfig.actions, function(action, actionName) {
var actionMethod = self.request;
if (endpointConfig.model) {
...
You have to add a property like model: true
in each endpoint.
myApiEndpoint.endpoint('awesome')
.customAction('get', 'get', {isArray: true, model: true})
.model(awesomeModel)
The following enables a model for all relevant actions as long as .model()
is specified.
angular.forEach(endpointConfig.actions, function(action, actionName) {
var actionMethod = self.request;
if (self.config.model) {
...
Thanks for this! It inspired me to build https://github.com/meanie/angular-api
This is an Angular API service (part of the Meanie MEAN boilerplate) to easily define endpoints, optionally using data models. It allows you to easily bind functions on your models like .save() or .delete(), much like with Angular's $resource
service but with more flexibility. It also works with params like $resource
does, e.g. they can be extracted from supplied params or from the model.
Furthermore, it's fully configurable, but with sensible defaults. You can specify configuration globally for the whole API, per endpoint, or per action. Each next level will merge it's configuration with the previous.
As a bonus it comes with an optional $http
decorator to filter out duplicate requests, and either reject them or simply return the promise of the already pending request.
Can be installed via Bower, npm or the Meanie CLI. Give it a go next time you need an API service!
This is awesome. Thanks for sharing.
Hi, i'm quite new to Angular and trying to use this provider. I'm facing problem with https://gist.github.com/jelbourn/6276338#file-api-provider-js-L172-L176 If i try to use it as it is, i have undefined response in my controller, but if i do: promise.then ... and the return at the end: return promise; than i have the response in my controller. Am i doing something wrong here?