diff --git a/package.json b/package.json index 60fa7f1bde..412c2ade0e 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "postinstall": "npm run typings && webdriver-manager update", "webdriver-update": "bash ./node_modules/.bin/webdriver-manager update", "pretest": "rm -rf ./dist && ng build", - "test": "karma start ./config/karma.conf.js", + "test": "karma start ./config/karma.conf.js --single-run", "bump-dev": "gulp bump-version", "bump-patch": "gulp bump-version --ver patch", "bump-minor": "gulp bump-version --ver minor", diff --git a/src/app/components/components/components.component.ts b/src/app/components/components/components.component.ts index 7071c053ff..759082280c 100644 --- a/src/app/components/components/components.component.ts +++ b/src/app/components/components/components.component.ts @@ -59,6 +59,11 @@ export class ComponentsComponent { icon: 'devices', route: 'media', title: 'Media Queries', + }, { + description: 'Http wrappers and helpers', + icon: 'http', + route: 'http', + title: 'Http', }, { description: 'Custom Angular pipes (filters)', icon: 'filter_list', diff --git a/src/app/components/components/components.routes.ts b/src/app/components/components/components.routes.ts index 8456c8e929..f411cd0e6e 100644 --- a/src/app/components/components/components.routes.ts +++ b/src/app/components/components/components.routes.ts @@ -9,6 +9,7 @@ import { FileUploadDemoComponent } from './file-upload'; import { LoadingDemoComponent } from './loading'; import { MarkdownDemoComponent } from './markdown'; import { MediaDemoComponent } from './media'; +import { HttpDemoComponent } from './http'; import { PipesComponent } from './pipes'; export const componentsRoutes: RouterConfig = [{ @@ -36,6 +37,9 @@ export const componentsRoutes: RouterConfig = [{ }, { component: MediaDemoComponent, path: 'media', + }, { + component: HttpDemoComponent, + path: 'http', }, { component: PipesComponent, path: 'pipes', diff --git a/src/app/components/components/http/http.component.html b/src/app/components/components/http/http.component.html new file mode 100644 index 0000000000..a559d8d29c --- /dev/null +++ b/src/app/components/components/http/http.component.html @@ -0,0 +1,125 @@ + + HttpInterceptorService + How to use this service + + +

HttpInterceptorService

+

Service provided with methods that wrap the ng2 [Http] service and provide an easier experience for interceptor implementation.

+

To add a desired interceptor, it needs to implement the [IHttpInterceptor] interface.

+ + export interface IHttpInterceptor { + onRequest?: (requestOptions: RequestOptionsArgs) => RequestOptionsArgs; + onResponse?: (response: Response) => Response; + onResponseError?: (error: Response) => Response; + } + +

Methods:

+

The [HttpInterceptorService] service has {{interceptorServiceMethods.length}} properties:

+ + + +

Example:

+

Typescript:

+ + + +

Bootstrap interceptor providers:

+ + + +

After that, just inject [HttpInterceptorService] and use it for your requests.

+
+
+ + RESTService + How to use this class + + +

RESTService

+

Abstract class provided with methods that wraps http services to facilitate REST API calls.

+

Methods:

+

The RESTService class has {{restServiceMethods.length}} methods:

+ + + +

Example:

+

Typescript:

+ + { + + constructor(private _http: Http /* or HttpInterceptorService */) { + super(_http, { + baseUrl: 'www.api.com', + path: '/path/to/endpoint', + transform: (res: Response): any => res.json(), + }); + } + } + ]]> + +

Note: the constructor takes any object that implements the methods in [IHttp] interface.

+

This can be the angular2 [Http] service, the covalent [HttpInterceptorService] or a custom service.

+ + Observable; + get: (url: string, options?: RequestOptionsArgs) => Observable; + head: (url: string, options?: RequestOptionsArgs) => Observable; + patch: (url: string, body: any, options?: RequestOptionsArgs) => Observable; + post: (url: string, body: any, options?: RequestOptionsArgs) => Observable; + put: (url: string, body: any, options?: RequestOptionsArgs) => Observable; + request: (url: string | Request, options: RequestOptionsArgs) => Observable; + } + ]]> + +
+
diff --git a/src/app/components/components/http/http.component.scss b/src/app/components/components/http/http.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/components/components/http/http.component.spec.ts b/src/app/components/components/http/http.component.spec.ts new file mode 100644 index 0000000000..2ad9c74f40 --- /dev/null +++ b/src/app/components/components/http/http.component.spec.ts @@ -0,0 +1,36 @@ +import { + beforeEach, + addProviders, + describe, + expect, + it, + inject, +} from '@angular/core/testing'; +import { ComponentFixture, TestComponentBuilder } from '@angular/compiler/testing'; +import { HttpDemoComponent } from './http.component'; + +describe('Component: MediaDemo', () => { + let builder: TestComponentBuilder; + + beforeEach(() => { + addProviders([ + HttpDemoComponent, + ]); + }); + + beforeEach(inject([TestComponentBuilder], function (tcb: TestComponentBuilder): void { + builder = tcb; + })); + + it('should inject the component', inject([HttpDemoComponent], (component: HttpDemoComponent) => { + expect(component).toBeTruthy(); + })); + + it('should create the component', inject([], () => { + return builder.createAsync(HttpDemoComponent) + .then((fixture: ComponentFixture) => { + let httpDemoComp: HttpDemoComponent = fixture.componentInstance; + expect(httpDemoComp).toBeTruthy(); + }); + })); +}); diff --git a/src/app/components/components/http/http.component.ts b/src/app/components/components/http/http.component.ts new file mode 100644 index 0000000000..15428999ad --- /dev/null +++ b/src/app/components/components/http/http.component.ts @@ -0,0 +1,88 @@ +import { Component } from '@angular/core'; + +import { MD_CARD_DIRECTIVES } from '@angular2-material/card'; +import { MD_LIST_DIRECTIVES } from '@angular2-material/list'; +import { MdButton } from '@angular2-material/button'; +import { MD_INPUT_DIRECTIVES } from '@angular2-material/input'; + +import { TdHighlightComponent } from '../../../../platform/highlight'; + +@Component({ + directives: [ + MD_CARD_DIRECTIVES, + MD_LIST_DIRECTIVES, + MdButton, + MD_INPUT_DIRECTIVES, + TdHighlightComponent, + ], + moduleId: module.id, + selector: 'http-demo', + styleUrls: [ 'http.component.css' ], + templateUrl: 'http.component.html', +}) +export class HttpDemoComponent { + + interceptorServiceMethods: Object[] = [{ + description: `Uses underlying ng2 [http] to request a DELETE method to a URL, + executing the interceptors as part of the request pipeline.`, + name: 'delete', + type: 'function(url: string, options: RequestOptionsArgs)', + }, { + description: `Uses underlying ng2 [http] to request a GET method to a URL, + executing the interceptors as part of the request pipeline.`, + name: 'get', + type: 'function(url: string, options: RequestOptionsArgs)', + }, { + description: `Uses underlying ng2 [http] to request a HEAD method to a URL, + executing the interceptors as part of the request pipeline.`, + name: 'head', + type: 'function(url: string, options: RequestOptionsArgs)', + }, { + description: `Uses underlying ng2 [http] to request a PATCH method to a URL, + executing the interceptors as part of the request pipeline.`, + name: 'patch', + type: 'function(url: string, data: any, options: RequestOptionsArgs)', + }, { + description: `Uses underlying ng2 [http] to request a POST method to a URL, + executing the interceptors as part of the request pipeline.`, + name: 'post', + type: 'function(url: string, data: any, options: RequestOptionsArgs)', + }, { + description: `Uses underlying ng2 [http] to request a PUT method to a URL, + executing the interceptors as part of the request pipeline.`, + name: 'put', + type: 'function(url: string, data: any, options: RequestOptionsArgs)', + }, { + description: `Uses underlying ng2 [http] to request a generic request to a URL, + executing the interceptors as part of the request pipeline.`, + name: 'request', + type: 'function(url: string | Request, options: RequestOptionsArgs)', + }]; + + restServiceMethods: Object[] = [{ + description: `Creates a GET request to the generated endpoint URL.`, + name: 'query', + type: 'function(query?: IRestQuery)', + }, { + description: `Creates a GET request to the generated endpoint URL, adding the ID at the end.`, + name: 'get', + type: 'function(id: string | number)', + }, { + description: `Creates a POST request to the generated endpoint URL.`, + name: 'create', + type: 'function(obj: T)', + }, { + description: `Creates a PATCH request to the generated endpoint URL, adding the ID at the end.`, + name: 'update', + type: 'function(id: string | number, obj: T)', + }, { + description: `Creates a DELETE request to the generated endpoint URL, adding the ID at the end.`, + name: 'delete', + type: 'function(id: string | number)', + }, { + description: `Builds the endpoint URL with the configured properties and arguments passed in the method.`, + name: 'buildUrl', + type: 'function(id?: string | number, query?: IRestQuery)', + }]; + +} diff --git a/src/app/components/components/http/index.ts b/src/app/components/components/http/index.ts new file mode 100644 index 0000000000..db44868fd3 --- /dev/null +++ b/src/app/components/components/http/index.ts @@ -0,0 +1 @@ +export { HttpDemoComponent } from './http.component'; diff --git a/src/app/components/components/overview/overview.component.ts b/src/app/components/components/overview/overview.component.ts index 9a00325a76..cff1fc740c 100644 --- a/src/app/components/components/overview/overview.component.ts +++ b/src/app/components/components/overview/overview.component.ts @@ -52,6 +52,11 @@ export class OverviewComponent { icon: 'devices', route: 'media', title: 'Media', + }, { + color: 'indigo-700', + icon: 'http', + route: 'http', + title: 'Http', }, { color: 'deep-orange-700', icon: 'filter_list', diff --git a/src/platform/core/index.ts b/src/platform/core/index.ts index 5264d4aa39..089952efec 100644 --- a/src/platform/core/index.ts +++ b/src/platform/core/index.ts @@ -66,11 +66,6 @@ export { TdTimeDifferencePipe } from './pipes/time-difference/time-difference.pi export { TdBytesPipe } from './pipes/bytes/bytes.pipe'; export { TdDigitsPipe } from './pipes/digits/digits.pipe'; -/** - * SERVICES - */ -export { RESTService, IRestTransform, IRestConfig, IRestQuery, IHttp } from './services/rest.service'; - /** * MEDIA */ diff --git a/src/platform/http/README.md b/src/platform/http/README.md new file mode 100644 index 0000000000..fe17503ce3 --- /dev/null +++ b/src/platform/http/README.md @@ -0,0 +1,126 @@ +# HttpInterceptorService + +## API Summary + +Methods: + +| Name | Type | Description | +| --- | --- | --- | +| `delete` | `function(url: string, options: RequestOptionsArgs)` | Uses underlying ng2 [http] to request a DELETE method to a URL, executing the interceptors as part of the request pipeline. +| `get` | `function(url: string, options: RequestOptionsArgs)` | Uses underlying ng2 [http] to request a GET method to a URL, executing the interceptors as part of the request pipeline. +| `head` | `function(url: string, options: RequestOptionsArgs)` | Uses underlying ng2 [http] to request a HEAD method to a URL, executing the interceptors as part of the request pipeline. +| `patch` | `function(url: string, data: any, options: RequestOptionsArgs)` | Uses underlying ng2 [http] to request a PATCH method to a URL, executing the interceptors as part of the request pipeline. +| `post` | `function(url: string, data: any, options: RequestOptionsArgs)` | Uses underlying ng2 [http] to request a POST method to a URL, executing the interceptors as part of the request pipeline. +| `put` | `function(url: string, data: any, options: RequestOptionsArgs)` | Uses underlying ng2 [http] to request a PUT method to a URL, executing the interceptors as part of the request pipeline. +| `request` | `function(url: string | Request, options: RequestOptionsArgs)` | Uses underlying ng2 [http] to request a generic request to a URL, executing the interceptors as part of the request pipeline. + +## Usage + +Service provided with methods that wrap the ng2 [Http] service and provide an easier experience for interceptor implementation. + +To add a desired interceptor, it needs to implement the [IHttpInterceptor] interface. + +```typescript +export interface IHttpInterceptor { + onRequest?: (requestOptions: RequestOptionsArgs) => RequestOptionsArgs; + onResponse?: (response: Response) => Response; + onResponseError?: (error: Response) => Response; +} +``` +Every method is optional, so you can just implement the ones that are needed. + +Example: + +```typescript +import { Injectable } from '@angular/core'; +import { IHttpInterceptor } from '@covalent/http'; + +@Injectable() +export class CustomInterceptor implements IHttpInterceptor { + + onRequest(requestOptions: RequestOptionsArgs): RequestOptionsArgs { + ... // do something to requestOptions + return requestOptions; + } + + onResponse(response: Response): Response { + ... // check response status and do something + return response; + } + + onResponseError(error: Response): Response { + ... // check error status and do something + return error; + } +} + +``` + +Also, you need to bootstrap the interceptor providers + +```typescript +import { HTTP_PROVIDERS } from '@angular/http'; +import { provideInterceptors } from '@covalent/http'; +import { CustomInterceptor } from 'dir/to/interceptor'; + +bootstrap(AppComponent, [ + HTTP_PROVIDERS, + provideInterceptors([CustomInterceptor]), +]); +``` + +After that, just inject [HttpInterceptorService] and use it for your requests. + + +# RESTService + +## API Summary + +Methods: + +| Name | Type | Description | +| --- | --- | --- | +| `query` | `function(query?: IRestQuery)` | Creates a GET request to the generated endpoint URL. +| `get` | `function(id: string | number)` | Creates a GET request to the generated endpoint URL, adding the ID at the end. +| `create` | `function(obj: T)` | Creates a POST request to the generated endpoint URL. +| `update` | `function(id: string | number, obj: T)` | Creates a PATCH request to the generated endpoint URL, adding the ID at the end. +| `delete` | `function(id: string | number)` | Creates a DELETE request to the generated endpoint URL, adding the ID at the end. +| `buildUrl` | `function(id?: string | number, query?: IRestQuery)` | Builds the endpoint URL with the configured properties and arguments passed in the method. + +## Usage + +Abstract class provided with methods that wraps http services to facilitate REST API calls. + +Example: + +```typescript +import { Injectable } from '@angular/core'; +import { Response, Http } from '@angular/http'; +import { RESTService, HttpInterceptorService } from '@covalent/http'; + +@Injectable() +export class CustomRESTService extends RESTService { + + constructor(private _http: Http /* or HttpInterceptorService */) { + super(_http, { + baseUrl: 'www.api.com', + path: '/path/to/endpoint', + transform: (res: Response): any => res.json(), + }); + } +} + +``` +Note: the constructor takes any object that implements the methods in [IHttp] interface. This can be the ng2 [Http] service, the covalent [HttpInterceptorService] or a custom service.

+ +```typescript +export interface IHttp { + delete: (url: string, options?: RequestOptionsArgs) => Observable; + get: (url: string, options?: RequestOptionsArgs) => Observable; + head: (url: string, options?: RequestOptionsArgs) => Observable; + patch: (url: string, body: any, options?: RequestOptionsArgs) => Observable; + post: (url: string, body: any, options?: RequestOptionsArgs) => Observable; + put: (url: string, body: any, options?: RequestOptionsArgs) => Observable; + request: (url: string | Request, options: RequestOptionsArgs) => Observable; +} +``` diff --git a/src/platform/http/http-interceptor.service.ts b/src/platform/http/http-interceptor.service.ts new file mode 100644 index 0000000000..c0563270b7 --- /dev/null +++ b/src/platform/http/http-interceptor.service.ts @@ -0,0 +1,138 @@ +import { Injectable, Type, Provider, Injector } from '@angular/core'; +import { Http, RequestOptionsArgs, Response, Request } from '@angular/http'; + +import { Observable } from 'rxjs/Observable'; +import { Subscriber } from 'rxjs/Subscriber'; + +export interface IHttpInterceptor { + onRequest?: (requestOptions: RequestOptionsArgs) => RequestOptionsArgs; + onResponse?: (response: Response) => Response; + onResponseError?: (error: Response) => Response; +} + +@Injectable() +export class HttpInterceptorService { + + requestInterceptors: IHttpInterceptor[] = []; + + constructor(private _http: Http, private _injector: Injector, requestInterceptors: Type[]) { + requestInterceptors.forEach((interceptor: Type) => { + this.requestInterceptors.push(_injector.get(interceptor)); + }); + } + + request(url: string | Request, options: RequestOptionsArgs = {}): Observable { + this._requestConfig(options); + return this._http.request(url, options) + .do((response: Response) => { + return this._responseResolve(response); + }).catch((error: Response) => { + return this._errorResolve(error); + }); + } + + delete(url: string, options: RequestOptionsArgs = {}): Observable { + this._requestConfig(options); + return this._http.delete(url, options) + .do((response: Response) => { + return this._responseResolve(response); + }).catch((error: Response) => { + return this._errorResolve(error); + }); + } + + get(url: string, options: RequestOptionsArgs = {}): Observable { + this._requestConfig(options); + return this._http.get(url, options) + .do((response: Response) => { + return this._responseResolve(response); + }).catch((error: Response) => { + return this._errorResolve(error); + }); + } + + head(url: string, options: RequestOptionsArgs = {}): Observable { + this._requestConfig(options); + return this._http.head(url, options) + .do((response: Response) => { + return this._responseResolve(response); + }).catch((error: Response) => { + return this._errorResolve(error); + }); + } + + patch(url: string, data: any, options: RequestOptionsArgs = {}): Observable { + this._requestConfig(options); + return this._http.patch(url, data, options) + .do((response: Response) => { + return this._responseResolve(response); + }).catch((error: Response) => { + return this._errorResolve(error); + }); + } + + post(url: string, data: any, options: RequestOptionsArgs = {}): Observable { + this._requestConfig(options); + return this._http.post(url, data, options) + .do((response: Response) => { + return this._responseResolve(response); + }).catch((error: Response) => { + return this._errorResolve(error); + }); + } + + put(url: string, data: any, options: RequestOptionsArgs = {}): Observable { + this._requestConfig(options); + return this._http.put(url, data, options) + .do((response: Response) => { + return this._responseResolve(response); + }).catch((error: Response) => { + return this._errorResolve(error); + }); + } + + private _requestConfig(requestOptions: RequestOptionsArgs): void { + this.requestInterceptors.forEach((interceptor: IHttpInterceptor) => { + if (interceptor.onRequest) { + requestOptions = interceptor.onRequest(requestOptions); + } + }); + } + + private _responseResolve(response: Response): Observable { + this.requestInterceptors.forEach((interceptor: IHttpInterceptor) => { + if (interceptor.onResponse) { + response = interceptor.onResponse(response); + } + }); + return new Observable((subscriber: Subscriber) => { + subscriber.next(response); + }); + } + + private _errorResolve(error: Response): Observable { + this.requestInterceptors.forEach((interceptor: IHttpInterceptor) => { + if (interceptor.onResponseError) { + error = interceptor.onResponseError(error); + } + }); + return new Observable((subscriber: Subscriber) => { + subscriber.error(error); + }); + } + +} + +export function provideInterceptors(requestInterceptors: Type[] = []): any[] { + let providers: any[] = []; + requestInterceptors.forEach((interceptor: Type) => { + providers.push(interceptor); + }); + providers.push(new Provider(HttpInterceptorService, { + useFactory: (http: Http, injector: Injector): HttpInterceptorService => { + return new HttpInterceptorService(http, injector, requestInterceptors); + }, + deps: [Http, Injector], + })); + return providers; +} diff --git a/src/platform/core/services/rest.service.ts b/src/platform/http/http-rest.service.ts similarity index 91% rename from src/platform/core/services/rest.service.ts rename to src/platform/http/http-rest.service.ts index 735f2e3927..74608a9046 100644 --- a/src/platform/core/services/rest.service.ts +++ b/src/platform/http/http-rest.service.ts @@ -1,4 +1,4 @@ -import { Headers, RequestOptionsArgs, Response } from '@angular/http'; +import { Headers, RequestOptionsArgs, Response, Request } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import { Subscriber } from 'rxjs/Subscriber'; @@ -24,6 +24,7 @@ export interface IHttp { patch: (url: string, body: any, options?: RequestOptionsArgs) => Observable; post: (url: string, body: any, options?: RequestOptionsArgs) => Observable; put: (url: string, body: any, options?: RequestOptionsArgs) => Observable; + request: (url: string | Request, options?: RequestOptionsArgs) => Observable; } export abstract class RESTService { @@ -75,9 +76,7 @@ export abstract class RESTService { public create(obj: T): Observable { let requestOptions: RequestOptionsArgs = this.buildRequestOptions(); - // Need to do this till angular fixes the automatic body content type detection issue. - requestOptions.headers.append('Content-Type', 'application/json'); - let request: Observable = this.http.post(this.buildUrl(), JSON.stringify(obj), requestOptions); + let request: Observable = this.http.post(this.buildUrl(), obj, requestOptions); return request.map((res: Response) => { if (res.status === 201) { return this.transform(res); @@ -97,9 +96,7 @@ export abstract class RESTService { public update(id: string | number, obj: T): Observable { let requestOptions: RequestOptionsArgs = this.buildRequestOptions(); - // Need to do this till angular fixes the automatic body content type detection issue. - requestOptions.headers.append('Content-Type', 'application/json'); - let request: Observable = this.http.patch(this.buildUrl(id), JSON.stringify(obj), requestOptions); + let request: Observable = this.http.patch(this.buildUrl(id), obj, requestOptions); return request.map((res: Response) => { return this.transform(res); }).catch((error: Response) => { diff --git a/src/platform/http/index.ts b/src/platform/http/index.ts new file mode 100644 index 0000000000..5279a7bef5 --- /dev/null +++ b/src/platform/http/index.ts @@ -0,0 +1,2 @@ +export { RESTService, IRestTransform, IRestConfig, IRestQuery, IHttp } from './http-rest.service'; +export { IHttpInterceptor, HttpInterceptorService, provideInterceptors } from './http-interceptor.service'; diff --git a/src/platform/http/package.json b/src/platform/http/package.json new file mode 100644 index 0000000000..f731f17689 --- /dev/null +++ b/src/platform/http/package.json @@ -0,0 +1,47 @@ +{ + "name": "@covalent/http", + "version": "0.5.0", + "description": "Teradata UI Platform Http Helper Module", + "keywords": [ + "angular", + "components", + "reusable" + ], + "scripts": { + }, + "engines": { + "node": ">4.4 < 5", + "npm": ">= 3" + }, + "repository": { + "type": "git", + "url": "https://github.com/teradata/covalent.git" + }, + "bugs": { + "url": "https://github.com/teradata/covalent/issues" + }, + "license": "MIT", + "author": "Teradata UX", + "contributors": [ + "Kyle Ledbetter ", + "Richa Vyas ", + "Ed Morales ", + "Jason Weaver ", + "Jeremy Wilken " + ], + "dependencies": { + "@angular/common": "2.0.0-rc.4", + "@angular/compiler": "2.0.0-rc.4", + "@angular/core": "2.0.0-rc.4", + "@angular/forms": "0.2.0", + "@angular/http": "2.0.0-rc.4", + "@angular/platform-browser": "2.0.0-rc.4", + "@angular/platform-browser-dynamic": "2.0.0-rc.4", + "@angular/router": "3.0.0-beta.2", + "es6-shim": "0.35.1", + "reflect-metadata": "^0.1.3", + "rxjs": "5.0.0-beta.6", + "systemjs": "0.19.31", + "zone.js": "^0.6.12" + } +} diff --git a/src/system-config.ts b/src/system-config.ts index edb8e4d68c..514414dd40 100644 --- a/src/system-config.ts +++ b/src/system-config.ts @@ -34,6 +34,7 @@ const barrels: string[] = [ 'platform/highlight', 'platform/file-upload', 'platform/markdown', + 'platform/http', // App specific barrels. 'app', @@ -47,6 +48,7 @@ const barrels: string[] = [ 'app/components/components/loading', 'app/components/components/pipes', 'app/components/components/media', + 'app/components/components/http', 'app/components/components/markdown', 'app/components/docs', 'app/components/docs/overview',