├── .codeclimate.yml ├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.MD ├── e2e ├── bundle.e2e.ts └── demo │ ├── config.js │ ├── index.html │ ├── server.js │ └── src │ ├── app.component.ts │ ├── app.module.ts │ └── main.ts ├── karma.conf.js ├── package.json ├── protractor.conf.js ├── rollup.config.js ├── rollup.config.umd.js ├── scripts ├── build.js └── release.js ├── src ├── http │ ├── helpers │ │ ├── getHttpHeadersOrInit.spec.ts │ │ ├── getHttpHeadersOrInit.ts │ │ ├── getHttpOptions.spec.ts │ │ ├── getHttpOptions.ts │ │ ├── getHttpOptionsAndIdx.spec.ts │ │ ├── getHttpOptionsAndIdx.ts │ │ ├── getHttpOptionsIdx.spec.ts │ │ ├── getHttpOptionsIdx.ts │ │ └── index.ts │ ├── high-level-api.spec.ts │ ├── http-interceptor.service.spec.ts │ ├── http-interceptor.service.ts │ ├── http-interceptor.ts │ ├── index.ts │ ├── interceptable-http-proxy.service.spec.ts │ ├── interceptable-http-proxy.service.ts │ ├── interceptable-http.spec.ts │ ├── interceptable-http.ts │ ├── interceptable-store.spec.ts │ ├── interceptable-store.ts │ ├── interceptable.ts │ ├── module.ts │ ├── providers.ts │ └── util.ts ├── index.ts ├── polyfills.ts ├── rxjs.ts ├── test.ts └── typings.d.ts ├── tsconfig.es2015.json ├── tsconfig.es5.json ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── yarn.lock /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | duplication: 3 | enabled: true 4 | config: 5 | languages: 6 | - javascript 7 | tslint: 8 | enabled: true 9 | fixme: 10 | enabled: true 11 | ratings: 12 | paths: 13 | - "src/**/*.ts" 14 | exclude_paths: [] 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | 3 | # Node 4 | node_modules/* 5 | npm-debug.log 6 | 7 | # TypeScript 8 | dist 9 | 10 | # JetBrains 11 | .idea 12 | .project 13 | .settings 14 | .idea/* 15 | *.iml 16 | 17 | # Windows 18 | Thumbs.db 19 | Desktop.ini 20 | 21 | # Mac 22 | .DS_Store 23 | **/.DS_Store 24 | 25 | typings 26 | coverage 27 | .coveralls.yml 28 | 29 | # NG2 AOT 30 | /compiled 31 | *.metadata.json 32 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules 3 | npm-debug.log 4 | 5 | # DO NOT IGNORE TYPESCRIPT FILES FOR NPM 6 | # TypeScript 7 | # *.js 8 | # *.map 9 | # *.d.ts 10 | 11 | # JetBrains 12 | .idea 13 | .project 14 | .settings 15 | .idea/* 16 | *.iml 17 | 18 | # Windows 19 | Thumbs.db 20 | Desktop.ini 21 | 22 | # Mac 23 | .DS_Store 24 | **/.DS_Store 25 | 26 | # Project 27 | src 28 | test 29 | coverage 30 | typings 31 | compiled 32 | karma.conf.js 33 | webpack.config.js 34 | tslint.json 35 | tsconfig*.json 36 | rollup.config.js 37 | travis_after_all.py 38 | .codeclimate.yml 39 | .travis.yml 40 | .to_export_back 41 | *.deb 42 | scripts 43 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: node_js 4 | cache: 5 | directories: 6 | - node_modules 7 | notifications: 8 | email: false 9 | node_js: 10 | - node 11 | # - "6" # Excluding v6 due to issue with Yarn installation https://github.com/npm/npm/issues/14626 12 | branches: 13 | only: 14 | - master 15 | before_install: 16 | - sudo apt-get update 17 | - sudo apt-get install -y libappindicator1 fonts-liberation 18 | - wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb 19 | - sudo dpkg -i google-chrome*.deb 20 | - export CHROME_BIN=/usr/bin/google-chrome 21 | - export DISPLAY=:99.0 22 | - sh -e /etc/init.d/xvfb start 23 | - npm i -g yarn 24 | install: 25 | - yarn 26 | - yarn e2e:update 27 | script: 28 | - yarn ci 29 | - yarn e2e 30 | after_success: 31 | - 'curl -Lo travis_after_all.py https://git.io/travis_after_all' 32 | - python travis_after_all.py 33 | - export $(cat .to_export_back) &> /dev/null 34 | - yarn semantic-release 35 | - yarn test:report 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Alex Malkevich 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 | # ng-http-interceptor 2 | 3 | > Http Interceptor library for Angular 4 | 5 | *Previously was called `ng2-http-interceptor`* 6 | 7 | [![Travis CI](https://img.shields.io/travis/gund/ng-http-interceptor/master.svg?maxAge=2592000)](https://travis-ci.org/gund/ng-http-interceptor) 8 | [![Coverage](https://img.shields.io/codecov/c/github/gund/ng-http-interceptor.svg?maxAge=2592000)](https://codecov.io/gh/gund/ng-http-interceptor) 9 | [![Code Climate](https://img.shields.io/codeclimate/github/gund/ng-http-interceptor.svg?maxAge=2592000)](https://codeclimate.com/github/gund/ng-http-interceptor) 10 | [![Npm](https://img.shields.io/npm/v/ng-http-interceptor.svg?maxAge=2592000)](https://badge.fury.io/js/ng-http-interceptor) 11 | [![Npm Downloads](https://img.shields.io/npm/dt/ng2-http-interceptor.svg?maxAge=2592000)](https://www.npmjs.com/package/ng-http-interceptor) 12 | [![Licence](https://img.shields.io/npm/l/ng-http-interceptor.svg?maxAge=2592000)](https://github.com/gund/ng-http-interceptor/blob/master/LICENSE) 13 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 14 | 15 | Version 4.x.x supports Angular 5 (`ng-http-interceptor@^4.0.0`) 16 | 17 | Version 3.x.x supports Angular 4 (`ng-http-interceptor@^3.0.0`) 18 | 19 | Version 2.x.x supports Angular 2 (`ng-http-interceptor@^2.0.0`) 20 | 21 | ## Features 22 | 23 | * Registering interceptors globally 24 | * Separate interceptors for requests and responses 25 | * Attach interceptors for specific urls via strings or RegExp's 26 | * Remove specific/all interceptor(s) 27 | * Modify requests (even url) from request interceptors 28 | * Cancel requests from request interceptors 29 | * Modify responses from response interceptors 30 | * Interceptor Service is not coupled with Http Service 31 | * Choose between overriding original Http Service or keep it and still use interceptors 32 | * Comprehensive type assistance for your interceptor functions 33 | * Supports AOT compilation with shipped _*.metadata.json_ files 34 | * UMD builds in _dist/bundles_ folder ready to use in Browsers 35 | * Simple http data extraction and manipulation with [Helpers Functions](#helpers-functions-since-v130) 36 | * Sharing context object between all interceptors 37 | 38 | ## Table of Contents 39 | 40 | * [Features](#features) 41 | * [Prerequisites](#prerequisites) 42 | * [Installation](#installation) 43 | * [Usage](#usage) 44 | * [Documentation](#documentation) 45 | * [Development](#development) 46 | 47 | ## Prerequisites 48 | 49 | This library uses `Proxy` from ES6 spec so if you need to support browsers 50 | that are ES5 compatible include [proxy-polyfill](https://github.com/GoogleChrome/proxy-polyfill). 51 | 52 | Library uses `tslib` (standard Typescript runtime library) so please make sure 53 | you have this module installed via `npm`. 54 | 55 | ## Installation 56 | 57 | To install this library, run: 58 | 59 | ```bash 60 | $ npm install ng-http-interceptor --save 61 | ``` 62 | 63 | ## Usage 64 | 65 | #### Case #1 66 | 67 | Import `HttpInterceptorModule` to your application module. 68 | This will override original `Http` Service and all requests will be intercepted. 69 | 70 | #### Case #2 71 | 72 | Import as `HttpInterceptorModule.noOverrideHttp()` to keep original `Http` Service 73 | and use `InterceptableHttp` for requests to be intercepted. 74 | 75 | ### Example use-case 76 | 77 | You can use `InterceptableHttp` for your requests in case #1 and #2 78 | and `Http` if you chose to override it (case #1 only): 79 | ```ts 80 | constructor(http: Http, httpInterceptor: HttpInterceptorService) { 81 | httpInterceptor.request().addInterceptor((data, method) => { 82 | console.log(method, data); 83 | return data; 84 | }); 85 | 86 | httpInterceptor.response().addInterceptor((res, method) => { 87 | return res.do(r => console.log(method, r)); 88 | }); 89 | 90 | this.http.get('/') 91 | .map(r => r.text()) 92 | .subscribe(console.log); 93 | } 94 | ``` 95 | 96 | In this setup every request and response will be logged to the console. 97 | You can also cancel request by returning `false` value (that coerce to boolean false) 98 | from one of registered _request_ interceptors. 99 | You can return `Observable` from _request_ interceptors to do some async job. 100 | 101 | You can find in-depth explanation of internal concepts here: https://goo.gl/GU9VWo 102 | Also if you want to play with it check this [repo](https://github.com/gund/angular2-http-interceptor-test). 103 | Or check this [plnkr demo](https://plnkr.co/edit/qTTdPl8EggQleTfTSbch). 104 | 105 | ## Documentation 106 | 107 | All and every interception setup is made by `HttpInterceptorService` service. 108 | Inject this service in place where you want to manage interceptors. 109 | 110 | ### Public API 111 | 112 | **HttpInterceptorService** 113 | ```ts 114 | HttpInterceptorService: { 115 | request(url?: string|RegExp): Interceptable, 116 | response(url?: string|RegExp): Interceptable 117 | } 118 | ``` 119 | 120 | See [src/http/http-interceptor.ts](./src/http/http-interceptor.ts#L8) for full reference 121 | 122 | _Description_: Methods will determine when to call interceptor - before 123 | request (`request()`) or after response (`response()`). 124 | You can also specify url filtering (`string|RegExp`) which will indicate 125 | when interceptor must be triggered depending on url. 126 | By default all interceptors fall under `'/'` url key which means every 127 | interceptor registered that way will be triggered despite of actual url. 128 | 129 | **Interceptable** 130 | ```ts 131 | Interceptable: { 132 | addInterceptor(interceptorFn: Interceptor): Interceptable, 133 | removeInterceptor(interceptorFn: Interceptor): Interceptable, 134 | clearInterceptors(interceptorFns?: Interceptor[]): Interceptable 135 | } 136 | ``` 137 | 138 | See [src/http/interceptable.ts](./src/http/interceptable.ts#L1) for full reference 139 | 140 | _Description_: This object will help you manage interceptors with 141 | respect to your selected configuration (url filtering). 142 | 143 | **Interceptor** 144 | ```ts 145 | Interceptor { 146 | (data: T, method: string, ctx?: any): D; 147 | } 148 | ``` 149 | 150 | See [src/http/interceptable.ts](./src/http/interceptable.ts#L7) for full reference 151 | 152 | _Description_: This is generic type of interceptor - which is a plain old JavaScript function. 153 | You will be dealing with specific one to satisfy it's criteria: 154 | * `Interceptor` - for **request** interceptors 155 | Function will get an array of parameters with which call on `Http` 156 | was made + method name as string (`get`, `post`, `delete`...) 157 | and should return array of the same structure or `false` to cancel request. 158 | * `Interceptor, Observable>` - for **response** interceptors 159 | Function will get Observable + method name as string (`get`, `post`, `delete`...) 160 | and should return same or new Observable but with type Response 161 | (this is made specifically to prevent other code being broken 162 | because response was intercepted and structure changed) 163 | 164 | #### Helpers Functions (since v1.3.0) 165 | 166 | There are a bunch of helper functions added to simplify some common 167 | work with _data_ array (for ex. getting `RequestOptions` or even `Headers`). 168 | 169 | **getHttpHeadersOrInit()** 170 | ```ts 171 | function getHttpHeadersOrInit(data: any[], method: string): Headers; 172 | ``` 173 | 174 | See [src/http/helpers/getHttpHeadersOrInit.ts](./src/http/helpers/getHttpHeadersOrInit.ts) for full reference 175 | 176 | _Description_: Gets `Headers` from _data_ array. 177 | If no `RequestOptions` found - creates it and updates original _data_ array. 178 | If no `Headers` found - creates it and sets to `RequestOptions`. 179 | 180 | Exmaple how to add custom headers to requests: 181 | ```ts 182 | httpInterceptor.request().addInterceptor((data, method) => { 183 | const headers = getHttpHeadersOrInit(data, method); 184 | headers.set('X-Custom-Header', 'value'); 185 | return data; 186 | }); 187 | ``` 188 | 189 | **getHttpOptionsAndIdx()** 190 | ```ts 191 | function getHttpOptionsAndIdx( 192 | data: any[], 193 | method: string, 194 | alwaysOriginal?: boolean 195 | ): { 196 | options: RequestOptions; 197 | idx: number; 198 | }; 199 | ``` 200 | 201 | See [src/http/helpers/getHttpOptionsAndIdx.ts](./src/http/helpers/getHttpOptionsAndIdx.ts) for full reference 202 | 203 | _Description_: Gets `RequestOptions` and it's index location in _data_ array. 204 | If no options found and `alwaysOriginal = false` - creates new `RequestOptions` 205 | but does NOT set it back on _data_ array. 206 | * Param `alwaysOriginal` is `false` by default. 207 | 208 | **getHttpOptions()** 209 | ```ts 210 | function getHttpOptions( 211 | data: any[], 212 | method: string, 213 | alwaysOriginal?: boolean): RequestOptions; 214 | ``` 215 | 216 | See [src/http/helpers/getHttpOptions.ts](./src/http/helpers/getHttpOptions.ts) for full reference 217 | 218 | _Description_: Gets http `RequestOptions` from data array. 219 | If no options found and `alwaysOriginal = false` - returns new `RequestOptions` 220 | but does NOT set it back on _data_ array. 221 | * Param `alwaysOriginal` is `false` by default. 222 | 223 | **getHttpOptionsIdx()** 224 | ```ts 225 | function getHttpOptionsIdx(method: string): number; 226 | ``` 227 | 228 | See [src/http/helpers/getHttpOptionsIdx.ts](./src/http/helpers/getHttpOptionsIdx.ts) for full reference 229 | 230 | _Description_: Gets index of `RequestOptions` in http data array for specified `method`. 231 | 232 | 233 | ## Development 234 | 235 | To generate all `*.js`, `*.js.map`, `*.d.ts` and `*.metadata.json` files: 236 | 237 | ```bash 238 | $ npm run build 239 | ``` 240 | 241 | To lint all `*.ts` files: 242 | 243 | ```bash 244 | $ npm run lint 245 | ``` 246 | 247 | To run unit tests: 248 | 249 | ```bash 250 | $ npm test 251 | ``` 252 | 253 | ## License 254 | 255 | MIT © [Alex Malkevich](malkevich.alex@gmail.com) 256 | -------------------------------------------------------------------------------- /e2e/bundle.e2e.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, ElementArrayFinder, ElementFinder } from 'protractor'; 2 | 3 | describe('Umd bundle', () => { 4 | 5 | beforeEach(() => browser.get('http://localhost:8080')); 6 | 7 | it('should have title', () => { 8 | expect(browser.getTitle()).toBe('Ng Http Demo' as any); 9 | }); 10 | 11 | describe('Ng App', () => { 12 | beforeEach(() => browser.wait(() => browser.element(by.css('.loaded-app')).isPresent())); 13 | 14 | describe('controls', () => { 15 | it('should be present', () => { 16 | const h2 = browser.element(by.css('h2')); 17 | expect(h2.getText()).toBe('Ng2 Http Interceptor Demo' as any); 18 | 19 | const input = browser.element(by.css('input[type="text"]')); 20 | expect(input.isPresent()).toBeTruthy(); 21 | 22 | const button = browser.element(by.css('button')); 23 | expect(button.getText()).toBe('Make request' as any); 24 | }); 25 | }); 26 | 27 | describe('interception flow', () => { 28 | let input: ElementFinder 29 | , btn: ElementFinder 30 | , requests: ElementArrayFinder 31 | , error: ElementFinder 32 | , response: ElementFinder; 33 | 34 | beforeEach(() => { 35 | input = browser.element(by.css('input[type="text"]')); 36 | btn = browser.element(by.css('button')); 37 | requests = browser.element(by.css('.requests')).all(by.css('p')); 38 | error = browser.element(by.css('.error')); 39 | response = browser.element(by.css('.response')); 40 | }); 41 | 42 | it('should make request to `/` and render response by default', () => { 43 | const request = requests.last(); 44 | expect(request.getText()).toMatch(/get request to \//i); 45 | expect(response.getText()).toMatch(/DOCTYPE/); 46 | }); 47 | 48 | it('should make request to `/none` and render error', () => { 49 | input.sendKeys('/none'); 50 | btn.click(); 51 | browser.waitForAngular(); 52 | 53 | expect(requests.count()).toBe(2 as any); 54 | expect(requests.last().getText()).toMatch(/get request to \/none/i); 55 | 56 | expect(error.isPresent()).toBeTruthy(); 57 | expect(error.getText()).toMatch(/404/); 58 | }); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /e2e/demo/config.js: -------------------------------------------------------------------------------- 1 | System.config({ 2 | //use typescript for compilation 3 | transpiler: 'typescript', 4 | //typescript compiler options 5 | typescriptOptions: { 6 | emitDecoratorMetadata: true 7 | }, 8 | paths: { 9 | 'npm:': 'https://unpkg.com/' 10 | }, 11 | //map tells the System loader where to look for things 12 | map: { 13 | 14 | 'app': './src', 15 | 'ng2-http-interceptor': './dist/bundles/ng-http-interceptor.umd.js', 16 | 17 | '@angular/core': 'npm:@angular/core@5.0.0/bundles/core.umd.js', 18 | '@angular/common': 'npm:@angular/common@5.0.0/bundles/common.umd.js', 19 | '@angular/compiler': 'npm:@angular/compiler@5.0.0/bundles/compiler.umd.js', 20 | '@angular/platform-browser': 'npm:@angular/platform-browser@5.0.0/bundles/platform-browser.umd.js', 21 | '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic@5.0.0/bundles/platform-browser-dynamic.umd.js', 22 | '@angular/http': 'npm:@angular/http@5.0.0/bundles/http.umd.js', 23 | '@angular/router': 'npm:@angular/router@5.0.0/bundles/router.umd.js', 24 | '@angular/forms': 'npm:@angular/forms@5.0.0/bundles/forms.umd.js', 25 | 26 | '@angular/core/testing': 'npm:@angular/core@5.0.0/bundles/core-testing.umd.js', 27 | '@angular/common/testing': 'npm:@angular/common@5.0.0/bundles/common-testing.umd.js', 28 | '@angular/compiler/testing': 'npm:@angular/compiler@5.0.0/bundles/compiler-testing.umd.js', 29 | '@angular/platform-browser/testing': 'npm:@angular/platform-browser@5.0.0/bundles/platform-browser-testing.umd.js', 30 | '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic@5.0.0/bundles/platform-browser-dynamic-testing.umd.js', 31 | '@angular/http/testing': 'npm:@angular/http@5.0.0/bundles/http-testing.umd.js', 32 | '@angular/router/testing': 'npm:@angular/router@5.0.0/bundles/router-testing.umd.js', 33 | 34 | 'rxjs': 'npm:rxjs', 35 | 'typescript': 'npm:typescript@2.2.1/lib/typescript.js' 36 | }, 37 | //packages defines our app package 38 | packages: { 39 | app: { 40 | main: './main.ts', 41 | defaultExtension: 'ts' 42 | }, 43 | rxjs: { 44 | defaultExtension: 'js' 45 | } 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /e2e/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ng Http Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | loading... 20 | 21 | 22 | -------------------------------------------------------------------------------- /e2e/demo/server.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | 4 | var app = express(); 5 | 6 | app.use('/dist', express.static(path.join(__dirname, '..' , '..', 'dist'))); 7 | app.use('/', express.static(__dirname)); 8 | 9 | app.listen(8080, () => console.log('Running on 8080')); 10 | -------------------------------------------------------------------------------- /e2e/demo/src/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Http } from '@angular/http'; 3 | import { HttpInterceptorService } from 'ng2-http-interceptor'; 4 | import 'rxjs/add/operator/do'; 5 | 6 | @Component({ 7 | // tslint:disable-next-line:component-selector-prefix 8 | selector: 'my-app', 9 | template: ` 10 |
11 |

