├── .babelrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── dist └── angular2-api.js ├── index.d.ts ├── karma-shim.js ├── karma.conf.js ├── package.json ├── src ├── ApiConfig.ts ├── ApiResource.ts ├── ApiService.ts └── index.ts ├── test └── ApiService.spec.ts ├── tsconfig.json ├── typings.json └── webpack.config.babel.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: ["es2015", "stage-2"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node,osx 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | 41 | ### OSX ### 42 | .DS_Store 43 | .AppleDouble 44 | .LSOverride 45 | 46 | # Icon must end with two \r 47 | Icon 48 | 49 | # Thumbnails 50 | ._* 51 | 52 | # Files that might appear in the root of a volume 53 | .DocumentRevisions-V100 54 | .fseventsd 55 | .Spotlight-V100 56 | .TemporaryItems 57 | .Trashes 58 | .VolumeIcon.icns 59 | 60 | # Directories potentially created on remote AFP share 61 | .AppleDB 62 | .AppleDesktop 63 | Network Trash Folder 64 | Temporary Items 65 | .apdisk 66 | 67 | typings 68 | typings/ 69 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mika Kalathil 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular2-api 2 | Api service for angular2 to work with REST resources 3 | 4 | ## Getting Started 5 | 6 | ```bash 7 | $ npm i --save angular2-api 8 | ``` 9 | 10 | Add this to your bootstrap file 11 | ```typescript 12 | import { ApiService, provideApiService } from 'angular2-api'; 13 | 14 | bootstrap(App, [ 15 | ApiService 16 | ]) 17 | 18 | // To provide custom global deserialize/serialize/serializeParams 19 | bootstrap(App, [ 20 | { provide: ApiConfig, useValue: new ApiConfig({ 21 | basePath: '/test', 22 | 23 | deserialize(data) { 24 | return data 25 | }, 26 | 27 | serialize(data) { 28 | return data 29 | }, 30 | 31 | serializeParams(data) { 32 | return data 33 | } 34 | }) 35 | } 36 | ApiService 37 | ]) 38 | ``` 39 | 40 | ## Using ApiService 41 | The most common use case for api service would be to create resources 42 | 43 | ```typescript 44 | import {Injectable} from 'angular2/core' 45 | import {ApiResource, ApiService} from 'angular2-api' 46 | 47 | @Injectable() 48 | export class MyResource implements ApiResource { 49 | endpoint: string = 'my-endpoint' 50 | idAttribute: string = 'testId' // If not provided will use 'id' 51 | 52 | deserialize(data) { 53 | return data 54 | } 55 | 56 | serialize(data) { 57 | return data 58 | } 59 | 60 | serializeParams(params) { 61 | return params 62 | } 63 | } 64 | 65 | export class MyComponent { 66 | constructor(apiService: ApiService, myResource: MyResource) { 67 | apiService.find(myResource, 1, ...args) 68 | } 69 | } 70 | ``` 71 | 72 | ## Methods 73 | *For `url` you can pass an array of strings or a string* 74 | 75 | - `ApiService.get(resource, url, [params])` 76 | - `ApiService.put(resource, url, data, [params])` 77 | - `ApiService.patch(resource, url, data, [params])` 78 | - `ApiService.post(resource, url, data, [params])` 79 | - `ApiService.delete(resource, url, [params])` 80 | - `ApiService.find(resource, id, [params])` 81 | - `ApiService.findAll(resource, [params])` 82 | - `ApiService.create(resource, data, [params])` 83 | - `ApiService.update(resource, [data, params])` 84 | - `ApiService.destroy(resource, [id, params])` 85 | 86 | 87 | -------------------------------------------------------------------------------- /dist/angular2-api.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(require("@angular/core"), require("@angular/http"), require("rxjs/Observable"), require("rxjs/add/operator/map"), require("rxjs/add/operator/catch"), require("rxjs/add/observable/throw")); 4 | else if(typeof define === 'function' && define.amd) 5 | define(["@angular/core", "@angular/http", "rxjs/Observable", "rxjs/add/operator/map", "rxjs/add/operator/catch", "rxjs/add/observable/throw"], factory); 6 | else if(typeof exports === 'object') 7 | exports["angular2-api"] = factory(require("@angular/core"), require("@angular/http"), require("rxjs/Observable"), require("rxjs/add/operator/map"), require("rxjs/add/operator/catch"), require("rxjs/add/observable/throw")); 8 | else 9 | root["angular2-api"] = factory(root["@angular/core"], root["@angular/http"], root["rxjs/Observable"], root["rxjs/add/operator/map"], root["rxjs/add/operator/catch"], root["rxjs/add/observable/throw"]); 10 | })(this, function(__WEBPACK_EXTERNAL_MODULE_2__, __WEBPACK_EXTERNAL_MODULE_3__, __WEBPACK_EXTERNAL_MODULE_5__, __WEBPACK_EXTERNAL_MODULE_6__, __WEBPACK_EXTERNAL_MODULE_7__, __WEBPACK_EXTERNAL_MODULE_8__) { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) 20 | /******/ return installedModules[moduleId].exports; 21 | 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ exports: {}, 25 | /******/ id: moduleId, 26 | /******/ loaded: false 27 | /******/ }; 28 | 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | 32 | /******/ // Flag the module as loaded 33 | /******/ module.loaded = true; 34 | 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | 39 | 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | 46 | /******/ // __webpack_public_path__ 47 | /******/ __webpack_require__.p = ""; 48 | 49 | /******/ // Load entry module and return exports 50 | /******/ return __webpack_require__(0); 51 | /******/ }) 52 | /************************************************************************/ 53 | /******/ ([ 54 | /* 0 */ 55 | /***/ function(module, exports, __webpack_require__) { 56 | 57 | "use strict"; 58 | 59 | var ApiService_1 = __webpack_require__(1); 60 | exports.ApiService = ApiService_1.ApiService; 61 | var ApiConfig_1 = __webpack_require__(4); 62 | exports.ApiConfig = ApiConfig_1.ApiConfig; 63 | 64 | /***/ }, 65 | /* 1 */ 66 | /***/ function(module, exports, __webpack_require__) { 67 | 68 | "use strict"; 69 | 70 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 71 | 72 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; 73 | 74 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 75 | 76 | var __decorate = undefined && undefined.__decorate || function (decorators, target, key, desc) { 77 | var c = arguments.length, 78 | r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, 79 | d; 80 | if ((typeof Reflect === "undefined" ? "undefined" : _typeof(Reflect)) === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);else for (var i = decorators.length - 1; i >= 0; i--) { 81 | if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 82 | }return c > 3 && r && Object.defineProperty(target, key, r), r; 83 | }; 84 | var __metadata = undefined && undefined.__metadata || function (k, v) { 85 | if ((typeof Reflect === "undefined" ? "undefined" : _typeof(Reflect)) === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 86 | }; 87 | var __param = undefined && undefined.__param || function (paramIndex, decorator) { 88 | return function (target, key) { 89 | decorator(target, key, paramIndex); 90 | }; 91 | }; 92 | var core_1 = __webpack_require__(2); 93 | var http_1 = __webpack_require__(3); 94 | var ApiConfig_1 = __webpack_require__(4); 95 | var Observable_1 = __webpack_require__(5); 96 | __webpack_require__(6); 97 | __webpack_require__(7); 98 | __webpack_require__(8); 99 | var removeSlashes = function removeSlashes(url) { 100 | if (!url) return url; 101 | if (url.startsWith('/')) url = url.slice(1, url.length); 102 | if (url.endsWith('/')) url = url.slice(0, url.length - 1); 103 | return url; 104 | }; 105 | var toJSON = function toJSON(data) { 106 | try { 107 | return JSON.stringify(data); 108 | } catch (e) { 109 | return data; 110 | } 111 | }; 112 | var createParams = function createParams(headers) {}; 113 | var serializeParams = function serializeParams() { 114 | var params = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; 115 | 116 | if (!params) params = {}; 117 | var _params = params; 118 | var headers = _params.headers; 119 | 120 | if (!headers) { 121 | params.headers = new http_1.Headers({ 'Content-Type': 'application/json' }); 122 | } else if (headers && !headers.get('Content-Type')) { 123 | headers.set('Content-Type', 'application/json'); 124 | } 125 | return params; 126 | }; 127 | var deserializeResponse = function deserializeResponse(resp) { 128 | var contentType = resp && resp.headers && (resp.headers.get('content-type') || resp.headers.get('Content-Type')); 129 | if (!contentType) return resp; 130 | if (/json/.test(contentType)) return resp.json();else if (/text/.test(contentType)) return resp.text();else if (/blob/.test(contentType)) return resp.blob();else return resp; 131 | }; 132 | var runTransformIfHas = function runTransformIfHas(transformBase, method, data) { 133 | return typeof transformBase[method] === 'function' ? transformBase[method](data) : data; 134 | }; 135 | var resourceDeserialize = function resourceDeserialize(resource) { 136 | return function (data) { 137 | return runTransformIfHas(resource, 'deserialize', data); 138 | }; 139 | }; 140 | var ApiService = function () { 141 | function ApiService(_http, _config) { 142 | _classCallCheck(this, ApiService); 143 | 144 | this._http = _http; 145 | this._config = _config; 146 | } 147 | 148 | _createClass(ApiService, [{ 149 | key: "createUrl", 150 | value: function createUrl(resource, url) { 151 | var qUrl = String(Array.isArray(url) ? url.join('/') : url); 152 | return removeSlashes(this._config.basePath) + "/" + removeSlashes(resource.endpoint) + "/" + removeSlashes(qUrl); 153 | } 154 | }, { 155 | key: "get", 156 | value: function get(resource, url, params) { 157 | var _this = this; 158 | 159 | return this._http.get(this.createUrl(resource, url), this._serializeParams(resource, params)).map(function (data) { 160 | return _this._deserialize(data); 161 | }).map(resourceDeserialize(resource)).catch(function (error) { 162 | return _this._catchError(error); 163 | }); 164 | } 165 | }, { 166 | key: "put", 167 | value: function put(resource, url, data, params) { 168 | var _this2 = this; 169 | 170 | return this._http.put(this.createUrl(resource, url), this._serialize(resource, data), this._serializeParams(resource, params)).map(function (data) { 171 | return _this2._deserialize(data); 172 | }).map(resourceDeserialize(resource)).catch(function (error) { 173 | return _this2._catchError(error); 174 | }); 175 | } 176 | }, { 177 | key: "patch", 178 | value: function patch(resource, url, data, params) { 179 | var _this3 = this; 180 | 181 | return this._http.patch(this.createUrl(resource, url), this._serialize(resource, data), this._serializeParams(resource, params)).map(function (data) { 182 | return _this3._deserialize(data); 183 | }).map(resourceDeserialize(resource)).catch(function (error) { 184 | return _this3._catchError(error); 185 | }); 186 | } 187 | }, { 188 | key: "post", 189 | value: function post(resource, url, data, params) { 190 | var _this4 = this; 191 | 192 | return this._http.post(this.createUrl(resource, url), this._serialize(resource, data), this._serializeParams(resource, params)).map(function (data) { 193 | return _this4._deserialize(data); 194 | }).map(resourceDeserialize(resource)).catch(function (error) { 195 | return _this4._catchError(error); 196 | }); 197 | } 198 | }, { 199 | key: "delete", 200 | value: function _delete(resource, url, params) { 201 | var _this5 = this; 202 | 203 | return this._http.get(this.createUrl(resource, url), this._serializeParams(resource, params)).map(function (data) { 204 | return _this5._deserialize(data); 205 | }).map(resourceDeserialize(resource)).catch(function (error) { 206 | return _this5._catchError(error); 207 | }); 208 | } 209 | }, { 210 | key: "find", 211 | value: function find(resource, id, params) { 212 | if (typeof id === 'undefined') throw new Error('You must provide an id'); 213 | return this.get(resource, id, params); 214 | } 215 | }, { 216 | key: "findAll", 217 | value: function findAll(resource, params) { 218 | return this.get(resource, '', params); 219 | } 220 | }, { 221 | key: "create", 222 | value: function create(resource, data, params) { 223 | return this.post(resource, '', data, params); 224 | } 225 | }, { 226 | key: "update", 227 | value: function update(resource, data, params) { 228 | var id = data[resource.idAttribute || 'id'], 229 | url = id ? id : ''; 230 | return this.put(resource, url, data, params); 231 | } 232 | }, { 233 | key: "destroy", 234 | value: function destroy(resource, id, params) { 235 | if ((typeof id === "undefined" ? "undefined" : _typeof(id)) === 'object') { 236 | params = id; 237 | id = null; 238 | } 239 | return this.delete(resource, id ? id : '', params); 240 | } 241 | }, { 242 | key: "_serialize", 243 | value: function _serialize(resource, data) { 244 | return toJSON(runTransformIfHas(this._config, 'serialize', runTransformIfHas(resource, 'serialize', data))); 245 | } 246 | }, { 247 | key: "_deserialize", 248 | value: function _deserialize(data) { 249 | return runTransformIfHas(this._config, 'deserialize', deserializeResponse(data)); 250 | } 251 | }, { 252 | key: "_serializeParams", 253 | value: function _serializeParams(resource, params) { 254 | params = runTransformIfHas(this._config, 'serializeParams', serializeParams(params)); 255 | return runTransformIfHas(resource, 'serializeParams', params); 256 | } 257 | }, { 258 | key: "_catchError", 259 | value: function _catchError(error) { 260 | if (error instanceof Error) throw error;else return Observable_1.Observable.throw(this._deserialize(error)); 261 | } 262 | }]); 263 | 264 | return ApiService; 265 | }(); 266 | ApiService = __decorate([core_1.Injectable(), __param(1, core_1.Optional()), __metadata('design:paramtypes', [http_1.Http, ApiConfig_1.ApiConfig])], ApiService); 267 | exports.ApiService = ApiService; 268 | 269 | /***/ }, 270 | /* 2 */ 271 | /***/ function(module, exports) { 272 | 273 | module.exports = __WEBPACK_EXTERNAL_MODULE_2__; 274 | 275 | /***/ }, 276 | /* 3 */ 277 | /***/ function(module, exports) { 278 | 279 | module.exports = __WEBPACK_EXTERNAL_MODULE_3__; 280 | 281 | /***/ }, 282 | /* 4 */ 283 | /***/ function(module, exports, __webpack_require__) { 284 | 285 | "use strict"; 286 | 287 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 288 | 289 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; 290 | 291 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 292 | 293 | var __decorate = undefined && undefined.__decorate || function (decorators, target, key, desc) { 294 | var c = arguments.length, 295 | r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, 296 | d; 297 | if ((typeof Reflect === "undefined" ? "undefined" : _typeof(Reflect)) === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);else for (var i = decorators.length - 1; i >= 0; i--) { 298 | if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 299 | }return c > 3 && r && Object.defineProperty(target, key, r), r; 300 | }; 301 | var __metadata = undefined && undefined.__metadata || function (k, v) { 302 | if ((typeof Reflect === "undefined" ? "undefined" : _typeof(Reflect)) === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 303 | }; 304 | var __param = undefined && undefined.__param || function (paramIndex, decorator) { 305 | return function (target, key) { 306 | decorator(target, key, paramIndex); 307 | }; 308 | }; 309 | var core_1 = __webpack_require__(2); 310 | var AbstractApiConfig = function AbstractApiConfig() { 311 | _classCallCheck(this, AbstractApiConfig); 312 | }; 313 | AbstractApiConfig = __decorate([core_1.Injectable(), __metadata('design:paramtypes', [])], AbstractApiConfig); 314 | exports.AbstractApiConfig = AbstractApiConfig; 315 | var ApiConfig = function () { 316 | function ApiConfig(_ref) { 317 | var basePath = _ref.basePath; 318 | var deserialize = _ref.deserialize; 319 | var serialize = _ref.serialize; 320 | var serializeParams = _ref.serializeParams; 321 | 322 | _classCallCheck(this, ApiConfig); 323 | 324 | this.basePath = basePath; 325 | if (deserialize) this.deserialize = deserialize; 326 | if (serialize) this.serialize = serialize; 327 | if (serializeParams) this.serializeParams = serializeParams; 328 | } 329 | 330 | _createClass(ApiConfig, [{ 331 | key: "deserialize", 332 | value: function deserialize(data) { 333 | return data; 334 | } 335 | }, { 336 | key: "serialize", 337 | value: function serialize(data) { 338 | return data; 339 | } 340 | }, { 341 | key: "serializeParams", 342 | value: function serializeParams(params) { 343 | return params; 344 | } 345 | }]); 346 | 347 | return ApiConfig; 348 | }(); 349 | ApiConfig = __decorate([core_1.Injectable(), __param(0, core_1.Optional()), __metadata('design:paramtypes', [Object])], ApiConfig); 350 | exports.ApiConfig = ApiConfig; 351 | 352 | /***/ }, 353 | /* 5 */ 354 | /***/ function(module, exports) { 355 | 356 | module.exports = __WEBPACK_EXTERNAL_MODULE_5__; 357 | 358 | /***/ }, 359 | /* 6 */ 360 | /***/ function(module, exports) { 361 | 362 | module.exports = __WEBPACK_EXTERNAL_MODULE_6__; 363 | 364 | /***/ }, 365 | /* 7 */ 366 | /***/ function(module, exports) { 367 | 368 | module.exports = __WEBPACK_EXTERNAL_MODULE_7__; 369 | 370 | /***/ }, 371 | /* 8 */ 372 | /***/ function(module, exports) { 373 | 374 | module.exports = __WEBPACK_EXTERNAL_MODULE_8__; 375 | 376 | /***/ } 377 | /******/ ]) 378 | }); 379 | ; -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import {Http, RequestOptionsArgs} from '@angular/http' 2 | import {Observable} from 'rxjs/Observable' 3 | 4 | export interface ApiConfigArgs { 5 | basePath?: string 6 | deserialize: ((data: any|any[]) => any|any[]) 7 | serialize: ((data: any|any[]) => any|any[]) 8 | serializeParams: ((params: RequestOptionsArgs) => RequestOptionsArgs) 9 | } 10 | 11 | export abstract class AbstractApiConfig { 12 | basePath: string 13 | abstract deserialize(data: any|any[]): any|any[] 14 | abstract serialize(data: any|any[]): any|any[] 15 | abstract serializeParams(params: RequestOptionsArgs): RequestOptionsArgs 16 | } 17 | 18 | export class ApiConfig { 19 | constructor({basePath, deserialize, serialize, serializeParams}: ApiConfigArgs) 20 | basePath: string 21 | deserialize: ((data: any|any[]) => any|any[]) 22 | serialize: ((data: any|any[]) => any|any[]) 23 | serializeParams: ((params: RequestOptionsArgs) => RequestOptionsArgs) 24 | } 25 | 26 | export const provideApiService: ((config: ApiConfig) => Array) 27 | 28 | export interface ApiResource { 29 | idAttribute?: string 30 | endpoint: string 31 | 32 | deserialize?(data: any): any 33 | serialize?(data: any): any 34 | serializeParams?(params: RequestOptionsArgs): RequestOptionsArgs 35 | } 36 | 37 | export class ApiService { 38 | private _http 39 | basePath: string 40 | constructor(_http: Http) 41 | initialize(resource: ApiResource): void 42 | createUrl(resource: ApiResource, url: string|string[]): string 43 | get(resource: ApiResource, url: string|string[], params?: RequestOptionsArgs): Observable 44 | put(resource: ApiResource, url: string|string[], data: any, params?: RequestOptionsArgs): Observable 45 | patch(resource: ApiResource, url: string|string[], data: any, params?: RequestOptionsArgs): Observable 46 | post(resource: ApiResource, url: string|string[], data: any, params?: RequestOptionsArgs): Observable 47 | delete(resource: ApiResource, url: string|string[], params?: RequestOptionsArgs): Observable 48 | find(resource: ApiResource, id: number|string, params?: any): Observable 49 | findAll(resource: ApiResource, params?: any): Observable 50 | create(resource: ApiResource, data: any, params?: any): Observable 51 | update(resource: ApiResource, data: any, params?: any): Observable 52 | destroy(resource: ApiResource, id?: number|string|any, params?: any): Observable 53 | private _serialize(resource, data) 54 | private _deserialize(data) 55 | private _serializeParams(params) 56 | } 57 | -------------------------------------------------------------------------------- /karma-shim.js: -------------------------------------------------------------------------------- 1 | Error.stackTraceLimit = Infinity require('babel-register') require('phantomjs-polyfill') require('es6-shim') require('angular2/bundles/angular2-polyfills.js') require('angular2/testing') /* Ok, this is kinda crazy. We can use the the context method on require that webpack created in order to tell webpack what files we actually want to require or import. Below, context will be an function/object with file names as keys. using that regex we are saying look in client/app and find any file that ends with spec.js and get its path. By passing in true we say do this recursively */ var appContext = require.context('./test', true, /\.spec\.ts/) // get all the files, for each file, call the context function // that will require the file and load it up here. Context will // loop and require those spec files here appContext.keys().forEach(appContext) // Select BrowserDomAdapter. // see https://github.com/AngularClass/angular2-webpack-starter/issues/124 // Somewhere in the test setup var testing = require('angular2/testing') var browser = require('angular2/platform/testing/browser') testing.setBaseTestProviders(browser.TEST_BROWSER_PLATFORM_PROVIDERS, browser.TEST_BROWSER_APPLICATION_PROVIDERS) -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Wed Feb 17 2016 22:26:46 GMT-0800 (PST) 3 | require('babel-register') 4 | var preprocessors = {} 5 | var testEntry = './karma-shim.js' 6 | var webpackConfig = require('./webpack.config.babel') 7 | 8 | preprocessors[testEntry] = ['webpack', 'sourcemap'] 9 | 10 | module.exports = function(config) { 11 | config.set({ 12 | 13 | // base path that will be used to resolve all patterns (eg. files, exclude) 14 | basePath: './', 15 | 16 | 17 | // frameworks to use 18 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 19 | frameworks: ['jasmine'], 20 | 21 | 22 | // list of files / patterns to load in the browser 23 | files: [ 24 | {pattern: testEntry, watched: false}, 25 | {pattern: './test/*.spec.ts', watched: true, included: false} 26 | ], 27 | 28 | webpack: webpackConfig, 29 | 30 | // preprocess matching files before serving them to the browser 31 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 32 | preprocessors: preprocessors, 33 | 34 | webpackServer: { 35 | noInfo: true // please don't spam the console when running in karma! 36 | }, 37 | 38 | 39 | // test results reporter to use 40 | // possible values: 'dots', 'progress' 41 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 42 | reporters: ['spec'], 43 | 44 | 45 | // web server port 46 | port: 9876, 47 | 48 | 49 | // enable / disable colors in the output (reporters and logs) 50 | colors: true, 51 | 52 | 53 | // level of logging 54 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 55 | logLevel: config.LOG_INFO, 56 | 57 | 58 | // enable / disable watching file and executing tests whenever any file changes 59 | autoWatch: true, 60 | 61 | 62 | // start these browsers 63 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 64 | browsers: ['PhantomJS'], 65 | 66 | 67 | // Continuous Integration mode 68 | // if true, Karma captures browsers, runs the tests and exits 69 | singleRun: false, 70 | 71 | // Concurrency level 72 | // how many browser should be started simultaneous 73 | concurrency: Infinity 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-api", 3 | "version": "0.2.0", 4 | "description": "Api service for angular to work with REST resources", 5 | "main": "dist/angular2-api.js", 6 | "scripts": { 7 | "test": "NODE_ENV='test' karma start --single-run", 8 | "devTest": "NODE_ENV='test' karma start", 9 | "build": "NODE_ENV='production' webpack --progress --color", 10 | "tsd": "dts-generator --name angular2-api --project ./ --out dist/angular2-api.d.ts" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+ssh://git@github.com/MikaAK/angular2-api.git" 15 | }, 16 | "keywords": [ 17 | "angular2", 18 | "api-service", 19 | "api", 20 | "REST" 21 | ], 22 | "author": "Mika Kalathil", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/MikaAK/angular2-api/issues" 26 | }, 27 | "homepage": "https://github.com/MikaAK/angular2-api#readme", 28 | "devDependencies": { 29 | "@angular/core": "^2.0.0-rc.1", 30 | "@angular/http": "^2.0.0-rc.1", 31 | "babel-core": "^6.8.0", 32 | "babel-loader": "^6.2.3", 33 | "babel-preset-es2015": "^6.5.0", 34 | "babel-preset-stage-2": "^6.5.0", 35 | "babel-register": "^6.8.0", 36 | "dts-generator": "^1.6.3", 37 | "es6-promise": "^3.1.2", 38 | "es6-shim": "^0.35.0", 39 | "jasmine-core": "^2.4.1", 40 | "karma": "^0.13.21", 41 | "karma-cli": "^1.0.0", 42 | "karma-jasmine": "^1.0.2", 43 | "karma-phantomjs-launcher": "^1.0.0", 44 | "karma-sourcemap-loader": "^0.3.7", 45 | "karma-spec-reporter": "0.0.26", 46 | "karma-webpack": "^1.7.0", 47 | "phantomjs-polyfill": "^0.0.2", 48 | "phantomjs-prebuilt": "^2.1.7", 49 | "reflect-metadata": "^0.1.3", 50 | "rxjs": "^5.0.0-beta.6", 51 | "ts-loader": "^0.8.2", 52 | "typescript": "^1.8.10", 53 | "typings": "^0.8.0", 54 | "webpack": "^1.13.0", 55 | "zone.js": "^0.6.6" 56 | }, 57 | "peerDependencies": { 58 | "rxjs": "^5.0.0-beta.6", 59 | "@angular/core": "^2.0.0-rc.1", 60 | "@angular/http": "^2.0.0-rc.1" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/ApiConfig.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, Optional} from '@angular/core' 2 | import {RequestOptionsArgs} from '@angular/http' 3 | 4 | export interface ApiConfigArgs { 5 | basePath: string 6 | deserialize?: ((data: any|any[]) => any|any[]) 7 | serialize?: ((data: any|any[]) => any|any[]) 8 | serializeParams?: ((params: RequestOptionsArgs) => RequestOptionsArgs) 9 | } 10 | 11 | @Injectable() 12 | export abstract class AbstractApiConfig { 13 | public basePath: string 14 | abstract deserialize(data: any|any[]): any|any[] 15 | abstract serialize(data: any|any[]): any|any[] 16 | abstract serializeParams(params: RequestOptionsArgs): RequestOptionsArgs 17 | } 18 | 19 | @Injectable() 20 | export class ApiConfig implements AbstractApiConfig { 21 | public basePath: string 22 | 23 | constructor(@Optional() {basePath, deserialize, serialize, serializeParams}: ApiConfigArgs) { 24 | this.basePath = basePath 25 | 26 | if (deserialize) 27 | this.deserialize = deserialize 28 | 29 | if (serialize) 30 | this.serialize = serialize 31 | 32 | if (serializeParams) 33 | this.serializeParams = serializeParams 34 | } 35 | 36 | public deserialize(data: any|any[]): any|any[] { 37 | return data 38 | } 39 | 40 | public serialize(data: any|any[]): any|any[] { 41 | return data 42 | } 43 | 44 | public serializeParams(params: RequestOptionsArgs): RequestOptionsArgs { 45 | return params 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ApiResource.ts: -------------------------------------------------------------------------------- 1 | import {RequestOptionsArgs} from '@angular/http' 2 | 3 | export interface ApiResource { 4 | idAttribute?: string 5 | endpoint: string 6 | 7 | deserialize?(data: any): any 8 | serialize?(data: any): any 9 | serializeParams?(params: RequestOptionsArgs): any 10 | } 11 | -------------------------------------------------------------------------------- /src/ApiService.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, Optional} from '@angular/core' 2 | import {Http, RequestOptionsArgs, Response, Headers} from '@angular/http' 3 | import {ApiResource} from './ApiResource' 4 | import {ApiConfig} from './ApiConfig' 5 | import {Observable} from 'rxjs/Observable' 6 | import 'rxjs/add/operator/map' 7 | import 'rxjs/add/operator/catch' 8 | import 'rxjs/add/observable/throw' 9 | 10 | const removeSlashes = (url: string): string => { 11 | if (!url) 12 | return url 13 | 14 | if (url.startsWith('/')) 15 | url = url.slice(1, url.length) 16 | 17 | if (url.endsWith('/')) 18 | url = url.slice(0, url.length - 1) 19 | 20 | return url 21 | } 22 | 23 | const toJSON = (data: any): string => { 24 | try { 25 | return JSON.stringify(data) 26 | } catch(e) { 27 | return data 28 | } 29 | } 30 | 31 | var createParams = function(headers: Headers) { 32 | } 33 | 34 | const serializeParams = (params: any = {}) => { 35 | if (!params) 36 | params = {} 37 | 38 | let {headers} = params 39 | 40 | if (!headers) { 41 | params.headers = new Headers({'Content-Type': 'application/json'}) 42 | } else if (headers && !headers.get('Content-Type')) { 43 | headers.set('Content-Type', 'application/json') 44 | } 45 | 46 | return params 47 | } 48 | 49 | const deserializeResponse = (resp: Response) => { 50 | let contentType = resp && resp.headers && (resp.headers.get('content-type') || resp.headers.get('Content-Type')) 51 | 52 | if (!contentType) 53 | return resp 54 | 55 | if (/json/.test(contentType)) 56 | return resp.json() 57 | else if (/text/.test(contentType)) 58 | return resp.text() 59 | else if (/blob/.test(contentType)) 60 | return resp.blob() 61 | else 62 | return resp 63 | } 64 | 65 | const runTransformIfHas = (transformBase, method, data) => typeof transformBase[method] === 'function' ? transformBase[method](data) : data 66 | const resourceDeserialize = (resource) => (data) => runTransformIfHas(resource, 'deserialize', data) 67 | 68 | @Injectable() 69 | export class ApiService { 70 | constructor(private _http: Http, @Optional() private _config: ApiConfig) {} 71 | 72 | public createUrl(resource: ApiResource, url: string|string[]): string { 73 | let qUrl = String(Array.isArray(url) ? url.join('/') : url) 74 | 75 | return `${removeSlashes(this._config.basePath)}/${removeSlashes(resource.endpoint)}/${removeSlashes(qUrl)}` 76 | } 77 | 78 | public get(resource: ApiResource, url: string|string[], params?: RequestOptionsArgs): Observable { 79 | return this._http.get(this.createUrl(resource, url), this._serializeParams(resource, params)) 80 | .map(data => this._deserialize(data)) 81 | .map(resourceDeserialize(resource)) 82 | .catch(error => this._catchError(error)) 83 | } 84 | 85 | public put(resource: ApiResource, url: string|string[], data?: any, params?: RequestOptionsArgs): Observable { 86 | return this._http.put(this.createUrl(resource, url), this._serialize(resource, data), this._serializeParams(resource, params)) 87 | .map(data => this._deserialize(data)) 88 | .map(resourceDeserialize(resource)) 89 | .catch(error => this._catchError(error)) 90 | } 91 | 92 | public patch(resource: ApiResource, url: string|string[], data?: any, params?: RequestOptionsArgs): Observable { 93 | return this._http.patch(this.createUrl(resource, url), this._serialize(resource, data), this._serializeParams(resource, params)) 94 | .map(data => this._deserialize(data)) 95 | .map(resourceDeserialize(resource)) 96 | .catch(error => this._catchError(error)) 97 | } 98 | 99 | public post(resource: ApiResource, url: string|string[], data?: any, params?: RequestOptionsArgs): Observable { 100 | return this._http.post(this.createUrl(resource, url), this._serialize(resource, data), this._serializeParams(resource, params)) 101 | .map(data => this._deserialize(data)) 102 | .map(resourceDeserialize(resource)) 103 | .catch(error => this._catchError(error)) 104 | } 105 | 106 | public delete(resource: ApiResource, url: string|string[], params?: RequestOptionsArgs): Observable { 107 | return this._http.get(this.createUrl(resource, url), this._serializeParams(resource, params)) 108 | .map(data => this._deserialize(data)) 109 | .map(resourceDeserialize(resource)) 110 | .catch(error => this._catchError(error)) 111 | } 112 | 113 | public find(resource: ApiResource, id: number|string|any, params?) { 114 | if (typeof id === 'undefined') 115 | throw new Error('You must provide an id') 116 | 117 | return this.get(resource, id, params) 118 | } 119 | 120 | public findAll(resource: ApiResource, params?) { 121 | return this.get(resource, '', params) 122 | } 123 | 124 | public create(resource: ApiResource, data?, params?) { 125 | return this.post(resource, '', data, params) 126 | } 127 | 128 | public update(resource: ApiResource, data, params?) { 129 | let id = data[resource.idAttribute || 'id'], 130 | url = id ? id : '' 131 | 132 | return this.put(resource, url, data, params) 133 | } 134 | 135 | public destroy(resource: ApiResource, id?: number|string, params?) { 136 | if (typeof id === 'object') { 137 | params = id 138 | id = null 139 | } 140 | 141 | return this.delete(resource, id ? id : '', params) 142 | } 143 | 144 | private _serialize(resource: ApiResource, data): any { 145 | return toJSON(runTransformIfHas(this._config, 'serialize', runTransformIfHas(resource, 'serialize', data))) 146 | } 147 | 148 | private _deserialize(data: any): any|any[] { 149 | return runTransformIfHas(this._config, 'deserialize', deserializeResponse(data)) 150 | } 151 | 152 | private _serializeParams(resource, params): RequestOptionsArgs { 153 | params = runTransformIfHas(this._config, 'serializeParams', serializeParams(params)) 154 | 155 | return runTransformIfHas(resource, 'serializeParams', params) 156 | } 157 | 158 | private _catchError(error) { 159 | if (error instanceof Error) 160 | throw error 161 | else 162 | return Observable.throw(this._deserialize(error)) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {ApiService} from './ApiService' 2 | import {ApiResource} from './ApiResource' 3 | import {ApiConfig} from './ApiConfig' 4 | 5 | export {ApiService, ApiResource, ApiConfig} 6 | -------------------------------------------------------------------------------- /test/ApiService.spec.ts: -------------------------------------------------------------------------------- 1 | // import {provide} from 'angular2/core' 2 | // import {it, inject, beforeEachProviders} from 'angular2/testing' 3 | // import {MockBackend} from 'angular2/http/testing' 4 | // import {Http, BaseRequestOptions, Response, Headers, ResponseOptions} from 'angular2/http' 5 | // import {ApiService} from '../src/ApiService.ts' 6 | // import {ApiResource} from '../src/ApiResource' 7 | // import {Observable} from 'rxjs/Observable' 8 | 9 | // const mockResponse = (backend, config) => backend.connections.subscribe(c => c.mockRespond(new Response(new ResponseOptions(config)))) 10 | // const TEST_DATA = { 11 | // test: 'data' 12 | // } 13 | 14 | // const testHttpMethod = (mType, cb: ((api: ApiService, resource: ApiResource) => Observable)) => { 15 | // it(`takes url and params and creates ${mType.toUpperCase()} request`, inject([ApiService, MockBackend, ExampleApi], (api: ApiService, backend: MockBackend, example: ExampleApi) => { 16 | // mockResponse(backend, {body: JSON.stringify(TEST_DATA)}) 17 | 18 | // cb(api, example) 19 | // .map(data => data.json()) 20 | // .subscribe(posts => expect(posts).toEqual(TEST_DATA)) 21 | // })) 22 | // } 23 | 24 | // class ExampleApi implements ApiResource { 25 | // endpoint = 'example' 26 | // } 27 | 28 | // describe('ApiService', () => { 29 | // beforeEachProviders(() => [ 30 | // MockBackend, 31 | // ApiService, 32 | // BaseRequestOptions, 33 | // ExampleApi, 34 | // provide(Http, { 35 | // useFactory: (backend, options) => new Http(backend, options), 36 | // deps: [MockBackend, BaseRequestOptions] 37 | // }) 38 | // ]) 39 | 40 | // describe('#get', () => testHttpMethod('get', (api, resource) => api.get(resource, 'test'))) 41 | // describe('#post', () => testHttpMethod('post', (api, resource) => api.post(resource, 'test', TEST_DATA))) 42 | // describe('#patch', () => testHttpMethod('patch', (api, resource) => api.patch(resource, 'test', TEST_DATA))) 43 | // describe('#put', () => testHttpMethod('put', (api, resource) => api.put(resource, 'test', TEST_DATA))) 44 | // describe('#delete', () => testHttpMethod('delete', (api, resource) => api.delete(resource, 'test'))) 45 | 46 | // describe('#find', () => { 47 | 48 | // }) 49 | 50 | // describe('#findAll', () => { 51 | 52 | // }) 53 | 54 | // describe('#create', () => { 55 | 56 | // }) 57 | 58 | // describe('#update', () => { 59 | 60 | // }) 61 | 62 | // describe('#destroy', () => { 63 | 64 | // }) 65 | // }) 66 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true 8 | }, 9 | "exclude": [ 10 | "node_modules", 11 | "typings/browser.d.ts", 12 | "typings/browser" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-api", 3 | "dependencies": {}, 4 | "devDependencies": {}, 5 | "ambientDependencies": {}, 6 | "ambientDevDependencies": { 7 | "jasmine": "github:DefinitelyTyped/DefinitelyTyped/jasmine/jasmine.d.ts#dd638012d63e069f2c99d06ef4dcc9616a943ee4" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import {DefinePlugin} from 'webpack' 3 | 4 | const CONTEXT = path.resolve(__dirname), 5 | {NODE_ENV} = process.env 6 | 7 | var createPath = function(nPath) { 8 | return path.resolve(CONTEXT, nPath) 9 | } 10 | 11 | var config = { 12 | context: CONTEXT, 13 | entry: './src/index.ts', 14 | 15 | output: { 16 | path: createPath('dist'), 17 | library: 'angular2-api', 18 | libraryTarget: 'umd', 19 | filename: 'angular2-api.js' 20 | }, 21 | 22 | plugins: [ 23 | new DefinePlugin({ 24 | __DEV__: NODE_ENV === 'development' || NODE_ENV === 'test' 25 | }) 26 | ], 27 | 28 | module: { 29 | loaders: [{ 30 | test: /\.ts/, 31 | loader: 'babel!ts', 32 | include: [createPath('src'), createPath('test')], 33 | exclude: [createPath('node_modules')] 34 | }] 35 | }, 36 | 37 | externals: [ 38 | '@angular/core', 39 | '@angular/http', 40 | 'rxjs/add/operator/map', 41 | 'rxjs/add/operator/mergeMap', 42 | 'rxjs/add/operator/catch', 43 | 'rxjs/add/observable/throw', 44 | 'rxjs/Observable', 45 | 'rxjs/observable' 46 | ], 47 | 48 | resolve: { 49 | extensions: ['.ts', '.js',''] 50 | } 51 | } 52 | 53 | module.exports = config 54 | --------------------------------------------------------------------------------