From cac278ba2edd41f2843feb712217c18b85d692f1 Mon Sep 17 00:00:00 2001 From: Friedrich Pawelka Date: Thu, 8 Mar 2018 09:50:34 +0100 Subject: [PATCH 1/7] Remove http dependency --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index b9d36a0b..6ff2cc2b 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "qs": "^6.5.1" }, "peerDependencies": { - "@angular/http": ">=4.0.0", "reflect-metadata": ">=0.1.3", "rxjs": ">=5.2.0" }, From 3914bd0fec53a58601a214c1264f640df7045706 Mon Sep 17 00:00:00 2001 From: Friedrich Pawelka Date: Mon, 12 Mar 2018 14:14:30 +0100 Subject: [PATCH 2/7] Use httpClientModule instead of httpModule --- src/module.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/module.ts b/src/module.ts index b719f8f8..01bcbc31 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,10 +1,10 @@ import { NgModule } from '@angular/core'; -import { HttpModule } from '@angular/http'; +import { HttpClientModule } from '@angular/common/http'; import { PROVIDERS } from './providers'; @NgModule({ providers: [PROVIDERS], - exports: [HttpModule] + exports: [HttpClientModule] }) export class JsonApiModule { } From 72ecccd7564633f11b4e02f80e1f20b2ab3a7b02 Mon Sep 17 00:00:00 2001 From: Friedrich Pawelka Date: Mon, 12 Mar 2018 14:18:24 +0100 Subject: [PATCH 3/7] Remove deprecated http module from json-api model --- src/models/json-api.model.spec.ts | 13 ++++--------- src/models/json-api.model.ts | 5 ++--- test/datastore.service.ts | 4 ++-- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/models/json-api.model.spec.ts b/src/models/json-api.model.spec.ts index fdc6e76a..96ea0b83 100644 --- a/src/models/json-api.model.spec.ts +++ b/src/models/json-api.model.spec.ts @@ -6,8 +6,7 @@ import { AUTHOR_CREATED, AUTHOR_UPDATED, getAuthorData, getIncludedBooks, BOOK_TITLE, BOOK_PUBLISHED, CHAPTER_TITLE } from '../../test/fixtures/author.fixture'; import { Book } from '../../test/models/book.model'; -import { Http, BaseRequestOptions, ConnectionBackend } from '@angular/http'; -import { MockBackend } from '@angular/http/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; import { Datastore } from '../../test/datastore.service'; import { Chapter } from '../../test/models/chapter.model'; @@ -18,14 +17,10 @@ describe('JsonApiModel', () => { beforeEach(() => { TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + ], providers: [ - { - provide: Http, useFactory: (backend: ConnectionBackend, defaultOptions: BaseRequestOptions) => { - return new Http(backend, defaultOptions); - }, deps: [MockBackend, BaseRequestOptions] - }, - MockBackend, - BaseRequestOptions, Datastore ] }); diff --git a/src/models/json-api.model.ts b/src/models/json-api.model.ts index bbd462cb..7c7e193a 100644 --- a/src/models/json-api.model.ts +++ b/src/models/json-api.model.ts @@ -1,4 +1,3 @@ -import { Headers } from '@angular/http'; import find from 'lodash-es/find'; import includes from 'lodash-es/includes'; import { Observable } from 'rxjs/Observable'; @@ -198,10 +197,10 @@ export class JsonApiModel { _.extend(peek, data.attributes); return peek; } - + const newObject: T = new modelType(this._datastore, data); this._datastore.addToStore(newObject); - + return newObject; } } diff --git a/test/datastore.service.ts b/test/datastore.service.ts index 0185f2f3..2ab8032b 100644 --- a/test/datastore.service.ts +++ b/test/datastore.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Http } from '@angular/http'; +import { HttpClient } from '@angular/common/http'; import { JsonApiDatastore, JsonApiDatastoreConfig } from '../src'; import { Author } from './models/author.model'; import { Book } from './models/book.model'; @@ -18,7 +18,7 @@ export const API_VERSION = 'v1'; } }) export class Datastore extends JsonApiDatastore { - constructor(http: Http) { + constructor(http: HttpClient) { super(http); } } From f021360eec70c5e33c9f358bf769f9ab352ba0ff Mon Sep 17 00:00:00 2001 From: Friedrich Pawelka Date: Mon, 12 Mar 2018 14:22:52 +0100 Subject: [PATCH 4/7] Integrate httpClient concepts into json-api datastore --- src/services/json-api-datastore.service.ts | 87 +++++++++++++--------- 1 file changed, 50 insertions(+), 37 deletions(-) diff --git a/src/services/json-api-datastore.service.ts b/src/services/json-api-datastore.service.ts index 6cb98855..20ceae06 100644 --- a/src/services/json-api-datastore.service.ts +++ b/src/services/json-api-datastore.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Headers, Http, RequestOptions, Response } from '@angular/http'; +import { HttpClient, HttpHeaders, HttpResponse, HttpErrorResponse } from '@angular/common/http'; import find from 'lodash-es/find'; import { Observable } from 'rxjs/Observable'; import { ErrorObservable } from 'rxjs/observable/ErrorObservable'; @@ -39,7 +39,7 @@ export class JsonApiDatastore { protected config: DatastoreConfig; - constructor(protected http: Http) {} + constructor(protected http: HttpClient) {} /** @deprecated - use findAll method to take all models **/ query( @@ -48,9 +48,9 @@ export class JsonApiDatastore { headers?: Headers, customUrl?: string ): Observable { - const options: RequestOptions = this.getOptions(headers); + const requestHeaders: HttpHeaders = this.buildHeaders(headers); const url: string = this.buildUrl(modelType, params, undefined, customUrl); - return this.http.get(url, options) + return this.http.get(url, { headers: requestHeaders }) .map((res: any) => this.extractQueryData(res, modelType)) .catch((res: any) => this.handleError(res)); } @@ -61,10 +61,10 @@ export class JsonApiDatastore { headers?: Headers, customUrl?: string ): Observable> { - const options: RequestOptions = this.getOptions(headers); + const requestHeaders: HttpHeaders = this.buildHeaders(headers); const url: string = this.buildUrl(modelType, params, undefined, customUrl); - return this.http.get(url, options) + return this.http.get(url, { headers: requestHeaders }) .map((res: any) => this.extractQueryData(res, modelType, true)) .catch((res: any) => this.handleError(res)); } @@ -76,10 +76,10 @@ export class JsonApiDatastore { headers?: Headers, customUrl?: string ): Observable { - const options: RequestOptions = this.getOptions(headers); + const requestHeaders: HttpHeaders = this.buildHeaders(headers); const url: string = this.buildUrl(modelType, params, id, customUrl); - return this.http.get(url, options) + return this.http.get(url, { headers: requestHeaders, observe: 'response' }) .map((res) => this.extractRecordData(res, modelType)) .catch((res: any) => this.handleError(res)); } @@ -114,11 +114,11 @@ export class JsonApiDatastore { const modelType = >model.constructor; const modelConfig: ModelConfig = model.modelConfig; const typeName: string = modelConfig.type; - const options: RequestOptions = this.getOptions(headers); + const requestHeaders: HttpHeaders = this.buildHeaders(headers); const relationships: any = this.getRelationships(model); const url: string = this.buildUrl(modelType, params, model.id, customUrl); - let httpCall: Observable; + let httpCall: Observable>; const body: any = { data: { relationships, @@ -129,9 +129,9 @@ export class JsonApiDatastore { }; if (model.id) { - httpCall = this.http.patch(url, body, options); + httpCall = this.http.patch(url, body, { headers: requestHeaders, observe: 'response' }); } else { - httpCall = this.http.post(url, body, options); + httpCall = this.http.post(url, body, { headers: requestHeaders, observe: 'response' }); } return httpCall @@ -155,10 +155,10 @@ export class JsonApiDatastore { headers?: Headers, customUrl?: string ): Observable { - const options: RequestOptions = this.getOptions(headers); + const requestHeaders: HttpHeaders = this.buildHeaders(headers); const url: string = this.buildUrl(modelType, null, id, customUrl); - return this.http.delete(url, options).catch((res: any) => this.handleError(res)); + return this.http.delete(url, { headers: requestHeaders }).catch((res: HttpErrorResponse) => this.handleError(res)); } peekRecord(modelType: ModelType, id: string): T | null { @@ -182,6 +182,7 @@ export class JsonApiDatastore { id?: string, customUrl?: string ): string { + // TODO: use HttpParams instead of appending a string to the url const queryParams: string = this.toQueryString(params); if (customUrl) { @@ -251,11 +252,10 @@ export class JsonApiDatastore { } protected extractQueryData( - res: any, + body: any, modelType: ModelType, withMeta = false ): T[] | JsonApiQueryData { - const body: any = res.json(); const models: T[] = []; body.data.forEach((data: any) => { @@ -281,10 +281,16 @@ export class JsonApiDatastore { return new modelType(this, data); } - protected extractRecordData(res: Response, modelType: ModelType, model?: T): T { - const body: any = res.json(); - - if (!body) { + protected extractRecordData( + res: HttpResponse, + modelType: ModelType, + model?: T + ): T { + const body: any = res.body; + // Error in Angular < 5.2.4 (see https://github.com/angular/angular/issues/20744) + // null is converted to 'null', so this is temporary needed to make testcase possible + // (and to avoid a decrease of the coverage) + if (!body || body === 'null') { throw new Error('no body in response'); } @@ -312,16 +318,15 @@ export class JsonApiDatastore { protected handleError(error: any): ErrorObservable { - try { - const body: any = error.json(); - - if (body.errors && body.errors instanceof Array) { - const errors: ErrorResponse = new ErrorResponse(body.errors); - console.error(error, errors); - return Observable.throw(errors); - } - } catch (e) { - // no valid JSON + if ( + error instanceof HttpErrorResponse && + error.error instanceof Object && + error.error.errors && + error.error.errors instanceof Array + ) { + const errors: ErrorResponse = new ErrorResponse(error.error.errors); + console.error(error, errors); + return Observable.throw(errors); } console.error(error); @@ -333,15 +338,23 @@ export class JsonApiDatastore { return new metaModel(body); } - protected getOptions(customHeaders?: Headers): RequestOptions { - const requestHeaders = new Headers(); + /** @deprecated - use buildHeaders method to build request headers **/ + protected getOptions(customHeaders?: Headers): any { + return { + headers: this.buildHeaders(customHeaders), + }; + } + + protected buildHeaders(customHeaders?: Headers): HttpHeaders { + const requestHeaders: any = { + Accept: 'application/vnd.api+json', + 'Content-Type': 'application/vnd.api+json' + }; - requestHeaders.set('Accept', 'application/vnd.api+json'); - requestHeaders.set('Content-Type', 'application/vnd.api+json'); if (this._headers) { this._headers.forEach((values, name) => { if (name !== undefined) { - requestHeaders.set(name, values); + requestHeaders[name] = values; } }); } @@ -349,12 +362,12 @@ export class JsonApiDatastore { if (customHeaders) { customHeaders.forEach((values, name) => { if (name !== undefined) { - requestHeaders.set(name, values); + requestHeaders[name] = values; } }); } - return new RequestOptions({ headers: requestHeaders }); + return new HttpHeaders(requestHeaders); } private _toQueryString(params: any): string { From 607db254b6507f9a170965bf535959b07f9f1677 Mon Sep 17 00:00:00 2001 From: Friedrich Pawelka Date: Mon, 12 Mar 2018 14:23:30 +0100 Subject: [PATCH 5/7] Use httpClient testing concepts in json-api datastore spec --- .../json-api-datastore.service.spec.ts | 552 +++++++++--------- test/datastore-with-config.service.ts | 4 +- 2 files changed, 276 insertions(+), 280 deletions(-) diff --git a/src/services/json-api-datastore.service.spec.ts b/src/services/json-api-datastore.service.spec.ts index 7ec1cd00..8dc56dae 100644 --- a/src/services/json-api-datastore.service.spec.ts +++ b/src/services/json-api-datastore.service.spec.ts @@ -3,31 +3,23 @@ import { format, parse } from 'date-fns'; import { Author } from '../../test/models/author.model'; import { AUTHOR_API_VERSION, AUTHOR_MODEL_ENDPOINT_URL, CustomAuthor } from '../../test/models/custom-author.model'; import { AUTHOR_BIRTH, AUTHOR_ID, AUTHOR_NAME, BOOK_TITLE, getAuthorData } from '../../test/fixtures/author.fixture'; -import { MockBackend, MockConnection } from '@angular/http/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { API_VERSION, BASE_URL, Datastore } from '../../test/datastore.service'; import { ErrorResponse } from '../models/error-response.model'; import { getSampleBook } from '../../test/fixtures/book.fixture'; import { Book } from '../../test/models/book.model'; import { ModelConfig } from '../index'; -import { - BaseRequestOptions, - ConnectionBackend, - Headers, - Http, - RequestMethod, - Response, - ResponseOptions -} from '@angular/http'; import { API_VERSION_FROM_CONFIG, BASE_URL_FROM_CONFIG, DatastoreWithConfig } from '../../test/datastore-with-config.service'; +import { HttpErrorResponse } from '@angular/common/http'; let datastore: Datastore; -let datastoreWithConfig: Datastore; -let backend: MockBackend; +let datastoreWithConfig: DatastoreWithConfig; +let httpMock: HttpTestingController; // workaround, see https://github.com/angular/angular/pull/8961 class MockError extends Response implements Error { @@ -38,83 +30,69 @@ class MockError extends Response implements Error { describe('JsonApiDatastore', () => { beforeEach(() => { TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + ], providers: [ - { - provide: Http, - useFactory: (connectionBackend: ConnectionBackend, defaultOptions: BaseRequestOptions) => { - return new Http(connectionBackend, defaultOptions); - }, - deps: [MockBackend, BaseRequestOptions] - }, - MockBackend, - BaseRequestOptions, Datastore, - DatastoreWithConfig + DatastoreWithConfig, ] }); datastore = TestBed.get(Datastore); datastoreWithConfig = TestBed.get(DatastoreWithConfig); - backend = TestBed.get(MockBackend); + httpMock = TestBed.get(HttpTestingController); }); + afterEach(() => { + httpMock.verify(); + }); describe('query', () => { it('should build basic url from the data from datastore decorator', () => { const authorModelConfig: ModelConfig = Reflect.getMetadata('JsonApiModelConfig', Author); - - backend.connections.subscribe((c: MockConnection) => { - expect(c.request.url).toEqual(`${BASE_URL}/${API_VERSION}/${authorModelConfig.type}`); - expect(c.request.method).toEqual(RequestMethod.Get); - }); + const expectedUrl = `${BASE_URL}/${API_VERSION}/${authorModelConfig.type}`; datastore.query(Author).subscribe(); + + const queryRequest = httpMock.expectOne({ method: 'GET', url: expectedUrl }); + queryRequest.flush({ data: [] }); }); it('should build basic url and apiVersion from the config variable if exists', () => { const authorModelConfig: ModelConfig = Reflect.getMetadata('JsonApiModelConfig', Author); - - backend.connections.subscribe((c: MockConnection) => { - expect(c.request.url).toEqual(`${BASE_URL_FROM_CONFIG}/${API_VERSION_FROM_CONFIG}/${authorModelConfig.type}`); - expect(c.request.method).toEqual(RequestMethod.Get); - }); + const expectedUrl = `${BASE_URL_FROM_CONFIG}/${API_VERSION_FROM_CONFIG}/${authorModelConfig.type}`; datastoreWithConfig.query(Author).subscribe(); + + const queryRequest = httpMock.expectOne({ method: 'GET', url: expectedUrl }); + queryRequest.flush({ data: [] }); }); // tslint:disable-next-line:max-line-length it('should use apiVersion and modelEnpointUrl from the model instead of datastore if model has apiVersion and/or modelEndpointUrl specified', () => { const authorModelConfig: ModelConfig = Reflect.getMetadata('JsonApiModelConfig', CustomAuthor); - - backend.connections.subscribe((c: MockConnection) => { - expect(c.request.url).toEqual(`${BASE_URL_FROM_CONFIG}/${AUTHOR_API_VERSION}/${AUTHOR_MODEL_ENDPOINT_URL}`); - expect(c.request.method).toEqual(RequestMethod.Get); - }); + const expectedUrl = `${BASE_URL_FROM_CONFIG}/${AUTHOR_API_VERSION}/${AUTHOR_MODEL_ENDPOINT_URL}`; datastoreWithConfig.query(CustomAuthor).subscribe(); + + const queryRequest = httpMock.expectOne({ method: 'GET', url: expectedUrl }); + queryRequest.flush({ data: [] }); }); it('should set JSON API headers', () => { - backend.connections.subscribe((c: MockConnection) => { - expect(c.request.url).toEqual(`${BASE_URL}/${API_VERSION}/authors`); - expect(c.request.method).toEqual(RequestMethod.Get); - expect(c.request.headers.get('Content-Type')).toEqual('application/vnd.api+json'); - expect(c.request.headers.get('Accept')).toEqual('application/vnd.api+json'); - }); + const expectedUrl = `${BASE_URL}/${API_VERSION}/authors`; + datastore.query(Author).subscribe(); + + const queryRequest = httpMock.expectOne({ method: 'GET', url: expectedUrl }); + expect(queryRequest.request.headers.get('Content-Type')).toEqual('application/vnd.api+json'); + expect(queryRequest.request.headers.get('Accept')).toEqual('application/vnd.api+json'); + queryRequest.flush({ data: [] }); }); it('should build url with nested params', () => { - backend.connections.subscribe((c: MockConnection) => { - expect(c.request.url).not.toEqual(`${BASE_URL}/${API_VERSION}`); - expect(c.request.url).toEqual(`${BASE_URL}/${API_VERSION}/` + 'authors?' + - encodeURIComponent('page[size]') + '=10&' + - encodeURIComponent('page[number]') + '=1&' + - encodeURIComponent('include') + '=comments&' + - encodeURIComponent('filter[title][keyword]') + '=Tolkien'); - expect(c.request.method).toEqual(RequestMethod.Get); - }); - datastore.query(Author, { + const queryData = { page: { size: 10, number: 1 }, @@ -124,41 +102,44 @@ describe('JsonApiDatastore', () => { keyword: 'Tolkien' } } - }).subscribe(); + }; + const expectedUrl = `${BASE_URL}/${API_VERSION}/` + 'authors?' + + encodeURIComponent('page[size]') + '=10&' + + encodeURIComponent('page[number]') + '=1&' + + encodeURIComponent('include') + '=comments&' + + encodeURIComponent('filter[title][keyword]') + '=Tolkien'; + + datastore.query(Author, queryData).subscribe(); + + httpMock.expectNone(`${BASE_URL}/${API_VERSION}`); + const queryRequest = httpMock.expectOne({ method: 'GET', url: expectedUrl }); + queryRequest.flush({ data: [] }); }); it('should have custom headers', () => { - backend.connections.subscribe((c: MockConnection) => { - expect(c.request.url).toEqual(`${BASE_URL}/${API_VERSION}/authors`); - expect(c.request.method).toEqual(RequestMethod.Get); - expect(c.request.headers.has('Authorization')).toBeTruthy(); - expect(c.request.headers.get('Authorization')).toBe('Bearer'); - }); + const expectedUrl = `${BASE_URL}/${API_VERSION}/authors`; datastore.query(Author, null, new Headers({ Authorization: 'Bearer' })).subscribe(); + + const queryRequest = httpMock.expectOne({ method: 'GET', url: expectedUrl }); + expect(queryRequest.request.headers.get('Authorization')).toEqual('Bearer'); + queryRequest.flush({ data: [] }); }); it('should override base headers', () => { - backend.connections.subscribe((c: MockConnection) => { - expect(c.request.url).toEqual(`${BASE_URL}/${API_VERSION}/authors`); - expect(c.request.method).toEqual(RequestMethod.Get); - expect(c.request.headers.has('Authorization')).toBeTruthy(); - expect(c.request.headers.get('Authorization')).toBe('Basic'); - }); + const expectedUrl = `${BASE_URL}/${API_VERSION}/authors`; + datastore.headers = new Headers({ Authorization: 'Bearer' }); datastore.query(Author, null, new Headers({ Authorization: 'Basic' })).subscribe(); + + const queryRequest = httpMock.expectOne({ method: 'GET', url: expectedUrl }); + expect(queryRequest.request.headers.get('Authorization')).toEqual('Basic'); + queryRequest.flush({ data: [] }); }); it('should get authors', () => { - backend.connections.subscribe((c: MockConnection) => { - c.mockRespond(new Response( - new ResponseOptions({ - body: JSON.stringify({ - data: [getAuthorData()] - }) - }) - )); - }); + const expectedUrl = `${BASE_URL}/${API_VERSION}/authors`; + datastore.query(Author).subscribe((authors) => { expect(authors).toBeDefined(); expect(authors.length).toEqual(1); @@ -166,53 +147,53 @@ describe('JsonApiDatastore', () => { expect(authors[0].name).toEqual(AUTHOR_NAME); expect(authors[1]).toBeUndefined(); }); + + const queryRequest = httpMock.expectOne(expectedUrl); + queryRequest.flush({ data: [getAuthorData()] }); }); it('should get authors with custom metadata', () => { - backend.connections.subscribe((c: MockConnection) => { - c.mockRespond(new Response( - new ResponseOptions({ - body: JSON.stringify({ - data: [getAuthorData()], - meta: { - page: { - number: 1, - size: 1, - total: 1, - last: 1 - } - } - }) - }) - )); - }); + const expectedUrl = `${BASE_URL}/${API_VERSION}/authors`; + datastore.findAll(Author).subscribe((document) => { expect(document).toBeDefined(); expect(document.getModels().length).toEqual(1); expect(document.getMeta().meta.page.number).toEqual(1); }); + + const findAllRequest = httpMock.expectOne(expectedUrl); + findAllRequest.flush({ + data: [getAuthorData()], + meta: { + page: { + number: 1, + size: 1, + total: 1, + last: 1 + } + } + }); }); it('should get data with default metadata', () => { - backend.connections.subscribe((c: MockConnection) => { - c.mockRespond(new Response( - new ResponseOptions({ - body: JSON.stringify({ - data: [getSampleBook(1, '1')], - links: ['http://www.example.org'] - }) - }) - )); - }); + const expectedUrl = `${BASE_URL}/${API_VERSION}/books`; + datastore.findAll(Book).subscribe((document) => { expect(document).toBeDefined(); expect(document.getModels().length).toEqual(1); expect(document.getMeta().links[0]).toEqual('http://www.example.org'); }); + + const findAllRequest = httpMock.expectOne(expectedUrl); + findAllRequest.flush({ + data: [getSampleBook(1, '1')], + links: ['http://www.example.org'] + }); }); it('should fire error', () => { - const resp = { + const expectedUrl = `${BASE_URL}/${API_VERSION}/authors`; + const dummyResponse = { errors: [ { code: '100', @@ -222,171 +203,168 @@ describe('JsonApiDatastore', () => { ] }; - backend.connections.subscribe((c: MockConnection) => { - c.mockError(new MockError( - new ResponseOptions({ - body: JSON.stringify(resp), - status: 500 - }) - )); - }); - datastore.query(Author).subscribe((authors) => fail('onNext has been called'), (response) => { - expect(response).toEqual(jasmine.any(ErrorResponse)); - expect(response.errors.length).toEqual(1); - expect(response.errors[0].code).toEqual(resp.errors[0].code); - expect(response.errors[0].title).toEqual(resp.errors[0].title); - expect(response.errors[0].detail).toEqual(resp.errors[0].detail); - }, () => fail('onCompleted has been called')); + datastore.query(Author).subscribe( + (authors) => fail('onNext has been called'), + (response) => { + expect(response).toEqual(jasmine.any(ErrorResponse)); + expect(response.errors.length).toEqual(1); + expect(response.errors[0].code).toEqual(dummyResponse.errors[0].code); + expect(response.errors[0].title).toEqual(dummyResponse.errors[0].title); + expect(response.errors[0].detail).toEqual(dummyResponse.errors[0].detail); + }, + () => fail('onCompleted has been called') + ); + + const queryRequest = httpMock.expectOne(expectedUrl); + queryRequest.flush(dummyResponse, { status: 500, statusText: 'Internal Server Error' }); }); it('should generate correct query string for array params with findAll', () => { - backend.connections.subscribe((c: MockConnection) => { - const decodedQueryString = decodeURI(c.request.url).split('?')[1]; - const expectedQueryString = 'arrayParam[]=4&arrayParam[]=5&arrayParam[]=6'; - expect(decodedQueryString).toEqual(expectedQueryString); - }); + const expectedQueryString = 'arrayParam[]=4&arrayParam[]=5&arrayParam[]=6'; + const expectedUrl = encodeURI(`${BASE_URL}/${API_VERSION}/books?${expectedQueryString}`); + datastore.findAll(Book, { arrayParam: [4, 5, 6] }).subscribe(); + + const findAllRequest = httpMock.expectOne(expectedUrl); + findAllRequest.flush({ data: [] }); }); it('should generate correct query string for array params with query', () => { - backend.connections.subscribe((c: MockConnection) => { - const decodedQueryString = decodeURI(c.request.url).split('?')[1]; - const expectedQueryString = 'arrayParam[]=4&arrayParam[]=5&arrayParam[]=6'; - expect(decodedQueryString).toEqual(expectedQueryString); - }); + const expectedQueryString = 'arrayParam[]=4&arrayParam[]=5&arrayParam[]=6'; + const expectedUrl = encodeURI(`${BASE_URL}/${API_VERSION}/books?${expectedQueryString}`); + datastore.query(Book, { arrayParam: [4, 5, 6] }).subscribe(); + + const queryRequest = httpMock.expectOne(expectedUrl); + queryRequest.flush({ data: [] }); }); it('should generate correct query string for nested params with findAll', () => { - backend.connections.subscribe((c: MockConnection) => { - const decodedQueryString = decodeURI(c.request.url).split('?')[1]; - const expectedQueryString = 'filter[text]=test123'; - expect(decodedQueryString).toEqual(expectedQueryString); - }); + const expectedQueryString = 'filter[text]=test123'; + const expectedUrl = encodeURI(`${BASE_URL}/${API_VERSION}/books?${expectedQueryString}`); + datastore.findAll(Book, { filter: { text: 'test123' } }).subscribe(); + + const findAllRequest = httpMock.expectOne(expectedUrl); + findAllRequest.flush({ data: [] }); }); it('should generate correct query string for nested array params with findAll', () => { - backend.connections.subscribe((c: MockConnection) => { - const decodedQueryString = decodeURI(c.request.url).split('?')[1]; - const expectedQueryString = 'filter[text][]=1&filter[text][]=2'; - expect(decodedQueryString).toEqual(expectedQueryString); - }); + const expectedQueryString = 'filter[text][]=1&filter[text][]=2'; + const expectedUrl = encodeURI(`${BASE_URL}/${API_VERSION}/books?${expectedQueryString}`); + datastore.findAll(Book, { filter: { text: [1, 2] } }).subscribe(); + + const findAllRequest = httpMock.expectOne(expectedUrl); + findAllRequest.flush({ data: [] }); }); }); describe('findRecord', () => { it('should get author', () => { - backend.connections.subscribe((c: MockConnection) => { - c.mockRespond(new Response( - new ResponseOptions({ - body: JSON.stringify({ - data: getAuthorData() - }) - }) - )); - }); - datastore.findRecord(Author, '1').subscribe((author) => { + const expectedUrl = `${BASE_URL}/${API_VERSION}/authors/${AUTHOR_ID}`; + + datastore.findRecord(Author, AUTHOR_ID).subscribe((author) => { expect(author).toBeDefined(); expect(author.id).toBe(AUTHOR_ID); expect(author.date_of_birth).toEqual(parse(AUTHOR_BIRTH)); }); + + const findRecordRequest = httpMock.expectOne(expectedUrl); + findRecordRequest.flush({ data: getAuthorData() }); }); it('should generate correct query string for array params with findRecord', () => { - backend.connections.subscribe((c: MockConnection) => { - const decodedQueryString = decodeURI(c.request.url).split('?')[1]; - const expectedQueryString = 'arrayParam[]=4&arrayParam[]=5&arrayParam[]=6'; - expect(decodedQueryString).toEqual(expectedQueryString); - }); + const expectedQueryString = 'arrayParam[]=4&arrayParam[]=5&arrayParam[]=6'; + const expectedUrl = encodeURI(`${BASE_URL}/${API_VERSION}/books/1?${expectedQueryString}`); + datastore.findRecord(Book, '1', { arrayParam: [4, 5, 6] }).subscribe(); + + const findRecordRequest = httpMock.expectOne(expectedUrl); + findRecordRequest.flush({ data: getAuthorData() }); }); }); describe('saveRecord', () => { it('should create new author', () => { - backend.connections.subscribe((c: MockConnection) => { - expect(c.request.url).not.toEqual(`${BASE_URL}/${API_VERSION}`); - expect(c.request.url).toEqual(`${BASE_URL}/${API_VERSION}/authors`); - expect(c.request.method).toEqual(RequestMethod.Post); - const obj = c.request.json().data; - expect(obj.attributes.name).toEqual(AUTHOR_NAME); - expect(obj.attributes.dob).toEqual(format(parse(AUTHOR_BIRTH), 'YYYY-MM-DDTHH:mm:ssZ')); - expect(obj.id).toBeUndefined(); - expect(obj.type).toBe('authors'); - expect(obj.relationships).toBeUndefined(); - - c.mockRespond(new Response( - new ResponseOptions({ - status: 201, - body: JSON.stringify({ - data: { - id: '1', - type: 'authors', - attributes: { - name: obj.attributes.name - } - } - }) - }) - )); - }); + const expectedUrl = `${BASE_URL}/${API_VERSION}/authors`; const author = datastore.createRecord(Author, { name: AUTHOR_NAME, date_of_birth: AUTHOR_BIRTH }); + author.save().subscribe((val) => { expect(val.id).toBeDefined(); - expect(val.id).toEqual('1'); - }); + expect(val.id).toEqual(AUTHOR_ID); + }); + + httpMock.expectNone(`${BASE_URL}/${API_VERSION}`); + const saveRequest = httpMock.expectOne({ method: 'POST', url: expectedUrl }); + const obj = saveRequest.request.body.data; + expect(obj.attributes).toBeDefined(); + expect(obj.attributes.name).toEqual(AUTHOR_NAME); + expect(obj.attributes.dob).toEqual(format(parse(AUTHOR_BIRTH), 'YYYY-MM-DDTHH:mm:ssZ')); + expect(obj.id).toBeUndefined(); + expect(obj.type).toBe('authors'); + expect(obj.relationships).toBeUndefined(); + + saveRequest.flush({ + data: { + id: AUTHOR_ID, + type: 'authors', + attributes: { + name: AUTHOR_NAME, + } + } + }, { status: 201, statusText: 'Created' }); }); it('should throw error on new author with 201 response but no body', () => { - backend.connections.subscribe((c: MockConnection) => { - expect(c.request.method).toEqual(RequestMethod.Post); - c.mockRespond(new Response(new ResponseOptions({ status: 201 }))); - }); + const expectedUrl = `${BASE_URL}/${API_VERSION}/authors`; const author = datastore.createRecord(Author, { name: AUTHOR_NAME }); - expect(() => author.save().subscribe()).toThrow(new Error('no body in response')); + + author.save().subscribe( + () => fail('should throw error'), + (error) => expect(error).toEqual(new Error('no body in response')) + ); + + const saveRequest = httpMock.expectOne({ method: 'POST', url: expectedUrl }); + saveRequest.flush(null, { status: 201, statusText: 'Created' }); }); it('should throw error on new author with 201 response but no data', () => { - backend.connections.subscribe((c: MockConnection) => { - expect(c.request.method).toEqual(RequestMethod.Post); - c.mockRespond(new Response(new ResponseOptions({ status: 201, body: {} }))); - }); + const expectedUrl = `${BASE_URL}/${API_VERSION}/authors`; const author = datastore.createRecord(Author, { name: AUTHOR_NAME }); - expect(() => author.save().subscribe()).toThrow(new Error('expected data in response')); + + author.save().subscribe( + () => fail('should throw error'), + (error) => expect(error).toEqual(new Error('expected data in response')) + ); + + const saveRequest = httpMock.expectOne({ method: 'POST', url: expectedUrl }); + saveRequest.flush({}, { status: 201, statusText: 'Created' }); }); it('should create new author with 204 response', () => { - backend.connections.subscribe((c: MockConnection) => { - expect(c.request.method).toEqual(RequestMethod.Post); - c.mockRespond(new Response(new ResponseOptions({ status: 204 }))); - }); + const expectedUrl = `${BASE_URL}/${API_VERSION}/authors`; const author = datastore.createRecord(Author, { name: AUTHOR_NAME }); + author.save().subscribe((val) => { expect(val).toBeDefined(); }); + + const saveRequest = httpMock.expectOne({ method: 'POST', url: expectedUrl }); + saveRequest.flush(null, { status: 204, statusText: 'No Content' }); }); it('should create new author with existing ToMany-relationship', () => { - backend.connections.subscribe((c: MockConnection) => { - const obj = c.request.json().data; - expect(obj.attributes.name).toEqual(AUTHOR_NAME); - expect(obj.id).toBeUndefined(); - expect(obj.type).toBe('authors'); - expect(obj.relationships).toBeDefined(); - expect(obj.relationships.books.data.length).toBe(1); - expect(obj.relationships.books.data[0].id).toBe('10'); - }); + const expectedUrl = `${BASE_URL}/${API_VERSION}/authors`; const author = datastore.createRecord(Author, { name: AUTHOR_NAME }); @@ -394,37 +372,45 @@ describe('JsonApiDatastore', () => { id: '10', title: BOOK_TITLE })]; + author.save().subscribe(); + + const saveRequest = httpMock.expectOne(expectedUrl); + const obj = saveRequest.request.body.data; + expect(obj.attributes.name).toEqual(AUTHOR_NAME); + expect(obj.id).toBeUndefined(); + expect(obj.type).toBe('authors'); + expect(obj.relationships).toBeDefined(); + expect(obj.relationships.books.data.length).toBe(1); + expect(obj.relationships.books.data[0].id).toBe('10'); + + saveRequest.flush(null, { status: 204, statusText: 'No Content' }); }); it('should create new author with new ToMany-relationship', () => { - backend.connections.subscribe((c: MockConnection) => { - const obj = c.request.json().data; - expect(obj.attributes.name).toEqual(AUTHOR_NAME); - expect(obj.id).toBeUndefined(); - expect(obj.type).toBe('authors'); - expect(obj.relationships).toBeDefined(); - expect(obj.relationships.books.data.length).toBe(0); - }); - + const expectedUrl = `${BASE_URL}/${API_VERSION}/authors`; const author = datastore.createRecord(Author, { name: AUTHOR_NAME }); - author.books = [datastore.createRecord(Book, { title: BOOK_TITLE })]; author.save().subscribe(); + + const saveRequest = httpMock.expectOne(expectedUrl); + const obj = saveRequest.request.body.data; + expect(obj.attributes.name).toEqual(AUTHOR_NAME); + expect(obj.id).toBeUndefined(); + expect(obj.type).toBe('authors'); + expect(obj.relationships).toBeDefined(); + expect(obj.relationships.books.data.length).toBe(0); + + saveRequest.flush(null, { status: 204, statusText: 'No Content' }); }); it('should create new author with new ToMany-relationship 2', () => { - backend.connections.subscribe((c: MockConnection) => { - const obj = c.request.json().data; - expect(obj.id).toBeUndefined(); - expect(obj.relationships).toBeDefined(); - expect(obj.relationships.books.data.length).toBe(1); - }); + const expectedUrl = `${BASE_URL}/${API_VERSION}/authors`; const author = datastore.createRecord(Author, { name: AUTHOR_NAME }); @@ -434,42 +420,44 @@ describe('JsonApiDatastore', () => { }), datastore.createRecord(Book, { title: `New book - ${BOOK_TITLE}` })]; + author.save().subscribe(); + + const saveRequest = httpMock.expectOne(expectedUrl); + const obj = saveRequest.request.body.data; + expect(obj.id).toBeUndefined(); + expect(obj.relationships).toBeDefined(); + expect(obj.relationships.books.data.length).toBe(1); + + saveRequest.flush(null, { status: 204, statusText: 'No Content' }); }); - it('should create new author with existing BelongsTo-relationship', () => { - backend.connections.subscribe((c: MockConnection) => { - const obj = c.request.json().data; - expect(obj.attributes.title).toEqual(BOOK_TITLE); - expect(obj.id).toBeUndefined(); - expect(obj.type).toBe('books'); - expect(obj.relationships).toBeDefined(); - expect(obj.relationships.author.data.id).toBe(AUTHOR_ID); - }); + it('should create new book with existing BelongsTo-relationship', () => { + const expectedUrl = `${BASE_URL}/${API_VERSION}/books`; const book = datastore.createRecord(Book, { title: BOOK_TITLE }); book.author = new Author(datastore, { id: AUTHOR_ID }); + book.save().subscribe(); + + const saveRequest = httpMock.expectOne(expectedUrl); + const obj = saveRequest.request.body.data; + expect(obj.attributes.title).toEqual(BOOK_TITLE); + expect(obj.id).toBeUndefined(); + expect(obj.type).toBe('books'); + expect(obj.relationships).toBeDefined(); + expect(obj.relationships.author.data.id).toBe(AUTHOR_ID); + + saveRequest.flush(null, { status: 204, statusText: 'No Content' }); }); }); describe('updateRecord', () => { it('should update author with 200 response (no data)', () => { - backend.connections.subscribe((c: MockConnection) => { - expect(c.request.url).not.toEqual(`${BASE_URL}/${API_VERSION}/authors`); - expect(c.request.url).toEqual(`${BASE_URL}/${API_VERSION}/authors/1`); - expect(c.request.method).toEqual(RequestMethod.Patch); - const obj = c.request.json().data; - expect(obj.attributes.name).toEqual('Rowling'); - expect(obj.attributes.dob).toEqual(format(parse('1965-07-31'), 'YYYY-MM-DDTHH:mm:ssZ')); - expect(obj.id).toBe(AUTHOR_ID); - expect(obj.type).toBe('authors'); - expect(obj.relationships).toBeUndefined(); - c.mockRespond(new Response(new ResponseOptions({ status: 200, body: {} }))); - }); + const expectedUrl = `${BASE_URL}/${API_VERSION}/authors/${AUTHOR_ID}`; const author = new Author(datastore, { id: AUTHOR_ID, attributes: { @@ -479,24 +467,25 @@ describe('JsonApiDatastore', () => { }); author.name = 'Rowling'; author.date_of_birth = parse('1965-07-31'); + author.save().subscribe((val) => { expect(val.name).toEqual(author.name); }); + + httpMock.expectNone(`${BASE_URL}/${API_VERSION}/authors`); + const saveRequest = httpMock.expectOne({ method: 'PATCH', url: expectedUrl }); + const obj = saveRequest.request.body.data; + expect(obj.attributes.name).toEqual('Rowling'); + expect(obj.attributes.dob).toEqual(format(parse('1965-07-31'), 'YYYY-MM-DDTHH:mm:ssZ')); + expect(obj.id).toBe(AUTHOR_ID); + expect(obj.type).toBe('authors'); + expect(obj.relationships).toBeUndefined(); + + saveRequest.flush({}); }); it('should update author with 204 response', () => { - backend.connections.subscribe((c: MockConnection) => { - expect(c.request.url).not.toEqual(`${BASE_URL}/${API_VERSION}/authors`); - expect(c.request.url).toEqual(`${BASE_URL}/${API_VERSION}/authors/1`); - expect(c.request.method).toEqual(RequestMethod.Patch); - const obj = c.request.json().data; - expect(obj.attributes.name).toEqual('Rowling'); - expect(obj.attributes.dob).toEqual(format(parse('1965-07-31'), 'YYYY-MM-DDTHH:mm:ssZ')); - expect(obj.id).toBe(AUTHOR_ID); - expect(obj.type).toBe('authors'); - expect(obj.relationships).toBeUndefined(); - c.mockRespond(new Response(new ResponseOptions({ status: 204 }))); - }); + const expectedUrl = `${BASE_URL}/${API_VERSION}/authors/${AUTHOR_ID}`; const author = new Author(datastore, { id: AUTHOR_ID, attributes: { @@ -506,12 +495,25 @@ describe('JsonApiDatastore', () => { }); author.name = 'Rowling'; author.date_of_birth = parse('1965-07-31'); + author.save().subscribe((val) => { expect(val.name).toEqual(author.name); }); + + httpMock.expectNone(`${BASE_URL}/${API_VERSION}/authors`); + const saveRequest = httpMock.expectOne({ method: 'PATCH', url: expectedUrl }); + const obj = saveRequest.request.body.data; + expect(obj.attributes.name).toEqual('Rowling'); + expect(obj.attributes.dob).toEqual(format(parse('1965-07-31'), 'YYYY-MM-DDTHH:mm:ssZ')); + expect(obj.id).toBe(AUTHOR_ID); + expect(obj.type).toBe('authors'); + expect(obj.relationships).toBeUndefined(); + + saveRequest.flush(null, { status: 204, statusText: 'No Content' }); }); it('should integrate server updates on 200 response', () => { + const expectedUrl = `${BASE_URL}/${API_VERSION}/authors/${AUTHOR_ID}`; const author = new Author(datastore, { id: AUTHOR_ID, attributes: { @@ -522,33 +524,27 @@ describe('JsonApiDatastore', () => { author.name = 'Rowling'; author.date_of_birth = parse('1965-07-31'); - backend.connections.subscribe((c: MockConnection) => { - expect(c.request.url).not.toEqual(`${BASE_URL}/${API_VERSION}/authors`); - expect(c.request.url).toEqual(`${BASE_URL}/${API_VERSION}/authors/1`); - expect(c.request.method).toEqual(RequestMethod.Patch); - const obj = c.request.json().data; - expect(obj.attributes.name).toEqual('Rowling'); - expect(obj.attributes.dob).toEqual(format(parse('1965-07-31'), 'YYYY-MM-DDTHH:mm:ssZ')); - expect(obj.id).toBe(AUTHOR_ID); - expect(obj.type).toBe('authors'); - expect(obj.relationships).toBeUndefined(); - - c.mockRespond(new Response(new ResponseOptions({ - status: 200, - body: { - data: { - id: obj.id, - type: obj.type, - attributes: { - name: 'Potter', - } - } - } - }))); - }); author.save().subscribe((val) => { expect(val.name).toEqual('Potter'); }); + + httpMock.expectNone(`${BASE_URL}/${API_VERSION}/authors`); + const saveRequest = httpMock.expectOne({ method: 'PATCH', url: expectedUrl }); + const obj = saveRequest.request.body.data; + expect(obj.attributes.name).toEqual('Rowling'); + expect(obj.attributes.dob).toEqual(format(parse('1965-07-31'), 'YYYY-MM-DDTHH:mm:ssZ')); + expect(obj.id).toBe(AUTHOR_ID); + expect(obj.type).toBe('authors'); + expect(obj.relationships).toBeUndefined(); + + saveRequest.flush({ + data: { + id: obj.id, + attributes: { + name: 'Potter', + } + } + }); }); }); }); diff --git a/test/datastore-with-config.service.ts b/test/datastore-with-config.service.ts index 7fd743ef..9dcc548a 100644 --- a/test/datastore-with-config.service.ts +++ b/test/datastore-with-config.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Http } from '@angular/http'; +import { HttpClient } from '@angular/common/http'; import { JsonApiDatastore, JsonApiDatastoreConfig, DatastoreConfig } from '../src'; import { Author } from './models/author.model'; import { Book } from './models/book.model'; @@ -26,7 +26,7 @@ export class DatastoreWithConfig extends JsonApiDatastore { apiVersion: API_VERSION_FROM_CONFIG }; - constructor(http: Http) { + constructor(http: HttpClient) { super(http); } } From 2b158e9c95110e409275ad54ca73e739563fc084 Mon Sep 17 00:00:00 2001 From: Friedrich Pawelka Date: Mon, 12 Mar 2018 15:10:17 +0100 Subject: [PATCH 6/7] Add information about the new httpClient --- README.MD | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.MD b/README.MD index 06900cf7..d72f7ab0 100644 --- a/README.MD +++ b/README.MD @@ -89,7 +89,7 @@ export class AppModule { } Firstly, create your `Datastore` service: - Extend the `JsonApiDatastore` class - Decorate it with `@JsonApiDatastoreConfig`, set the `baseUrl` for your APIs and map your models (Optional: you can set `apiVersion`, `baseUrl` will be suffixed with it) -- Pass the `Http` depencency to the parent constructor. +- Pass the `HttpClient` depencency to the parent constructor. ```typescript import { JsonApiDatastoreConfig, JsonApiDatastore, DatastoreConfig } from 'angular2-jsonapi'; @@ -107,7 +107,7 @@ const config: DatastoreConfig = { @JsonApiDatastoreConfig(config) export class Datastore extends JsonApiDatastore { - constructor(http: Http) { + constructor(http: HttpClient) { super(http); } @@ -547,8 +547,8 @@ To lint all `*.ts` files: $ npm run lint ``` -## Additional tools -* Gem for generating the model definitions based on active model serializers: https://github.com/oncore-education/jsonapi_models +## Additional tools +* Gem for generating the model definitions based on active model serializers: https://github.com/oncore-education/jsonapi_models ## Thanks From d13964e229b0e4199c1d08021907d2d448f9cb53 Mon Sep 17 00:00:00 2001 From: Friedrich Pawelka Date: Mon, 12 Mar 2018 15:35:06 +0100 Subject: [PATCH 7/7] Remove http dependecy --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 6ff2cc2b..fca802a6 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "@angular/compiler": "^4.4.3", "@angular/compiler-cli": "^4.4.3", "@angular/core": "^4.4.3", - "@angular/http": "^4.4.3", "@angular/platform-browser": "^4.4.3", "@angular/platform-browser-dynamic": "^4.4.3", "@angular/platform-server": "^4.4.3",