Ng2 Http Interceptor Demo

12 |

13 | 14 | 15 |

16 |

Requests log:

17 |
18 |

19 | #{{ i + 1 }}: {{ request.method | uppercase }} request to {{ request.url }} 20 |

21 |
22 |

Response:

23 |

Error: {{ error }}

24 |
{{ res | json }}
25 |
26 | `, 27 | }) 28 | export class AppComponent implements OnInit { 29 | requests = []; 30 | res = null; 31 | error = null; 32 | 33 | constructor( 34 | private http: Http, 35 | private httpInterceptor: HttpInterceptorService 36 | ) { 37 | this.httpInterceptor.request().addInterceptor((data, method) => { 38 | this.error = null; 39 | this.requests.push({ 40 | method: method, 41 | url: data[0] 42 | }); 43 | 44 | return data; 45 | }); 46 | 47 | this.httpInterceptor.response().addInterceptor( 48 | res => res.do(null, e => this.error = e)); 49 | } 50 | 51 | ngOnInit() { 52 | this.makeRequest(); 53 | } 54 | 55 | makeRequest(url = '/') { 56 | this.http.get(url).subscribe(r => this.res = r.text()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /e2e/demo/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule } from '@angular/http'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { NgModule } from '@angular/core'; 4 | import { HttpInterceptorModule } from 'ng2-http-interceptor'; 5 | import { AppComponent } from './app.component'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | BrowserModule, 10 | HttpModule, 11 | HttpInterceptorModule 12 | ], 13 | declarations: [AppComponent], 14 | bootstrap: [AppComponent] 15 | }) 16 | export class AppModule { } 17 | -------------------------------------------------------------------------------- /e2e/demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; 2 | import {AppModule} from './app.module'; 3 | 4 | platformBrowserDynamic().bootstrapModule(AppModule); 5 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | var harmony_flags = '--js-flags="' + [ 6 | '--harmony-arrow-functions', 7 | '--harmony-classes', 8 | '--harmony-computed-property-names', 9 | '--harmony-spreadcalls', 10 | ].join(' ') + '"'; 11 | 12 | var configuration = { 13 | basePath: '', 14 | frameworks: ['jasmine'], 15 | plugins: [ 16 | require('karma-jasmine'), 17 | require('karma-chrome-launcher'), 18 | require('karma-remap-istanbul'), 19 | require('karma-sourcemap-loader'), 20 | require('karma-webpack') 21 | ], 22 | webpack: require('./webpack.config.js'), 23 | webpackMiddleware: { 24 | noInfo: true, // Hide webpack output because its noisy. 25 | stats: { // Also prevent chunk and module display output, cleaner look. Only emit errors. 26 | assets: false, 27 | colors: true, 28 | version: false, 29 | hash: false, 30 | timings: false, 31 | chunks: false, 32 | chunkModules: false 33 | } 34 | }, 35 | files: [ 36 | { pattern: './src/test.ts', watched: false } 37 | ], 38 | preprocessors: { 39 | './src/test.ts': ['webpack', 'sourcemap'] 40 | }, 41 | mime: { 42 | 'text/x-typescript': ['ts', 'tsx'] 43 | }, 44 | remapIstanbulReporter: { 45 | reports: { 46 | html: 'coverage', 47 | lcovonly: './coverage/coverage.lcov', 48 | json: './coverage/coverage.json' 49 | }, 50 | remapOptions: { 51 | exclude: /(test|polyfills|rxjs).ts$/ 52 | } 53 | }, 54 | reporters: ['progress', 'karma-remap-istanbul'], 55 | port: 9876, 56 | colors: true, 57 | logLevel: config.LOG_INFO, 58 | autoWatch: true, 59 | browsers: ['DebuggableChrome'], 60 | singleRun: false, 61 | 62 | // browser for travis-ci 63 | customLaunchers: { 64 | DebuggableChrome: { 65 | base: 'Chrome', 66 | flags: ['--remote-debugging-port=9222'] 67 | }, 68 | Chrome_travis_ci: { 69 | base: 'Chrome', 70 | flags: ['--no-sandbox', harmony_flags] 71 | } 72 | } 73 | }; 74 | 75 | if (process.env.TRAVIS) { 76 | configuration.browsers = ['Chrome_travis_ci']; 77 | } 78 | 79 | config.set(configuration); 80 | }; 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-http-interceptor", 3 | "version": "0.0.0-semantically-relesed", 4 | "scripts": { 5 | "build": "node scripts/build.js", 6 | "check-coverage": "istanbul check-coverage --functions 75 --lines 75 --branches 75 --statements 75", 7 | "ci": "npm run lint && npm run test:single && npm run check-coverage", 8 | "commit": "git-cz", 9 | "lint": "tslint src/**/*.ts", 10 | "prepublish": "npm run build", 11 | "test": "karma start", 12 | "test:single": "npm run test -- --single-run", 13 | "test:report": "cd coverage && codecov", 14 | "presemantic-release": "node scripts/release.js", 15 | "semantic-release": "cd dist && semantic-release pre && npm publish && semantic-release post", 16 | "e2e": "npm-run-all -p -r e2e:server e2e:protractor", 17 | "e2e:server": "node e2e/demo/server.js", 18 | "e2e:protractor": "protractor", 19 | "e2e:update": "webdriver-manager update" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/gund/ng-http-interceptor" 24 | }, 25 | "author": { 26 | "name": "Alex Malkevich", 27 | "email": "malkevich.alex@gmail.com" 28 | }, 29 | "keywords": [ 30 | "ng", 31 | "ng2", 32 | "angular", 33 | "angular2", 34 | "http", 35 | "interceptor", 36 | "library" 37 | ], 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/gund/ng-http-interceptor/issues" 41 | }, 42 | "main": "./dist/bundles/ng-http-interceptor.umd.js", 43 | "module": "./dist/bundles/ng-http-interceptor.es5.js", 44 | "es2015": "./dist/bundles/ng-http-interceptor.es2015.js", 45 | "typings": "./dist/ng-http-interceptor.d.ts", 46 | "peerDependencies": { 47 | "@angular/core": "^5.0.0", 48 | "@angular/http": "^5.0.0", 49 | "rxjs": "^5.0.0", 50 | "tslib": "^1.0.0" 51 | }, 52 | "devDependencies": { 53 | "@angular/common": "^5.0.0", 54 | "@angular/compiler": "^5.0.0", 55 | "@angular/compiler-cli": "^5.0.0", 56 | "@angular/core": "^5.0.0", 57 | "@angular/forms": "^5.0.0", 58 | "@angular/http": "^5.0.0", 59 | "@angular/platform-browser": "^5.0.0", 60 | "@angular/platform-browser-dynamic": "^5.0.0", 61 | "@angular/platform-server": "^5.0.0", 62 | "@angular/router": "^5.0.0", 63 | "@types/jasmine": "^2.5.45", 64 | "angular2-template-loader": "^0.6.2", 65 | "awesome-typescript-loader": "^3.1.2", 66 | "codecov": "^1.0.1", 67 | "codelyzer": "^3.0.0-beta.4", 68 | "commitizen": "^2.9.6", 69 | "copy-dir": "^0.3.0", 70 | "copyfiles": "^1.2.0", 71 | "core-js": "^2.4.1", 72 | "cz-conventional-changelog": "^2.0.0", 73 | "express": "^4.15.2", 74 | "ghooks": "^2.0.0", 75 | "istanbul": "^0.4.5", 76 | "jasmine-core": "2.5.2", 77 | "jasmine-spec-reporter": "^3.2.0", 78 | "json-loader": "^0.5.4", 79 | "karma": "^1.5.0", 80 | "karma-chrome-launcher": "^2.0.0", 81 | "karma-cli": "^1.0.1", 82 | "karma-jasmine": "^1.1.0", 83 | "karma-remap-istanbul": "^0.6.0", 84 | "karma-sourcemap-loader": "^0.3.7", 85 | "karma-webpack": "^2.0.2", 86 | "npm-run-all": "^4.0.2", 87 | "postcss-loader": "^1.3.3", 88 | "protractor": "^5.2.0", 89 | "put-cli": "^0.1.0", 90 | "raw-loader": "^0.5.1", 91 | "rimraf": "^2.6.1", 92 | "rollup": "^0.41.6", 93 | "rollup-globals-regex": "^0.0.1", 94 | "rollup-plugin-node-resolve": "^2.0.0", 95 | "rxjs": "^5.3.0", 96 | "semantic-release": "^6.3.2", 97 | "source-map": "^0.5.6", 98 | "source-map-loader": "^0.2.0", 99 | "sourcemap-istanbul-instrumenter-loader": "^0.2.0", 100 | "ts-node": "^3.3.0", 101 | "tslib": "^1.6.0", 102 | "tslint": "^4.5.1", 103 | "tslint-loader": "^3.4.3", 104 | "typescript": "~2.4.0", 105 | "typings": "^2.1.0", 106 | "uglifyjs": "^2.4.10", 107 | "url-loader": "^0.5.8", 108 | "webpack": "^2.6.1", 109 | "webpack-dev-server": "^2.4.5", 110 | "webpack-md5-hash": "^0.0.5", 111 | "webpack-merge": "^4.1.0", 112 | "webpack-sources": "^0.2.3", 113 | "zone.js": "^0.8.11" 114 | }, 115 | "engines": { 116 | "node": ">=6.0.0" 117 | }, 118 | "config": { 119 | "ghooks": { 120 | "pre-commit": "echo \"Verifying commit...\" && npm run ci" 121 | }, 122 | "commitizen": { 123 | "path": "./node_modules/cz-conventional-changelog" 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | require('ts-node').register({ 2 | compilerOptions: { 3 | module: 'commonjs' 4 | } 5 | }); 6 | 7 | var path = require('path'); 8 | 9 | exports.config = { 10 | baseUrl: 'http://localhost:8080/', 11 | 12 | specs: [ 13 | path.resolve('e2e', '*.e2e.ts') 14 | ], 15 | exclude: [], 16 | 17 | framework: 'jasmine2', 18 | 19 | allScriptsTimeout: 110000, 20 | 21 | jasmineNodeOpts: { 22 | showTiming: true, 23 | showColors: true, 24 | isVerbose: false, 25 | includeStackTrace: false, 26 | defaultTimeoutInterval: 400000 27 | }, 28 | directConnect: true, 29 | 30 | capabilities: { 31 | 'browserName': 'chrome', 32 | 'chromeOptions': { 33 | 'args': ['show-fps-counter=true'] 34 | } 35 | }, 36 | 37 | onPrepare: function () { 38 | browser.ignoreSynchronization = true; 39 | }, 40 | 41 | useAllAngular2AppRoots: true 42 | }; 43 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve'; 2 | import { globalsRegex, GLOBAL } from 'rollup-globals-regex'; 3 | 4 | export default { 5 | entry: 'dist/ng-http-interceptor.js', 6 | dest: 'dist/bundles/ng-http-interceptor.es2015.js', 7 | format: 'es', 8 | moduleName: 'httpInterceptor', 9 | plugins: [ 10 | nodeResolve({ jsnext: true, browser: true }) 11 | ], 12 | globals: globalsRegex({ 13 | 'tslib': 'tslib', 14 | [GLOBAL.NG2]: GLOBAL.NG2.TPL, 15 | [GLOBAL.RX]: GLOBAL.RX.TPL, 16 | [GLOBAL.RX_OBSERVABLE]: GLOBAL.RX_OBSERVABLE.TPL, 17 | [GLOBAL.RX_OPERATOR]: GLOBAL.RX_OPERATOR.TPL, 18 | }), 19 | external: (moduleId) => { 20 | if (/^(\@angular|rxjs|tslib)\/?/.test(moduleId)) { 21 | return true; 22 | } 23 | 24 | return false; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /rollup.config.umd.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve'; 2 | import { globalsRegex, GLOBAL } from 'rollup-globals-regex'; 3 | 4 | export default { 5 | entry: 'dist/bundles/ng-http-interceptor.es5.js', 6 | dest: 'dist/bundles/ng-http-interceptor.umd.js', 7 | format: 'umd', 8 | moduleName: 'httpInterceptor', 9 | plugins: [ 10 | nodeResolve({ jsnext: true, browser: true }) 11 | ], 12 | globals: globalsRegex({ 13 | 'tslib': 'tslib', 14 | [GLOBAL.NG2]: GLOBAL.NG2.TPL, 15 | [GLOBAL.RX]: GLOBAL.RX.TPL, 16 | [GLOBAL.RX_OBSERVABLE]: GLOBAL.RX_OBSERVABLE.TPL, 17 | [GLOBAL.RX_OPERATOR]: GLOBAL.RX_OPERATOR.TPL, 18 | }), 19 | external: (moduleId) => { 20 | if (/^(\@angular|rxjs)\/?/.test(moduleId)) { 21 | return true; 22 | } 23 | 24 | return false; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const exec = require('child_process').exec 3 | 4 | const NGC = `"node_modules/.bin/ngc"` 5 | const TSC = `"node_modules/.bin/tsc"` 6 | const ROLLUP = `"node_modules/.bin/rollup"` 7 | const RIMRAF = `"node_modules/.bin/rimraf"` 8 | const UGLIFYJS = `"node_modules/.bin/uglifyjs"` 9 | 10 | const cleanup = `${RIMRAF} dist` 11 | const buildMain = `${NGC} -p tsconfig.es2015.json` 12 | const buildFesmEs2015 = `${ROLLUP} -c rollup.config.js` 13 | const buildFesmEs5 = `${TSC} -p tsconfig.es5.json` 14 | const buildUmd = `${ROLLUP} -c rollup.config.umd.js` 15 | const buildUmdMin = `${UGLIFYJS} -c --screw-ie8 --comments -o dist/bundles/ng-http-interceptor.umd.min.js dist/bundles/ng-http-interceptor.umd.js` 16 | const removeTmpFesmEs5 = `${RIMRAF} dist/bundles/es5` 17 | 18 | execP(cleanup) 19 | .then(() => console.log('Compiling project...')) 20 | .then(() => execP(buildMain)) 21 | .then(() => console.log('OK.\n\nBuilding FESM ES2015...')) 22 | .then(() => execP(buildFesmEs2015)) 23 | .then(() => console.log('OK.\n\nBuilding FESM ES5...')) 24 | .then(() => execP(buildFesmEs5)) 25 | .then(() => moveFesmEs5()) 26 | .then(() => execP(removeTmpFesmEs5)) 27 | .then(() => console.log('OK.\n\nBuilding UMD...')) 28 | .then(() => execP(buildUmd)) 29 | .then(() => console.log('OK.\n\nMinifiyng UMD...')) 30 | .then(() => execP(buildUmdMin)) 31 | .then(() => console.log('OK.\n\n')) 32 | .catch(e => console.error(e)) 33 | 34 | function moveFesmEs5() { 35 | fs.renameSync('dist/bundles/es5/ng-http-interceptor.es2015.js', 'dist/bundles/ng-http-interceptor.es5.js') 36 | } 37 | 38 | function execP(string) { 39 | return new Promise((res, rej) => { 40 | exec(string, (err, stdout) => err ? rej(err) : res(stdout)) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | const { writeFileSync } = require('fs') 2 | const copyfiles = require('copyfiles') 3 | const copyDir = require('copy-dir') 4 | 5 | writeNewPackage('dist/package.json') 6 | copyFiles({ 7 | files: [ 8 | 'README.MD', 9 | 'LICENSE', 10 | '.npmignore', 11 | 'yarn.lock', 12 | ], 13 | to: 'dist', 14 | }) 15 | copyGit('dist') 16 | 17 | function writeNewPackage(to) { 18 | const package = require('../package.json') 19 | 20 | const pathKeys = ['main', 'typings', 'module', 'es2015'] 21 | pathKeys.forEach(k => package[k] = updatePath(package[k])) 22 | 23 | delete package.scripts 24 | delete package.devDependencies 25 | delete package.config 26 | 27 | console.log(`Writing new package.json to ${to}...`) 28 | writeFileSync( 29 | to, 30 | JSON.stringify(package, null, ' '), 31 | 'utf-8') 32 | console.log('OK') 33 | } 34 | 35 | function copyFiles({ files, to }) { 36 | console.log(`Copying files to ${to} [${files.join(', ')}]`) 37 | copyfiles([...files, to], {}, () => null) 38 | console.log('OK') 39 | } 40 | 41 | function copyGit(to) { 42 | console.log(`Copying .git folder to ${to}`) 43 | copyDir.sync('.git', to + '/.git') 44 | console.log('OK') 45 | } 46 | 47 | function updatePath(path) { 48 | return path.replace('dist/', '') 49 | } 50 | -------------------------------------------------------------------------------- /src/http/helpers/getHttpHeadersOrInit.spec.ts: -------------------------------------------------------------------------------- 1 | import { Headers } from '@angular/http'; 2 | import * as module from './getHttpOptionsAndIdx'; 3 | import { getHttpHeadersOrInit } from './getHttpHeadersOrInit'; 4 | 5 | describe('getHttpHeadersOrInit() function', () => { 6 | let getHttpOptionsAndIdxSpy: jasmine.Spy; 7 | 8 | beforeEach(() => getHttpOptionsAndIdxSpy = spyOn(module, 'getHttpOptionsAndIdx')); 9 | 10 | it('should call getHttpOptionsAndIdx() with `data` and `method` args', () => { 11 | getHttpHeadersOrInitTry([1], 'method'); 12 | expect(getHttpOptionsAndIdxSpy).toHaveBeenCalledWith([1], 'method'); 13 | }); 14 | 15 | it('should return `Headers` from `options` and set them back', () => { 16 | getHttpOptionsAndIdxSpy.and.returnValue({ 17 | options: { headers: 'headers' }, 18 | idx: 1 19 | }); 20 | 21 | const data = [0, 1]; 22 | const res = getHttpHeadersOrInit(data, 'method'); 23 | 24 | expect(res).toBe('headers' as any); 25 | expect(data).toEqual([0, { headers: 'headers' }] as any); 26 | }); 27 | 28 | it('should create `Headers` if not found in `options`, return it and set back', () => { 29 | getHttpOptionsAndIdxSpy.and.returnValue({ 30 | options: {}, 31 | idx: 1 32 | }); 33 | 34 | const data = [0, 1]; 35 | const res = getHttpHeadersOrInit(data, 'method'); 36 | 37 | expect(res).toEqual(jasmine.any(Headers)); 38 | expect(data).toEqual([0, { headers: jasmine.any(Headers) }] as any); 39 | }); 40 | 41 | }); 42 | 43 | function getHttpHeadersOrInitTry(data: any[], method: string) { 44 | try { 45 | return getHttpHeadersOrInit(data, method); 46 | } catch (_) { } 47 | } 48 | -------------------------------------------------------------------------------- /src/http/helpers/getHttpHeadersOrInit.ts: -------------------------------------------------------------------------------- 1 | import { Headers } from '@angular/http'; 2 | import { getHttpOptionsAndIdx } from './getHttpOptionsAndIdx'; 3 | 4 | /** 5 | * @description 6 | * Gets {@link Headers} from data array. 7 | * If no {@link RequestOptions} found - creates it and updates original data array. 8 | * If no {@link Headers} found - creates it and sets to {@link RequestOptions}. 9 | * @param data - Array of http data 10 | * @param method - Http method 11 | */ 12 | export function getHttpHeadersOrInit(data: any[], method: string): Headers { 13 | const { options, idx } = getHttpOptionsAndIdx(data, method); 14 | let headers = options.headers; 15 | 16 | // Create and update Headers 17 | if (!options.headers) { 18 | headers = new Headers(); 19 | options.headers = headers; 20 | } 21 | 22 | // Set Options back 23 | data[idx] = options; 24 | 25 | return headers; 26 | } 27 | -------------------------------------------------------------------------------- /src/http/helpers/getHttpOptions.spec.ts: -------------------------------------------------------------------------------- 1 | import { RequestOptions } from '@angular/http'; 2 | import * as module from './getHttpOptionsIdx'; 3 | import { getHttpOptions } from './getHttpOptions'; 4 | 5 | describe('getHttpOptions() function', () => { 6 | let getHttpOptionsIdxSpy: jasmine.Spy; 7 | 8 | beforeEach(() => getHttpOptionsIdxSpy = spyOn(module, 'getHttpOptionsIdx')); 9 | 10 | it('should call getHttpOptionsIdx() with `method` arg', () => { 11 | getHttpOptions([], 'method'); 12 | expect(getHttpOptionsIdxSpy).toHaveBeenCalledWith('method'); 13 | }); 14 | 15 | it('should return value from `data` by index returned from getHttpOptionsIdx()', () => { 16 | getHttpOptionsIdxSpy.and.returnValue(2); 17 | expect(getHttpOptions([1, 2, 3], 'method')).toBe(3 as any); 18 | }); 19 | 20 | it('should return `new RequestOptions` if no value at the index found by default', () => { 21 | getHttpOptionsIdxSpy.and.returnValue(1); 22 | expect(getHttpOptions([1], 'method')).toEqual(jasmine.any(RequestOptions)); 23 | }); 24 | 25 | it('should always return original value if `alwaysOriginal` set to `true`', () => { 26 | getHttpOptionsIdxSpy.and.returnValue(1); 27 | expect(getHttpOptions([1], 'method', true)).toBeUndefined(); 28 | }); 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /src/http/helpers/getHttpOptions.ts: -------------------------------------------------------------------------------- 1 | import { RequestOptions } from '@angular/http'; 2 | import { getHttpOptionsIdx } from './getHttpOptionsIdx'; 3 | 4 | /** 5 | * @description 6 | * Gets http {@link RequestOptions} from data array. 7 | * If no options found and `alwaysOriginal = false` - returns new {@link RequestOptions}. 8 | * @param data - Array of http data 9 | * @param method - Http method 10 | * @param alwaysOriginal - `false` by default 11 | */ 12 | export function getHttpOptions(data: any[], method: string, alwaysOriginal = false): RequestOptions { 13 | return alwaysOriginal ? 14 | data[getHttpOptionsIdx(method)] : 15 | data[getHttpOptionsIdx(method)] || new RequestOptions(); 16 | } 17 | -------------------------------------------------------------------------------- /src/http/helpers/getHttpOptionsAndIdx.spec.ts: -------------------------------------------------------------------------------- 1 | import * as module1 from './getHttpOptions'; 2 | import * as module2 from './getHttpOptionsIdx'; 3 | import { getHttpOptionsAndIdx } from './getHttpOptionsAndIdx'; 4 | 5 | describe('getHttpOptionsAndIdx() function', () => { 6 | let getHttpOptionsSpy: jasmine.Spy 7 | , getHttpOptionsIdxSpy: jasmine.Spy; 8 | 9 | beforeEach(() => { 10 | getHttpOptionsSpy = spyOn(module1, 'getHttpOptions'); 11 | getHttpOptionsIdxSpy = spyOn(module2, 'getHttpOptionsIdx'); 12 | }); 13 | 14 | it('should call getHttpOptions() with `data`, `method` and `alwaysOriginal`=true args by default', () => { 15 | getHttpOptionsAndIdx([1], 'method'); 16 | expect(getHttpOptionsSpy).toHaveBeenCalledWith([1], 'method', false); 17 | }); 18 | 19 | it('should call getHttpOptions() with `data`, `method` and `alwaysOriginal` args', () => { 20 | getHttpOptionsAndIdx([1], 'method', true); 21 | expect(getHttpOptionsSpy).toHaveBeenCalledWith([1], 'method', true); 22 | }); 23 | 24 | it('should call getHttpOptionsIdx() with `method` arg', () => { 25 | getHttpOptionsAndIdx([], 'method'); 26 | expect(getHttpOptionsIdxSpy).toHaveBeenCalledWith('method'); 27 | }); 28 | 29 | it('should return object with values from getHttpOptions() and getHttpOptionsIdx() functions', () => { 30 | getHttpOptionsSpy.and.returnValue('options'); 31 | getHttpOptionsIdxSpy.and.returnValue(5); 32 | 33 | expect(getHttpOptionsAndIdx([], 'method')).toEqual({ 34 | options: 'options', 35 | idx: 5 36 | } as any); 37 | }); 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /src/http/helpers/getHttpOptionsAndIdx.ts: -------------------------------------------------------------------------------- 1 | import { RequestOptions } from '@angular/http'; 2 | import { getHttpOptionsIdx } from './getHttpOptionsIdx'; 3 | import { getHttpOptions } from './getHttpOptions'; 4 | 5 | /** 6 | * @description 7 | * Gets {@link RequestOptions} and it's index location in data array. 8 | * If no options found and `alwaysOriginal = false` - creates new {@link RequestOptions}. 9 | * @param data - Array of http data 10 | * @param method - Http method 11 | * @param alwaysOriginal - `false` by default 12 | */ 13 | export function getHttpOptionsAndIdx(data: any[], method: string, alwaysOriginal = false) { 14 | return { 15 | options: getHttpOptions(data, method, alwaysOriginal), 16 | idx: getHttpOptionsIdx(method) 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/http/helpers/getHttpOptionsIdx.spec.ts: -------------------------------------------------------------------------------- 1 | import { getHttpOptionsIdx } from './getHttpOptionsIdx'; 2 | 3 | describe('getHttpOptionsIdx() function', () => { 4 | testGetHttpOptionsIdx('request', 1); 5 | testGetHttpOptionsIdx('get', 1); 6 | testGetHttpOptionsIdx('delete', 1); 7 | testGetHttpOptionsIdx('head', 1); 8 | testGetHttpOptionsIdx('options', 1); 9 | testGetHttpOptionsIdx('post', 2); 10 | testGetHttpOptionsIdx('put', 2); 11 | testGetHttpOptionsIdx('patch', 2); 12 | }); 13 | 14 | function testGetHttpOptionsIdx(method: string, expextedIdx: number) { 15 | it(`should return '${expextedIdx}' for '${method}' method`, () => 16 | expect(getHttpOptionsIdx(method)).toBe(expextedIdx)); 17 | } 18 | -------------------------------------------------------------------------------- /src/http/helpers/getHttpOptionsIdx.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 3 | * Gets index of {@link RequestOptions} in http data array for specified `method`. 4 | * @param method - Http method 5 | */ 6 | export function getHttpOptionsIdx(method: string): number { 7 | switch (method) { 8 | case 'post': 9 | case 'put': 10 | case 'patch': 11 | return 2; 12 | default: 13 | return 1; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/http/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getHttpOptions'; 2 | export * from './getHttpOptionsIdx'; 3 | export * from './getHttpOptionsAndIdx'; 4 | export * from './getHttpHeadersOrInit'; 5 | -------------------------------------------------------------------------------- /src/http/high-level-api.spec.ts: -------------------------------------------------------------------------------- 1 | import { InterceptableHttp } from './'; 2 | import { TestBed, inject, async, fakeAsync, tick } from '@angular/core/testing'; 3 | import { XHRBackend, HttpModule, Http, Response, ResponseOptions } from '@angular/http'; 4 | import { MockBackend } from '@angular/http/testing'; 5 | import { Observable } from 'rxjs/Observable'; 6 | import { Subject } from 'rxjs/Subject'; 7 | import { HttpInterceptorModule } from './module'; 8 | import { HTTP_INTERCEPTOR_PROVIDER } from './providers'; 9 | import { HttpInterceptorService } from './http-interceptor.service'; 10 | 11 | describe('High-level API', () => { 12 | let httpInterceptor: HttpInterceptorService; 13 | let mockBackend: MockBackend; 14 | let http: Http; 15 | const interceptor = jasmine.createSpy('interceptor'); 16 | 17 | beforeEach(() => { 18 | TestBed.configureTestingModule({ 19 | imports: [HttpModule], 20 | providers: [ 21 | { provide: XHRBackend, useClass: MockBackend }, 22 | ...HTTP_INTERCEPTOR_PROVIDER 23 | ] 24 | }); 25 | interceptor.calls.reset(); 26 | }); 27 | 28 | beforeEach(inject( 29 | [HttpInterceptorService, XHRBackend, Http], 30 | (httpInterceptor_, mockBackend_, http_) => { 31 | httpInterceptor = httpInterceptor_; 32 | mockBackend = mockBackend_; 33 | http = http_; 34 | })); 35 | 36 | describe('request()', () => { 37 | beforeAll(() => interceptor.and.callFake(d => d)); 38 | beforeEach(() => httpInterceptor.request().addInterceptor(interceptor)); 39 | 40 | testHttpRequest('request'); 41 | testHttpRequest('get'); 42 | testHttpRequest('post', 'data'); 43 | testHttpRequest('put', 'data'); 44 | testHttpRequest('delete'); 45 | testHttpRequest('patch', 'data'); 46 | testHttpRequest('head'); 47 | testHttpRequest('options'); 48 | 49 | it('should send request to changed url and with changed data from interceptor', () => { 50 | const connValidator = jasmine.createSpy('connValidator').and 51 | .callFake(conn => { 52 | expect(conn.request.url).toBe('/changed-url'); 53 | expect(conn.request.getBody()).toBe('changed-data'); 54 | }); 55 | 56 | interceptor.and.returnValue(['/changed-url', 'changed-data']); 57 | mockBackend.connections.subscribe(connValidator); 58 | 59 | http.post('/url', 'data').subscribe(() => null); 60 | 61 | expect(connValidator).toHaveBeenCalled(); 62 | }); 63 | 64 | it('should not send request when interceptor returned `false`', async(() => { 65 | const callback = jasmine.createSpy('callback'); 66 | interceptor.and.returnValue(false); 67 | 68 | mockBackend.connections.subscribe(conn => { 69 | throw Error(`Request was made to \`${conn.request.url}\``); 70 | }); 71 | 72 | http.get('/').subscribe(callback); 73 | 74 | expect(callback).not.toHaveBeenCalled(); 75 | })); 76 | 77 | it('should wait for `Observable` when returned from interceptor', fakeAsync(() => { 78 | const callback = jasmine.createSpy('callback'); 79 | const callbackBackend = jasmine.createSpy('callbackBackend').and.callFake(conn => { 80 | expect(conn.request.url).toBe('/changed'); 81 | conn.mockRespond(responseBody('ok')); 82 | }); 83 | const obs$ = new Subject(); 84 | 85 | interceptor.and.returnValue(obs$.asObservable()); 86 | mockBackend.connections.subscribe(callbackBackend); 87 | 88 | http.get('/').subscribe(callback); 89 | 90 | expect(interceptor).toHaveBeenCalledWith(['/'], 'get', jasmine.anything()); 91 | expect(callbackBackend).not.toHaveBeenCalled(); 92 | expect(callback).not.toHaveBeenCalled(); 93 | 94 | obs$.next(['/changed']); 95 | tick(); 96 | 97 | expect(callbackBackend).toHaveBeenCalled(); 98 | expect(callback).toHaveBeenCalled(); 99 | })); 100 | 101 | it('should be able to share data between interceptors', async(() => { 102 | interceptor.and.callFake((d, m, context) => { 103 | context['testkey'] = 'test'; 104 | return d; 105 | }); 106 | const interceptor1 = jasmine.createSpy('interceptor1'); 107 | interceptor1.and.callFake((d, m, context) => { 108 | expect(context).not.toBeNull(); 109 | expect(context['testkey']).toBe('test'); 110 | return d; 111 | }); 112 | httpInterceptor.request().addInterceptor(interceptor1); 113 | http.post('/url', 'data').subscribe(() => null); 114 | })); 115 | }); 116 | 117 | describe('response()', () => { 118 | beforeAll(() => interceptor.and.callFake(o => o.map(() => 'changed'))); 119 | beforeEach(() => httpInterceptor.response().addInterceptor(interceptor)); 120 | 121 | it('should intercept response', () => { 122 | const callback = jasmine.createSpy('callback'); 123 | const connCallback = jasmine.createSpy('connCallback').and 124 | .callFake(conn => conn.mockRespond(responseBody('mocked'))); 125 | 126 | mockBackend.connections.subscribe(connCallback); 127 | 128 | interceptor.and.callFake(o => { 129 | o.subscribe(() => null); 130 | return o.map(() => 'changed'); 131 | }); 132 | 133 | http.get('/url').subscribe(callback); 134 | 135 | expect(interceptor).toHaveBeenCalled(); 136 | expect(callback).toHaveBeenCalledWith('changed'); 137 | expect(connCallback).toHaveBeenCalledTimes(1); 138 | }); 139 | 140 | it('should intercept response on HTTP error', () => { 141 | interceptor.and.callFake(() => Observable.of('fixed error')); 142 | const errorCallback = jasmine.createSpy('errorCallback').and.returnValue(Observable.empty()); 143 | const callback = jasmine.createSpy('callback'); 144 | mockBackend.connections.subscribe(conn => conn.mockError(Error('error'))); 145 | 146 | http.get('/url').catch(errorCallback).subscribe(callback); 147 | 148 | expect(interceptor).toHaveBeenCalled(); 149 | expect(errorCallback).not.toHaveBeenCalled(); 150 | expect(callback).toHaveBeenCalledWith('fixed error'); 151 | }); 152 | }); 153 | 154 | describe('HttpInterceptorModule', () => { 155 | describe('by default', () => { 156 | beforeEach(() => { 157 | // Reconfigure to use HttpInterceptorModule 158 | TestBed 159 | .resetTestingModule() 160 | .configureTestingModule({ 161 | imports: [HttpModule, HttpInterceptorModule], 162 | providers: [{ provide: XHRBackend, useClass: MockBackend }] 163 | }); 164 | }); 165 | 166 | beforeEach(inject( 167 | [HttpInterceptorService, XHRBackend, Http], 168 | (httpInterceptor_, mockBackend_, http_) => { 169 | httpInterceptor = httpInterceptor_; 170 | mockBackend = mockBackend_; 171 | http = http_; 172 | })); 173 | 174 | it('should replace Http service with proxy', () => { 175 | expect(http).not.toEqual(jasmine.any(Http)); 176 | }); 177 | 178 | describe('request()', () => { 179 | beforeAll(() => interceptor.and.callFake(d => d)); 180 | beforeEach(() => httpInterceptor.request().addInterceptor(interceptor)); 181 | 182 | testHttpRequest('request'); 183 | }); 184 | }); 185 | 186 | describe('with noOverrideHttp()', () => { 187 | let interceptableHttp: InterceptableHttp; 188 | 189 | beforeEach(() => { 190 | // Reconfigure to use HttpInterceptorModule 191 | TestBed 192 | .resetTestingModule() 193 | .configureTestingModule({ 194 | imports: [HttpModule, HttpInterceptorModule.noOverrideHttp()], 195 | providers: [{ provide: XHRBackend, useClass: MockBackend }] 196 | }); 197 | }); 198 | 199 | beforeEach(inject( 200 | [HttpInterceptorService, XHRBackend, Http, InterceptableHttp], 201 | (httpInterceptor_, mockBackend_, http_, interceptableHttp_) => { 202 | httpInterceptor = httpInterceptor_; 203 | mockBackend = mockBackend_; 204 | http = http_; 205 | interceptableHttp = interceptableHttp_; 206 | })); 207 | 208 | it('should keep original Http service', () => { 209 | expect(http).toEqual(jasmine.any(Http)); 210 | }); 211 | 212 | describe('request()', () => { 213 | beforeAll(() => interceptor.and.callFake(d => d)); 214 | beforeEach(() => httpInterceptor.request().addInterceptor(interceptor)); 215 | 216 | it('should not intercept requests made by Http', async(() => { 217 | http.get('something').subscribe(); 218 | expect(interceptor).not.toHaveBeenCalled(); 219 | })); 220 | 221 | it('should intercept requests made by InterceptableHttp', async(() => { 222 | interceptableHttp.get('something').subscribe(); 223 | expect(interceptor).toHaveBeenCalledWith(['something'], 'get', jasmine.anything()); 224 | })); 225 | }); 226 | }); 227 | }); 228 | 229 | // ----------- 230 | // Helpers 231 | 232 | function testHttpRequest(method, data?, url = '/url') { 233 | it(`should intercept \`${method}\` request`, async(() => { 234 | const callback = jasmine.createSpy('callback').and 235 | .callFake(r => expect(r.text()).toBe('mocked')); // Make sure response arrived 236 | 237 | const connCallback = jasmine.createSpy('connCallback').and 238 | .callFake(conn => { 239 | expect(conn.request.url).toBe(url); // Make sure request valid 240 | conn.mockRespond(responseBody('mocked')); // Mock response 241 | }); 242 | 243 | mockBackend.connections.subscribe(connCallback); 244 | 245 | http[method](url, data).subscribe(callback); // Request 246 | 247 | expect(interceptor).toHaveBeenCalledWith([url, data], method, jasmine.anything()); // Interceptor called? 248 | expect(callback).toHaveBeenCalled(); // Response callback called? 249 | expect(connCallback).toHaveBeenCalledTimes(1); // Only one request? 250 | })); 251 | } 252 | 253 | function responseBody(body, status = 200) { 254 | return new Response(new ResponseOptions({ body, status })); 255 | } 256 | 257 | }); 258 | -------------------------------------------------------------------------------- /src/http/http-interceptor.service.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, inject, async } from '@angular/core/testing'; 4 | import { HttpInterceptorService } from './http-interceptor.service'; 5 | import { InterceptableStoreFactory, DEFAULT_URL_STORE } from './interceptable-store'; 6 | import Spy = jasmine.Spy; 7 | 8 | describe('Service: HttpInterceptor', () => { 9 | type InterceptableStoreMock = { 10 | setActiveStore: jasmine.Spy; 11 | getMatchedStores: jasmine.Spy; 12 | __isMock: boolean; 13 | }; 14 | type InterceptableStoreMockFn = (...a) => InterceptableStoreMock; 15 | 16 | class InterceptableStoreFactoryMock extends InterceptableStoreFactory { 17 | static stores: InterceptableStoreMock[] = []; 18 | 19 | createStore(): any { 20 | const s: InterceptableStoreMock = { setActiveStore: null, getMatchedStores: null, __isMock: true }; 21 | 22 | s.setActiveStore = jasmine.createSpy('setActiveStore').and.returnValue(s); 23 | s.getMatchedStores = jasmine.createSpy('getMatchedStores'); 24 | 25 | InterceptableStoreFactoryMock.stores.push(s); 26 | return s; 27 | } 28 | } 29 | 30 | let service: { request: InterceptableStoreMockFn, response: InterceptableStoreMockFn } & HttpInterceptorService; 31 | 32 | beforeEach(() => { 33 | TestBed.configureTestingModule({ 34 | providers: [ 35 | { provide: InterceptableStoreFactory, useClass: InterceptableStoreFactoryMock }, 36 | HttpInterceptorService 37 | ] 38 | }); 39 | }); 40 | 41 | beforeEach(inject([HttpInterceptorService], s => service = s)); 42 | afterEach(() => InterceptableStoreFactoryMock.stores.splice(0)); // Cleanup created stores 43 | 44 | it('should exist', () => expect(service).toBeTruthy()); 45 | 46 | it('should create 2 stores', () => { 47 | expect(InterceptableStoreFactoryMock.stores.length).toBe(2); 48 | expect(InterceptableStoreFactoryMock.stores[0].__isMock).toBeTruthy(); 49 | expect(InterceptableStoreFactoryMock.stores[1].__isMock).toBeTruthy(); 50 | }); 51 | 52 | describe('request() method', () => { 53 | it('should return `Interceptable` instance', () => { 54 | expect(service.request()).toEqual(jasmine.objectContaining({ __isMock: true })); 55 | }); 56 | 57 | it('should call setActiveStore() with default url', () => { 58 | const store = service.request(); 59 | expect(store.setActiveStore).toHaveBeenCalledWith(DEFAULT_URL_STORE); 60 | }); 61 | 62 | it('should call setActiveStore() with provided string', () => { 63 | const store = service.request('/url'); 64 | expect(store.setActiveStore).toHaveBeenCalledWith('/url'); 65 | }); 66 | 67 | it('should call setActiveStore() with provided RegExp', () => { 68 | const store = service.request(/\/my-url/); 69 | expect(store.setActiveStore).toHaveBeenCalledWith(/\/my-url/); 70 | }); 71 | }); 72 | 73 | describe('response() method', () => { 74 | it('should return `Interceptable` instance', () => { 75 | expect(service.response()).toEqual(jasmine.objectContaining({ __isMock: true })); 76 | }); 77 | 78 | it('should call setActiveStore() with default url', () => { 79 | const store = service.response(); 80 | expect(store.setActiveStore).toHaveBeenCalledWith(DEFAULT_URL_STORE); 81 | }); 82 | 83 | it('should call setActiveStore() with provided string', () => { 84 | const store = service.response('/url'); 85 | expect(store.setActiveStore).toHaveBeenCalledWith('/url'); 86 | }); 87 | 88 | it('should call setActiveStore() with provided RegExp', () => { 89 | const store = service.response(/\/my-url/); 90 | expect(store.setActiveStore).toHaveBeenCalledWith(/\/my-url/); 91 | }); 92 | }); 93 | 94 | describe('_interceptRequest() method', () => { 95 | it('should reduce on request interceptors, invoke each and return result', async(() => { 96 | const store = InterceptableStoreFactoryMock.stores[0]; 97 | const fn1 = jasmine.createSpy('fn1').and.returnValue(['/url1']); 98 | const fn2 = jasmine.createSpy('fn2').and.returnValue(['/url2']); 99 | const fn3 = jasmine.createSpy('fn3').and.returnValue(['/url3']); 100 | const method = 'method'; 101 | 102 | store.getMatchedStores.and.returnValue([fn1, fn2, fn3]); 103 | 104 | let res; 105 | service._interceptRequest('/url', method, ['/url']).subscribe(r => res = r); 106 | 107 | expect(store.getMatchedStores).toHaveBeenCalledWith('/url'); 108 | expect(fn1).toHaveBeenCalledWith(['/url'], method, undefined); 109 | expect(fn2).toHaveBeenCalledWith(['/url1'], method, undefined); 110 | expect(fn3).toHaveBeenCalledWith(['/url2'], method, undefined); 111 | expect(res).toEqual(['/url3']); 112 | })); 113 | 114 | it('should reduce on request interceptors, invoke each until `false` returned and return it', async(() => { 115 | const store = InterceptableStoreFactoryMock.stores[0]; 116 | const fn1 = jasmine.createSpy('fn1').and.returnValue(['/url1']); 117 | const fn2 = jasmine.createSpy('fn2').and.returnValue(false); 118 | const fn3 = jasmine.createSpy('fn3').and.returnValue(['/url3']); 119 | const method = 'method'; 120 | 121 | store.getMatchedStores.and.returnValue([fn1, fn2, fn3]); 122 | 123 | let res; 124 | service._interceptRequest('/url', method, ['/url']).subscribe(r => res = r); 125 | 126 | expect(store.getMatchedStores).toHaveBeenCalledWith('/url'); 127 | expect(fn1).toHaveBeenCalledWith(['/url'], method, undefined); 128 | expect(fn2).toHaveBeenCalledWith(['/url1'], method, undefined); 129 | expect(fn3).not.toHaveBeenCalled(); 130 | expect(res).toBeFalsy(); 131 | })); 132 | }); 133 | 134 | describe('_interceptResponse() method', () => { 135 | let observableMock: { flatMap: Spy }; 136 | 137 | beforeEach(() => { 138 | observableMock = {}; // Init 139 | 140 | observableMock.flatMap = jasmine.createSpy('Observable.flatMap') 141 | .and.returnValue(observableMock); 142 | }); 143 | 144 | it('should reduce on response interceptors, invoke flatMap() on each and return result', () => { 145 | const store = InterceptableStoreFactoryMock.stores[1]; 146 | const fn1 = jasmine.createSpy('fn1').and.returnValue(observableMock); 147 | const fn2 = jasmine.createSpy('fn2').and.returnValue(observableMock); 148 | const fn3 = jasmine.createSpy('fn3').and.returnValue('changed observable'); 149 | const method = 'method'; 150 | 151 | store.getMatchedStores.and.returnValue([fn1, fn2, fn3]); 152 | 153 | const res = service._interceptResponse('/url', method, observableMock); 154 | 155 | expect(store.getMatchedStores).toHaveBeenCalledWith('/url'); 156 | expect(fn1).toHaveBeenCalledWith(observableMock, method, undefined); 157 | expect(fn2).toHaveBeenCalledWith(observableMock, method, undefined); 158 | expect(fn3).toHaveBeenCalledWith(observableMock, method, undefined); 159 | expect(res).toBe('changed observable' as any); 160 | }); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /src/http/http-interceptor.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Response } from '@angular/http'; 3 | import { Observable } from 'rxjs/Observable'; 4 | import { HttpInterceptor, RequestInterceptor, ResponseInterceptor } from './http-interceptor'; 5 | import { Interceptable } from './interceptable'; 6 | import { InterceptableStoreFactory, DEFAULT_URL_STORE } from './interceptable-store'; 7 | 8 | @Injectable() 9 | export class HttpInterceptorService implements HttpInterceptor { 10 | 11 | private _requestStore = this.store.createStore(); 12 | private _responseStore = this.store.createStore(); 13 | 14 | private static wrapInObservable(res): Observable { 15 | return res instanceof Observable ? res : Observable.of(res); 16 | } 17 | 18 | constructor(private store: InterceptableStoreFactory) { 19 | } 20 | 21 | request(url: string | RegExp = DEFAULT_URL_STORE): Interceptable { 22 | return this._requestStore.setActiveStore(url); 23 | } 24 | 25 | response(url: string | RegExp = DEFAULT_URL_STORE): Interceptable { 26 | return this._responseStore.setActiveStore(url); 27 | } 28 | 29 | _interceptRequest(url: string, method: string, data: any[], context?: any): Observable { 30 | return this._requestStore.getMatchedStores(url).reduce( 31 | (o, i) => o.flatMap(d => { 32 | if (!d) { 33 | return Observable.of(d); 34 | } 35 | return HttpInterceptorService.wrapInObservable(i(d, method, context)); 36 | }), 37 | Observable.of(data) 38 | ); 39 | } 40 | 41 | _interceptResponse(url: string, method: string, response: Observable, context?: any): Observable { 42 | return this._responseStore.getMatchedStores(url).reduce((o, i) => i(o, method, context), response); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/http/http-interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Response } from '@angular/http'; 2 | import { Observable } from 'rxjs/Observable'; 3 | import { Interceptable, Interceptor } from './interceptable'; 4 | 5 | export type RequestInterceptor = Interceptor | boolean>; 6 | export type ResponseInterceptor = Interceptor, Observable>; 7 | 8 | export interface HttpInterceptor { 9 | request(url?: string|RegExp): Interceptable; 10 | response(url?: string|RegExp): Interceptable; 11 | 12 | /** @internal */ 13 | _interceptRequest(url: string, method: string, data: any[]): Observable; 14 | /** @internal */ 15 | _interceptResponse(url: string, method: string, response: Observable): Observable; 16 | } 17 | -------------------------------------------------------------------------------- /src/http/index.ts: -------------------------------------------------------------------------------- 1 | import '../rxjs'; 2 | 3 | export * from './module'; 4 | export * from './interceptable'; 5 | export * from './interceptable-http'; 6 | export * from './http-interceptor'; 7 | export * from './http-interceptor.service'; 8 | export * from './helpers/index'; 9 | -------------------------------------------------------------------------------- /src/http/interceptable-http-proxy.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject, async } from '@angular/core/testing'; 2 | import { 3 | InterceptableHttpProxyService, 4 | InterceptableHttpProxyProviders, 5 | InterceptableHttpProxyNoOverrideProviders, 6 | _proxyTarget 7 | } from './interceptable-http-proxy.service'; 8 | import { HttpModule, Http, XHRBackend, RequestOptions } from '@angular/http'; 9 | import { HttpInterceptorService } from './http-interceptor.service'; 10 | import { Observable } from 'rxjs'; 11 | 12 | describe('Service: InterceptableHttpProxy', () => { 13 | let service: InterceptableHttpProxyService; 14 | const HttpMock = { testMethod: null }, HttpInterceptorServiceMock = { _interceptRequest: null, _interceptResponse: null }; 15 | 16 | beforeEach(() => { 17 | HttpMock.testMethod = jasmine.createSpy('testMethod'); 18 | HttpInterceptorServiceMock._interceptRequest = jasmine.createSpy('_interceptRequest'); 19 | HttpInterceptorServiceMock._interceptResponse = jasmine.createSpy('_interceptResponse'); 20 | }); 21 | 22 | beforeEach(() => { 23 | TestBed.configureTestingModule({ 24 | imports: [HttpModule], 25 | providers: [ 26 | { provide: Http, useValue: HttpMock }, 27 | { provide: HttpInterceptorService, useValue: HttpInterceptorServiceMock }, 28 | InterceptableHttpProxyService 29 | ] 30 | }); 31 | }); 32 | 33 | beforeEach(inject([InterceptableHttpProxyService], s => service = s)); 34 | 35 | it('should exist', () => expect(service).toBeTruthy()); 36 | 37 | it('increase coverage to 100% ^^', () => { 38 | _proxyTarget(); 39 | }); 40 | 41 | describe('get() method', () => { 42 | it('should return receiver from args', () => expect(service.get(null, null, 'target')).toBe('target')); 43 | }); 44 | 45 | describe('apply() method', () => { 46 | let observable: Observable; 47 | 48 | beforeEach(() => { 49 | observable = Observable.of('response'); 50 | HttpMock.testMethod.and.returnValue(observable); 51 | HttpInterceptorServiceMock._interceptRequest.and.returnValue(Observable.of(['url modified'])); 52 | HttpInterceptorServiceMock._interceptResponse.and.returnValue(Observable.of('modified response')); 53 | }); 54 | 55 | it('should return empty observable if nothing in callStack', () => { 56 | spyOn(Observable, 'empty').and.returnValue('empty'); 57 | 58 | const res = service.apply(null, null, null); 59 | 60 | expect(res).toBe('empty'); 61 | }); 62 | 63 | it('should call _interceptRequest() on service and method on Http', async(() => { 64 | service.get(null, 'testMethod', null); 65 | service.apply(null, null, ['url']).subscribe(); 66 | 67 | expect(HttpInterceptorServiceMock._interceptRequest).toHaveBeenCalledWith('url', 'testMethod', ['url'], jasmine.anything()); 68 | expect(HttpMock.testMethod).toHaveBeenCalledWith('url modified'); 69 | })); 70 | 71 | it('should call _interceptRequest() and cancel request if it returns false', async(() => { 72 | HttpInterceptorServiceMock._interceptRequest.and.returnValue(Observable.of(false)); 73 | const callback = jasmine.createSpy('callback'); 74 | 75 | service.get(null, 'testMethod', null); 76 | service.apply(null, null, ['url']).subscribe(callback); 77 | 78 | expect(HttpInterceptorServiceMock._interceptRequest).toHaveBeenCalledWith('url', 'testMethod', ['url'], jasmine.anything()); 79 | expect(HttpMock.testMethod).not.toHaveBeenCalled(); 80 | expect(callback).not.toHaveBeenCalled(); 81 | })); 82 | 83 | it('should call _interceptRequest() with Request and correctly extract url', () => { 84 | HttpInterceptorServiceMock._interceptRequest.and.returnValue(Observable.of(false)); 85 | 86 | service.get(null, 'testMethod', null); 87 | service.apply(null, null, [{ url: 'url' }]); 88 | 89 | expect(HttpInterceptorServiceMock._interceptRequest).toHaveBeenCalledWith('url', 'testMethod', [{ url: 'url' }], jasmine.anything()); 90 | }); 91 | 92 | it('should call .flatMap() on success, call _interceptRequest() inside and return result', () => { 93 | const callback = jasmine.createSpy('callback'); 94 | spyOn(observable, 'flatMap').and.callThrough(); 95 | 96 | service.get(null, 'testMethod', null); 97 | const res = service.apply(null, null, ['url']); 98 | 99 | expect(res).toEqual(jasmine.any(Observable)); 100 | 101 | res.subscribe(callback); 102 | 103 | // This check is no longer valid since observable shared internally 104 | // expect(observable.flatMap).toHaveBeenCalledWith(jasmine.any(Function)); // Normal branch! 105 | expect(HttpInterceptorServiceMock._interceptResponse).toHaveBeenCalledWith('url modified', 'testMethod', 106 | jasmine.any(Observable), jasmine.anything()); 107 | expect(callback).toHaveBeenCalledWith('modified response'); 108 | }); 109 | 110 | it('should call .catch() on error, call _interceptRequest() inside and return result', async(() => { 111 | observable = Observable.throw('error'); 112 | HttpMock.testMethod.and.returnValue(observable); 113 | 114 | const callback = jasmine.createSpy('callback'); 115 | spyOn(observable, 'flatMap').and.returnValue(observable); // <-- Need this hack to make next call on .catch() 116 | spyOn(observable, 'catch').and.callThrough(); 117 | 118 | service.get(null, 'testMethod', null); 119 | const res = service.apply(null, null, ['url']); 120 | 121 | expect(res).toEqual(jasmine.any(Observable)); 122 | 123 | res.subscribe(callback); 124 | 125 | // This check is no longer valid since observable shared internally 126 | // expect(observable.catch).toHaveBeenCalledWith(jasmine.any(Function)); // Catch branch! 127 | expect(HttpInterceptorServiceMock._interceptResponse).toHaveBeenCalledWith('url modified', 'testMethod', 128 | jasmine.any(Observable), jasmine.anything()); 129 | expect(callback).toHaveBeenCalledWith('modified response'); 130 | })); 131 | }); 132 | }); 133 | 134 | describe('Provider factory', () => { 135 | beforeEach(() => spyOn(window, 'Proxy').and.returnValue(() => 'proxy')); 136 | 137 | describe('InterceptableHttpProxyProviders', () => { 138 | const factory: (backend, options, interceptor) => any = (InterceptableHttpProxyProviders[0]).useFactory; 139 | 140 | it('should wrap HttpInterceptorService into Proxy and return it', () => { 141 | const proxy = factory(XHRBackend, RequestOptions, {}); 142 | expect(proxy).toEqual(jasmine.any(Function)); 143 | expect(proxy()).toBe('proxy'); 144 | }); 145 | }); 146 | 147 | describe('InterceptableHttpProxyNoOverrideProviders', () => { 148 | const factory: (http, interceptor) => any = (InterceptableHttpProxyNoOverrideProviders[0]).useFactory; 149 | 150 | it('should wrap HttpInterceptorService into Proxy and return it', () => { 151 | const proxy = factory(Http, {}); 152 | expect(proxy).toEqual(jasmine.any(Function)); 153 | expect(proxy()).toBe('proxy'); 154 | }); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /src/http/interceptable-http-proxy.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Http, XHRBackend, RequestOptions } from '@angular/http'; 3 | import { Observable } from 'rxjs/Observable'; 4 | import { HttpInterceptorService } from './http-interceptor.service'; 5 | import { identityFactory, safeProxy } from './util'; 6 | 7 | @Injectable() 8 | export class InterceptableHttpProxyService implements ProxyHandler { 9 | 10 | private static _callStack: string[] = []; 11 | 12 | private static _extractUrl(url: any[]): string { 13 | const dirtyUrl: string & { url: string } = url[0]; 14 | return typeof dirtyUrl === 'object' && 'url' in dirtyUrl ? dirtyUrl.url : dirtyUrl; 15 | } 16 | 17 | constructor(private http: Http, private httpInterceptorService: HttpInterceptorService) { 18 | } 19 | 20 | get(target: any, p: PropertyKey, receiver: any): any { 21 | InterceptableHttpProxyService._callStack.push(p); 22 | return receiver; 23 | } 24 | 25 | apply(target: any, thisArg: any, argArray?: any): any { 26 | const method = InterceptableHttpProxyService._callStack.pop(); 27 | 28 | // Comply with strict null checks 29 | if (!method) { 30 | return Observable.empty(); 31 | } 32 | 33 | // create a object without prototype as the context object 34 | const context = Object.create(null); 35 | 36 | return this.httpInterceptorService 37 | ._interceptRequest(InterceptableHttpProxyService._extractUrl(argArray), method, argArray, context) 38 | .switchMap(args => { 39 | // Check for request cancellation 40 | if (!args) { 41 | return Observable.empty(); 42 | } 43 | 44 | const response = this.http[method].apply(this.http, args); 45 | 46 | return this.httpInterceptorService._interceptResponse( 47 | InterceptableHttpProxyService._extractUrl(args), method, response, context); 48 | }); 49 | } 50 | 51 | } 52 | 53 | export const _proxyTarget = () => null; 54 | 55 | // Make sure all Http methods are known for Proxy Polyfill 56 | Object.keys(Http.prototype).forEach(method => _proxyTarget[method] = `Http.${method}`); 57 | 58 | export function _proxyFactory(http, interceptor) { 59 | return safeProxy(_proxyTarget, new InterceptableHttpProxyService(http, interceptor)); 60 | } 61 | 62 | export function proxyFactory(backend, options, interceptor) { 63 | return _proxyFactory(new Http(backend, options), interceptor); 64 | } 65 | 66 | export const InterceptableHttpProxyProviders = [ 67 | { 68 | provide: Http, 69 | useFactory: proxyFactory, 70 | deps: [XHRBackend, RequestOptions, HttpInterceptorService] 71 | }, 72 | identityFactory(InterceptableHttpProxyService, Http), 73 | ]; 74 | 75 | export const InterceptableHttpProxyNoOverrideProviders = [ 76 | { 77 | provide: InterceptableHttpProxyService, 78 | useFactory: _proxyFactory, 79 | deps: [Http, HttpInterceptorService] 80 | } 81 | ]; 82 | -------------------------------------------------------------------------------- /src/http/interceptable-http.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, inject } from '@angular/core/testing'; 4 | import { InterceptableHttp } from './interceptable-http'; 5 | import { HttpModule, XHRBackend, Http, ConnectionBackend } from '@angular/http'; 6 | import { MockBackend } from '@angular/http/testing'; 7 | 8 | describe('Service: InterceptableHttp', () => { 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [HttpModule], 12 | providers: [ 13 | {provide: XHRBackend, useClass: MockBackend}, 14 | {provide: ConnectionBackend, useClass: MockBackend}, 15 | InterceptableHttp 16 | ] 17 | }); 18 | }); 19 | 20 | it('should exist', inject([InterceptableHttp], (service: InterceptableHttp) => { 21 | expect(service).toBeTruthy(); 22 | })); 23 | 24 | it('should extend Http service', inject([InterceptableHttp, Http], (service: InterceptableHttp, http: Http) => { 25 | expect(service).toEqual(jasmine.any(http.constructor)); 26 | })); 27 | }); 28 | -------------------------------------------------------------------------------- /src/http/interceptable-http.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Http, RequestOptions, ConnectionBackend } from '@angular/http'; 3 | import { InterceptableHttpProxyService } from './interceptable-http-proxy.service'; 4 | import { identityFactory } from './util'; 5 | 6 | @Injectable() 7 | export class InterceptableHttp extends Http { 8 | 9 | constructor(_backend: ConnectionBackend, _defaultOptions: RequestOptions) { 10 | super(_backend, _defaultOptions); 11 | } 12 | 13 | } 14 | 15 | export const InterceptableHttpProviders = [ 16 | identityFactory(InterceptableHttp, InterceptableHttpProxyService) 17 | ]; 18 | -------------------------------------------------------------------------------- /src/http/interceptable-store.spec.ts: -------------------------------------------------------------------------------- 1 | import { InterceptableStore, InterceptableStoreFactory } from './interceptable-store'; 2 | import Spy = jasmine.Spy; 3 | 4 | describe('InterceptableStore', () => { 5 | let store: InterceptableStore; 6 | 7 | beforeEach(() => store = new InterceptableStore()); 8 | 9 | it('should create an instance', () => { 10 | expect(store).toBeTruthy(); 11 | }); 12 | 13 | describe('addInterceptor() method', () => { 14 | it('should add item to default store and return self', () => { 15 | expect(store.addInterceptor(5)).toBe(store); 16 | expect(store.getStore().length).toBe(1); 17 | expect(store.getStore()[0]).toBe(5); 18 | }); 19 | 20 | it('should add item to two different stores', () => { 21 | store.setActiveStore('store1').addInterceptor(1); 22 | store.setActiveStore('store2').addInterceptor(2); 23 | 24 | expect(store.getStore('store1')).toEqual([1]); 25 | expect(store.getStore('store2')).toEqual([2]); 26 | expect(store.getStore().length).toBe(0); 27 | }); 28 | }); 29 | 30 | describe('removeInterceptor() method', () => { 31 | it('should remove item from default store and return self', () => { 32 | store.getStore().push(1); 33 | store.getStore().push(2); 34 | store.getStore().push(3); 35 | 36 | expect(store.removeInterceptor(2)).toBe(store); 37 | expect(store.getStore()).toEqual([1, 3]); 38 | }); 39 | 40 | it('should do nothing if item not found and return self', () => { 41 | store.getStore().push(1); 42 | store.getStore().push(2); 43 | store.getStore().push(3); 44 | 45 | expect(store.removeInterceptor(4)).toBe(store); 46 | expect(store.getStore()).toEqual([1, 2, 3]); 47 | }); 48 | 49 | it('should remove item from two different stores', () => { 50 | store.setActiveStore('store1').addInterceptor(1).addInterceptor(2).addInterceptor(3); 51 | store.setActiveStore('store2').addInterceptor(4).addInterceptor(5).addInterceptor(6); 52 | 53 | expect(store.setActiveStore('store1').removeInterceptor(2)).toBe(store); 54 | expect(store.getStore('store1')).toEqual([1, 3]); 55 | 56 | expect(store.setActiveStore('store2').removeInterceptor(5)).toBe(store); 57 | expect(store.getStore('store2')).toEqual([4, 6]); 58 | }); 59 | }); 60 | 61 | describe('clearInterceptors() method', () => { 62 | it('should clear default store and return self', () => { 63 | store.getStore().push(1); 64 | store.getStore().push(2); 65 | store.getStore().push(3); 66 | 67 | expect(store.clearInterceptors()).toBe(store); 68 | expect(store.getStore()).toEqual([]); 69 | }); 70 | 71 | it('should invoke removeInterceptor() for every item in arg and return self', () => { 72 | spyOn(store, 'removeInterceptor').and.callThrough(); 73 | store.getStore().push(1); 74 | store.getStore().push(2); 75 | store.getStore().push(3); 76 | 77 | expect(store.clearInterceptors([1, 3])).toBe(store); 78 | expect(store.removeInterceptor).toHaveBeenCalledTimes(2); 79 | expect(store.getStore()).toEqual([2]); 80 | }); 81 | 82 | it('should clear two different stores', () => { 83 | store.setActiveStore('store1').addInterceptor(1).addInterceptor(2).addInterceptor(3); 84 | store.setActiveStore('store2').addInterceptor(4).addInterceptor(5).addInterceptor(6); 85 | 86 | expect(store.setActiveStore('store1').clearInterceptors()).toBe(store); 87 | expect(store.getStore('store1')).toEqual([]); 88 | expect(store.getStore('store2')).toEqual([4, 5, 6]); // Still exists 89 | 90 | expect(store.setActiveStore('store2').clearInterceptors()).toBe(store); 91 | expect(store.getStore('store2')).toEqual([]); 92 | }); 93 | 94 | it('should invoke removeInterceptor() for every item in arg for active store', () => { 95 | spyOn(store, 'removeInterceptor').and.callThrough(); 96 | store.setActiveStore('store1').addInterceptor(1).addInterceptor(2).addInterceptor(3); 97 | store.setActiveStore('store2').addInterceptor(4).addInterceptor(5).addInterceptor(6); 98 | 99 | expect(store.setActiveStore('store1').clearInterceptors([1, 3])).toBe(store); 100 | expect(store.removeInterceptor).toHaveBeenCalledTimes(2); 101 | expect(store.getStore('store1')).toEqual([2]); 102 | expect(store.getStore('store2')).toEqual([4, 5, 6]); // Still exists 103 | 104 | (store.removeInterceptor).calls.reset(); 105 | expect(store.setActiveStore('store2').clearInterceptors([4, 6])).toBe(store); 106 | expect(store.removeInterceptor).toHaveBeenCalledTimes(2); 107 | expect(store.getStore('store2')).toEqual([5]); 108 | }); 109 | }); 110 | 111 | describe('setActiveStore() method', () => { 112 | it('should set `activeStore` from arg and return self', () => { 113 | expect(store.setActiveStore('key1')).toBe(store); 114 | store.addInterceptor(1); 115 | expect(store.getStore('key1')).toEqual([1]); 116 | expect(store.getStore()).toEqual([]); 117 | }); 118 | 119 | it('should set `activeStore` by default to `/` and return self', () => { 120 | store.setActiveStore('key1').addInterceptor(1); 121 | expect(store.setActiveStore()).toBe(store); 122 | store.addInterceptor(2); 123 | 124 | expect(store.getStore('key1')).toEqual([1]); 125 | expect(store.getStore()).toEqual([2]); 126 | }); 127 | }); 128 | 129 | describe('getStore() method', () => { 130 | it('should return new store by key', () => { 131 | expect(store.getStore('key')).toEqual([]); 132 | }); 133 | 134 | it('should return by default store with key `/`', () => { 135 | store.setActiveStore('/').addInterceptor(1); 136 | expect(store.getStore()).toEqual([1]); 137 | }); 138 | 139 | it('should return already created store by same key', () => { 140 | expect(store.getStore('key')).toEqual([]); 141 | store.setActiveStore('key').addInterceptor(1); 142 | expect(store.getStore('key')).toEqual([1]); 143 | }); 144 | }); 145 | 146 | describe('getMatchedStores() method', () => { 147 | beforeEach(() => { 148 | store.addInterceptor(1); 149 | store.setActiveStore('/key1').addInterceptor(2); 150 | store.setActiveStore('/key2/val1').addInterceptor(3); 151 | store.setActiveStore(/\/key\d+\/?.*/).addInterceptor(4); 152 | store.setActiveStore(/\/no-match/).addInterceptor(5); 153 | store.setActiveStore(/\/(will-match)?$/).addInterceptor(6); 154 | }); 155 | 156 | it('should return by default all stores matched by `/` merged', () => { 157 | expect(store.getMatchedStores()).toEqual([1, 6]); 158 | }); 159 | 160 | it('should return stores matched by key merged', () => { 161 | expect(store.getMatchedStores('/key2')).toEqual([1, 4]); 162 | expect(store.getMatchedStores('/key2/val1')).toEqual([1, 3, 4]); 163 | expect(store.getMatchedStores('/key1')).toEqual([1, 2, 4]); 164 | expect(store.getMatchedStores('/will-match')).toEqual([1, 6]); 165 | }); 166 | }); 167 | }); 168 | 169 | describe('InterceptableStoreFactory', () => { 170 | describe('createStore() method', () => { 171 | it('should create new InterceptableStore', () => { 172 | const storeFactory = new InterceptableStoreFactory(); 173 | const store = storeFactory.createStore(); 174 | expect(store).toEqual(jasmine.any(InterceptableStore)); 175 | }); 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /src/http/interceptable-store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Interceptable, Interceptor } from './interceptable'; 3 | 4 | export type AnyInterceptor = Interceptor; 5 | 6 | @Injectable() 7 | export class InterceptableStoreFactory { 8 | 9 | // noinspection JSMethodCanBeStatic 10 | createStore() { 11 | return new InterceptableStore(); 12 | } 13 | 14 | } 15 | 16 | export const DEFAULT_URL_STORE = '/'; 17 | 18 | export class InterceptableStore implements Interceptable { 19 | 20 | private storeMatcher: {[url: string]: RegExp} = {}; 21 | private stores: {[url: string]: T[]} = {}; 22 | private activeStore: string = DEFAULT_URL_STORE; 23 | 24 | private get store(): T[] { 25 | return this._getStoreSafely(this.activeStore); 26 | } 27 | 28 | addInterceptor(interceptor: T): Interceptable { 29 | this.store.push(interceptor); 30 | return this; 31 | } 32 | 33 | removeInterceptor(interceptor: T): Interceptable { 34 | const idx = this.store.indexOf(interceptor); 35 | 36 | if (idx === -1) { 37 | return this; 38 | } 39 | 40 | this.store.splice(idx, 1); 41 | return this; 42 | } 43 | 44 | clearInterceptors(interceptors: T[] = []): Interceptable { 45 | if (interceptors.length > 0) { 46 | interceptors.forEach(i => this.removeInterceptor(i)); 47 | } else { 48 | this.store.splice(0); 49 | } 50 | 51 | return this; 52 | } 53 | 54 | // ---------- 55 | // Internal API 56 | 57 | setActiveStore(url: string|RegExp = DEFAULT_URL_STORE): InterceptableStore { 58 | this.activeStore = String(url); 59 | if (url instanceof RegExp) { 60 | this.storeMatcher[this.activeStore] = url; 61 | } 62 | return this; 63 | } 64 | 65 | getStore(key = DEFAULT_URL_STORE): T[] { 66 | return this._getStoreSafely(key); 67 | } 68 | 69 | getMatchedStores(url = DEFAULT_URL_STORE): T[] { 70 | const backedUrl = `/${url.replace('/', '\\/')}/`; // Use it for direct string matching 71 | return Object.keys(this.stores) 72 | // Match all stores directly and by RegExp if available 73 | .filter(k => k === url || k === backedUrl || (this.storeMatcher[k] && this.storeMatcher[k].test(url))) 74 | // Remove duplications and default store 75 | .filter((k, i, arr) => k !== DEFAULT_URL_STORE && arr.indexOf(k) === i) 76 | .map(k => this.getStore(k)) 77 | .reduce((stores, store) => [...stores, ...store], this.getStore(DEFAULT_URL_STORE)); 78 | } 79 | 80 | private _getStoreSafely(key: string): T[] { 81 | return (this.stores[key] || (this.stores[key] = [])); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/http/interceptable.ts: -------------------------------------------------------------------------------- 1 | export interface Interceptable> { 2 | addInterceptor(interceptor: T): Interceptable; 3 | removeInterceptor(interceptor: T): Interceptable; 4 | clearInterceptors(interceptors?: T[]): Interceptable; 5 | } 6 | 7 | export interface Interceptor { 8 | (data: T, method: string, ctx?: any): D; 9 | } 10 | -------------------------------------------------------------------------------- /src/http/module.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders, NgModule } from '@angular/core'; 2 | import { HttpModule } from '@angular/http'; 3 | import { HTTP_INTERCEPTOR_PROVIDER, HTTP_INTERCEPTOR_NO_OVERRIDE_PROVIDER } from './providers'; 4 | 5 | /** 6 | * @module 7 | * @description 8 | * Library provides Http Interceptor Service for Angular 2 application 9 | * By default overrides angular's Http service 10 | * To keep original Http service use with {@see HttpInterceptorModule.noOverrideHttp()} 11 | */ 12 | @NgModule({ 13 | imports: [HttpModule], 14 | providers: [HTTP_INTERCEPTOR_PROVIDER] 15 | }) 16 | export class HttpInterceptorModule { 17 | 18 | /** 19 | * Keeps original Http service and adds InterceptableHttp service 20 | * Requests made by Http service will not be intercepted - only those made by InterceptableHttp 21 | */ 22 | static noOverrideHttp(): ModuleWithProviders { 23 | return { 24 | ngModule: HttpInterceptorNoOverrideModule 25 | }; 26 | } 27 | } 28 | 29 | @NgModule({ 30 | imports: [HttpModule], 31 | providers: [HTTP_INTERCEPTOR_NO_OVERRIDE_PROVIDER] 32 | }) 33 | export class HttpInterceptorNoOverrideModule { } 34 | -------------------------------------------------------------------------------- /src/http/providers.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@angular/core'; 2 | import { HttpInterceptorService } from './http-interceptor.service'; 3 | import { 4 | InterceptableHttpProxyProviders, 5 | InterceptableHttpProxyNoOverrideProviders 6 | } from './interceptable-http-proxy.service'; 7 | import { InterceptableHttpProviders } from './interceptable-http'; 8 | import { InterceptableStoreFactory } from './interceptable-store'; 9 | 10 | const SharedProviders = [ 11 | InterceptableStoreFactory, 12 | HttpInterceptorService, 13 | ...InterceptableHttpProviders 14 | ]; 15 | 16 | export const HTTP_INTERCEPTOR_PROVIDER: Provider[] = [ 17 | ...SharedProviders, 18 | ...InterceptableHttpProxyProviders 19 | ]; 20 | 21 | export const HTTP_INTERCEPTOR_NO_OVERRIDE_PROVIDER: Provider[] = [ 22 | ...SharedProviders, 23 | ...InterceptableHttpProxyNoOverrideProviders 24 | ]; 25 | -------------------------------------------------------------------------------- /src/http/util.ts: -------------------------------------------------------------------------------- 1 | export const SAFE_PROXY_TRAPS = ['get', 'set', 'apply']; 2 | 3 | export function identityFactory_(ref) { 4 | return ref; 5 | } 6 | 7 | export function identityFactory(provide, obj) { 8 | return { 9 | provide, 10 | useFactory: identityFactory_, 11 | deps: [obj] 12 | }; 13 | } 14 | 15 | export function safeProxyHandler_(handler: any): any { 16 | const safeHandler = {}; 17 | 18 | SAFE_PROXY_TRAPS 19 | .filter(trap => typeof handler[trap] === 'function') 20 | .forEach(trap => safeHandler[trap] = handler[trap].bind(handler)); 21 | 22 | return safeHandler; 23 | } 24 | 25 | export function safeProxy(obj: any, handler: any): any { 26 | return new Proxy(obj, safeProxyHandler_(handler)); 27 | } 28 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * @description 4 | * Library provides Http Interceptor Service for Angular 2 application 5 | * By default overrides angular's Http service 6 | * To keep original Http service use with {@see HttpInterceptorModule.noOverrideHttp()} 7 | */ 8 | export * from './http/index' 9 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | // This file includes polyfills needed by Angular 2 and is loaded before 2 | // the app. You can add your own extra polyfills to this file. 3 | import 'core-js/es6/symbol'; 4 | import 'core-js/es6/object'; 5 | import 'core-js/es6/function'; 6 | import 'core-js/es6/parse-int'; 7 | import 'core-js/es6/parse-float'; 8 | import 'core-js/es6/number'; 9 | import 'core-js/es6/math'; 10 | import 'core-js/es6/string'; 11 | import 'core-js/es6/date'; 12 | import 'core-js/es6/array'; 13 | import 'core-js/es6/regexp'; 14 | import 'core-js/es6/map'; 15 | import 'core-js/es6/set'; 16 | import 'core-js/es6/reflect'; 17 | 18 | import 'core-js/es7/reflect'; 19 | import 'zone.js/dist/zone'; 20 | -------------------------------------------------------------------------------- /src/rxjs.ts: -------------------------------------------------------------------------------- 1 | import 'rxjs/Observable'; 2 | import 'rxjs/add/observable/of'; 3 | import 'rxjs/add/observable/empty'; 4 | import 'rxjs/add/operator/switchMap'; 5 | import 'rxjs/add/operator/mergeMap'; 6 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | import './polyfills.ts'; 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | 10 | import { TestBed } from '@angular/core/testing'; 11 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; 12 | 13 | declare var require: any; 14 | 15 | TestBed.initTestEnvironment( 16 | BrowserDynamicTestingModule, 17 | platformBrowserDynamicTesting() 18 | ); 19 | 20 | const context = require.context('./', true, /\.spec\.ts/); 21 | context.keys().map(context); 22 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | // Typings reference file, see links for more information 2 | // https://github.com/typings/typings 3 | // https://www.typescriptlang.org/docs/handbook/writing-declaration-files.html 4 | 5 | declare var System: any; 6 | 7 | interface Window { 8 | Proxy: any; 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.es2015.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "noEmitHelpers": true, 7 | "importHelpers": true, 8 | "lib": ["es6", "dom"], 9 | "module": "es2015", 10 | "moduleResolution": "node", 11 | "outDir": "./dist", 12 | "sourceMap": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "./node_modules/@types" 16 | ] 17 | }, 18 | "files": [ 19 | "src/index.ts" 20 | ], 21 | "angularCompilerOptions": { 22 | "strictMetadataEmit": true, 23 | "skipTemplateCodegen": true, 24 | "flatModuleOutFile": "ng-http-interceptor.js", 25 | "flatModuleId": "ng-http-interceptor", 26 | "annotateForClosureCompiler": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "noEmitHelpers": true, 6 | "importHelpers": true, 7 | "lib": ["es6", "dom"], 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "outDir": "./dist/bundles/es5", 11 | "target": "es5", 12 | "allowJs": true 13 | }, 14 | "files": [ 15 | "dist/bundles/ng-http-interceptor.es2015.js" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "noEmitHelpers": true, 7 | "importHelpers": true, 8 | "lib": ["es6", "dom"], 9 | "module": "es2015", 10 | "moduleResolution": "node", 11 | "outDir": "./dist", 12 | "sourceMap": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "./node_modules/@types" 16 | ] 17 | }, 18 | "files": [ 19 | "src/index.ts", 20 | "src/typings.d.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "curly": true, 12 | "eofline": true, 13 | "forin": true, 14 | "indent": [ 15 | true, 16 | "spaces" 17 | ], 18 | "label-position": true, 19 | "max-line-length": [ 20 | true, 21 | 140 22 | ], 23 | "member-access": false, 24 | "member-ordering": [ 25 | true, 26 | "static-before-instance", 27 | "variables-before-functions" 28 | ], 29 | "no-arg": true, 30 | "no-bitwise": true, 31 | "no-console": [ 32 | true, 33 | "debug", 34 | "info", 35 | "time", 36 | "timeEnd", 37 | "trace" 38 | ], 39 | "no-construct": true, 40 | "no-debugger": true, 41 | "no-duplicate-variable": true, 42 | "no-empty": false, 43 | "no-eval": true, 44 | "no-inferrable-types": true, 45 | "no-shadowed-variable": true, 46 | "no-string-literal": false, 47 | "no-switch-case-fall-through": true, 48 | "no-trailing-whitespace": true, 49 | "no-unused-expression": true, 50 | "no-use-before-declare": true, 51 | "no-var-keyword": true, 52 | "object-literal-sort-keys": false, 53 | "one-line": [ 54 | true, 55 | "check-open-brace", 56 | "check-catch", 57 | "check-else", 58 | "check-whitespace" 59 | ], 60 | "quotemark": [ 61 | true, 62 | "single" 63 | ], 64 | "radix": true, 65 | "semicolon": [ 66 | "always" 67 | ], 68 | "triple-equals": [ 69 | true, 70 | "allow-null-check" 71 | ], 72 | "typedef-whitespace": [ 73 | true, 74 | { 75 | "call-signature": "nospace", 76 | "index-signature": "nospace", 77 | "parameter": "nospace", 78 | "property-declaration": "nospace", 79 | "variable-declaration": "nospace" 80 | } 81 | ], 82 | "variable-name": false, 83 | "whitespace": [ 84 | true, 85 | "check-branch", 86 | "check-decl", 87 | "check-operator", 88 | "check-separator", 89 | "check-type" 90 | ], 91 | 92 | "use-input-property-decorator": true, 93 | "use-output-property-decorator": true, 94 | "use-host-property-decorator": true, 95 | "no-input-rename": true, 96 | "no-output-rename": true, 97 | "use-life-cycle-interface": true, 98 | "use-pipe-transform-interface": true, 99 | "component-class-suffix": true, 100 | "directive-class-suffix": true 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | const projectRoot = ''; 5 | const appRoot = path.resolve(projectRoot, 'src'); 6 | 7 | module.exports = { 8 | devtool: 'inline-source-map', 9 | context: path.resolve(__dirname, './'), 10 | resolve: { 11 | modules: [ 12 | "node_modules", 13 | appRoot, 14 | ], 15 | extensions: ['.ts', '.js'] 16 | }, 17 | entry: { 18 | test: path.resolve(appRoot, 'test.ts') 19 | }, 20 | output: { 21 | path: './dist.test', 22 | filename: '[name].bundle.js' 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | enforce: 'pre', 28 | test: /\.ts$/, 29 | loader: 'tslint-loader', 30 | exclude: /node_modules/ 31 | }, 32 | { 33 | enforce: 'pre', 34 | test: /\.js$/, 35 | loader: 'source-map-loader', 36 | exclude: [ 37 | /node_modules\/rxjs/, 38 | /node_modules\/@angular/ 39 | ] 40 | }, 41 | { 42 | test: /\.ts$/, 43 | loaders: [ 44 | { 45 | loader: 'awesome-typescript-loader', 46 | query: { 47 | tsconfig: path.resolve(projectRoot, 'tsconfig.json'), 48 | module: 'commonjs', 49 | target: 'es5', 50 | useForkChecker: true 51 | } 52 | }, 53 | { 54 | loader: 'angular2-template-loader' 55 | } 56 | ], 57 | exclude: [/\.e2e\.ts$/] 58 | }, 59 | { test: /\.json$/, loader: 'json-loader' }, 60 | { test: /\.css$/, loaders: ['raw-loader', 'postcss-loader'] }, 61 | { test: /\.(jpg|png)$/, loader: 'url-loader?limit=128000' }, 62 | { test: /\.html$/, loader: 'raw-loader', exclude: [path.resolve(appRoot, 'index.html')] }, 63 | { 64 | enforce: 'post', 65 | test: /\.(js|ts)$/, 66 | loader: 'sourcemap-istanbul-instrumenter-loader', 67 | exclude: [ 68 | /\.(e2e|spec)\.ts$/, 69 | /node_modules/ 70 | ], 71 | query: { 'force-sourcemap': true } 72 | } 73 | ] 74 | }, 75 | plugins: [ 76 | new webpack.SourceMapDevToolPlugin({ 77 | filename: null, // if no value is provided the sourcemap is inlined 78 | test: /\.(ts|js)($|\?)/i // process .js and .ts files only 79 | }), 80 | new webpack.LoaderOptionsPlugin({ 81 | options: { 82 | tslint: { 83 | emitErrors: false, 84 | failOnHint: false, 85 | resourcePath: `./src` 86 | } 87 | } 88 | }) 89 | ], 90 | node: { 91 | fs: 'empty', 92 | global: true, 93 | process: false, 94 | crypto: 'empty', 95 | module: false, 96 | clearImmediate: false, 97 | setImmediate: false 98 | } 99 | }; 100 | --------------------------------------------------------------------------------