Skip to content

Commit

Permalink
refactor: supply operators for resolving links via ApiService
Browse files Browse the repository at this point in the history
BREAKING CHANGE: pipable operators for resolving links are now ApiService members
  • Loading branch information
dhhyi committed Jun 16, 2020
1 parent a5bfff5 commit 82e0de7
Show file tree
Hide file tree
Showing 12 changed files with 91 additions and 185 deletions.
6 changes: 3 additions & 3 deletions src/app/core/services/address/address.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ describe('Address Service', () => {

beforeEach(() => {
apiService = mock(ApiService);
when(apiService.icmServerURL).thenReturn('http://server');
addressService = new AddressService(instance(apiService));
});

it("should get addresses data when 'getCustomerAddresses' is called", done => {
when(apiService.get(`customers/-/addresses`)).thenReturn(of([]));
when(apiService.resolveLinks()).thenReturn(() => of([]));

addressService.getCustomerAddresses().subscribe(() => {
verify(apiService.get(`customers/-/addresses`)).once();
Expand All @@ -29,11 +29,11 @@ describe('Address Service', () => {
when(apiService.post(`customers/-/addresses`, anything())).thenReturn(
of({ type: 'Link', uri: 'site/-/customers/-/addresses/addressid' })
);
when(apiService.get(anything())).thenReturn(of(BasketMockData.getAddress()));
when(apiService.resolveLink()).thenReturn(() => of(BasketMockData.getAddress()));

addressService.createCustomerAddress('-', BasketMockData.getAddress()).subscribe(data => {
verify(apiService.post(`customers/-/addresses`, anything())).once();
verify(apiService.get('http://server/site/-/customers/-/addresses/addressid')).once();
verify(apiService.resolveLink()).once();
expect(data).toHaveProperty('firstName', 'Patricia');
done();
});
Expand Down
6 changes: 3 additions & 3 deletions src/app/core/services/address/address.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { map, mapTo } from 'rxjs/operators';
import { AddressMapper } from 'ish-core/models/address/address.mapper';
import { Address } from 'ish-core/models/address/address.model';
import { Link } from 'ish-core/models/link/link.model';
import { ApiService, resolveLink, resolveLinks, unpackEnvelope } from 'ish-core/services/api/api.service';
import { ApiService, unpackEnvelope } from 'ish-core/services/api/api.service';

/**
* The Address Service handles the interaction with the REST API concerning addresses.
Expand All @@ -22,7 +22,7 @@ export class AddressService {
getCustomerAddresses(customerId: string = '-'): Observable<Address[]> {
return this.apiService.get(`customers/${customerId}/addresses`).pipe(
unpackEnvelope<Link>(),
resolveLinks<Address>(this.apiService),
this.apiService.resolveLinks<Address>(),
map(addressesData => addressesData.map(AddressMapper.fromData))
);
}
Expand All @@ -41,7 +41,7 @@ export class AddressService {

return this.apiService
.post(`customers/${customerId}/addresses`, customerAddress)
.pipe(resolveLink<Address>(this.apiService), map(AddressMapper.fromData));
.pipe(this.apiService.resolveLink<Address>(), map(AddressMapper.fromData));
}

/**
Expand Down
16 changes: 8 additions & 8 deletions src/app/core/services/api/api.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { serverError } from 'ish-core/store/core/error';
import { CustomerStoreModule } from 'ish-core/store/customer/customer-store.module';
import { getAPIToken, getPGID, setAPIToken } from 'ish-core/store/customer/user';

import { ApiService, resolveLink, resolveLinks, unpackEnvelope } from './api.service';
import { ApiService, unpackEnvelope } from './api.service';
import { ApiServiceErrorHandler } from './api.service.errorhandler';

describe('Api Service', () => {
Expand Down Expand Up @@ -171,7 +171,7 @@ describe('Api Service', () => {
});
});

describe('API Service Pipable Operators', () => {
describe('API Service Pipeable Operators', () => {
let httpTestingController: HttpTestingController;
let apiService: ApiService;

Expand Down Expand Up @@ -263,7 +263,7 @@ describe('Api Service', () => {
it('should perform both operations when requested', done => {
apiService
.get('categories')
.pipe(unpackEnvelope(), resolveLinks(apiService))
.pipe(unpackEnvelope(), apiService.resolveLinks())
.subscribe(data => {
expect(data).toEqual([webcamResponse]);
done();
Expand All @@ -279,7 +279,7 @@ describe('Api Service', () => {
it('should filter out elements that are not links when doing link translation', done => {
apiService
.get('something')
.pipe(resolveLinks(apiService))
.pipe(apiService.resolveLinks())
.subscribe(data => {
expect(data).toHaveLength(1);
done();
Expand All @@ -296,7 +296,7 @@ describe('Api Service', () => {
it('should return empty array on link translation when no links are available', done => {
apiService
.get('something')
.pipe(resolveLinks(apiService))
.pipe(apiService.resolveLinks())
.subscribe(data => {
expect(data).toBeEmpty();
done();
Expand All @@ -309,7 +309,7 @@ describe('Api Service', () => {
it('should return empty array on element and link translation when source is empty', done => {
apiService
.get('categories')
.pipe(unpackEnvelope(), resolveLinks(apiService))
.pipe(unpackEnvelope(), apiService.resolveLinks())
.subscribe(data => {
expect(data).toBeEmpty();
done();
Expand All @@ -322,7 +322,7 @@ describe('Api Service', () => {
it('should resolve data when resolveLink is used', done => {
apiService
.get('something')
.pipe(resolveLink(apiService))
.pipe(apiService.resolveLink())
.subscribe(data => {
expect(data).toHaveProperty('data', 'dummy');
done();
Expand All @@ -336,7 +336,7 @@ describe('Api Service', () => {
it('should not resolve data when resolveLink is used and an invalid link is supplied', done => {
apiService
.get('something')
.pipe(resolveLink(apiService))
.pipe(apiService.resolveLink())
.subscribe(
fail,
err => {
Expand Down
94 changes: 45 additions & 49 deletions src/app/core/services/api/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
of,
throwError,
} from 'rxjs';
import { catchError, concatMap, first, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { catchError, concatMap, first, map, tap, withLatestFrom } from 'rxjs/operators';

import { Captcha } from 'ish-core/models/captcha/captcha.model';
import { Link } from 'ish-core/models/link/link.model';
Expand All @@ -22,55 +22,14 @@ import { getAPIToken, getPGID } from 'ish-core/store/customer/user';
import { ApiServiceErrorHandler } from './api.service.errorhandler';

/**
* Pipeable operator for elements translation (removing the envelop).
* Pipeable operator for elements translation (removing the envelope).
* @param key the name of the envelope (default 'elements')
* @returns The items of an elements array without the elements wrapper.
*/
export function unpackEnvelope<T>(key: string = 'elements'): OperatorFunction<{}, T[]> {
return map(data => (!!data && !!data[key] && !!data[key].length ? data[key] : []));
}

/**
* Pipable operator for link translation (resolving one single link).
* @param apiService The API service to be used for the link translation.
* @returns The link resolved to its actual REST response data.
*/
export function resolveLink<T>(apiService: ApiService): OperatorFunction<Link, T> {
return switchMap(link =>
iif(
// check if link data is properly formatted
() => !!link && link.type === 'Link' && !!link.uri,
// flat map to API request
apiService.get<T>(`${apiService.icmServerURL}/${link.uri}`),
// throw if link is not properly supplied
throwError(new Error('link was not properly formatted'))
)
);
}

/**
* Pipable operator for link translation (resolving the links).
* @param apiService The API service to be used for the link translation.
* @returns The links resolved to their actual REST response data.
*/
export function resolveLinks<T>(apiService: ApiService): OperatorFunction<Link[], T[]> {
return source$ =>
source$.pipe(
// filter for all real Link elements
map(links => links.filter(el => !!el && el.type === 'Link' && !!el.uri)),
// transform Link elements to API Observables
map(links => links.map(item => apiService.get<T>(`${apiService.icmServerURL}/${item.uri}`))),
// flatten to API requests O<O<T>[]> -> O<T[]>
switchMap(obsArray => iif(() => !!obsArray.length, forkJoin(obsArray), of([])))
);
}

function catchApiError<T>(handler: ApiServiceErrorHandler) {
return (source$: Observable<T>) =>
// tslint:disable-next-line:ban
source$.pipe(catchError(error => handler.dispatchCommunicationErrors<T>(error)));
}

export interface AvailableOptions {
params?: HttpParams;
headers?: HttpHeaders;
Expand All @@ -86,17 +45,13 @@ export class ApiService {
static TOKEN_HEADER_KEY = 'authentication-token';
static AUTHORIZATION_HEADER_KEY = 'Authorization';

icmServerURL: string;

private executionBarrier$: Observable<void> | Subject<void> = of(undefined);

constructor(
private httpClient: HttpClient,
private apiServiceErrorHandler: ApiServiceErrorHandler,
private store: Store
) {
store.pipe(select(getICMServerURL)).subscribe(url => (this.icmServerURL = url));
}
) {}

/**
* appends API token to requests if available and request is not an authorization request
Expand Down Expand Up @@ -160,7 +115,10 @@ export class ApiService {
private execute<T>(options: AvailableOptions, httpCall$: Observable<T>): Observable<T> {
const wrappedCall$ = options?.skipApiErrorHandling
? httpCall$
: httpCall$.pipe(catchApiError(this.apiServiceErrorHandler));
: httpCall$.pipe(
// tslint:disable-next-line:ban
catchError(error => this.apiServiceErrorHandler.dispatchCommunicationErrors<T>(error))
);

if (options?.runExclusively) {
// setup a barrier for other calls
Expand Down Expand Up @@ -300,4 +258,42 @@ export class ApiService {
)
);
}

/**
* Pipeable operator for link translation (resolving one single link).
* @returns The link resolved to its actual REST response data.
*/
resolveLink<T>(): OperatorFunction<Link, T> {
return stream$ =>
stream$.pipe(
withLatestFrom(this.store.pipe(select(getICMServerURL))),
concatMap(([link, icmServerURL]) =>
iif(
// check if link data is properly formatted
() => link?.type === 'Link' && !!link.uri,
// flat map to API request
this.get<T>(`${icmServerURL}/${link.uri}`),
// throw if link is not properly supplied
throwError(new Error('link was not properly formatted'))
)
)
);
}

/**
* Pipeable operator for link translation (resolving multiple links).
* @returns The links resolved to their actual REST response data.
*/
resolveLinks<T>(): OperatorFunction<Link[], T[]> {
return source$ =>
source$.pipe(
// filter for all real Link elements
map(links => links.filter(el => el?.type === 'Link' && !!el.uri)),
withLatestFrom(this.store.pipe(select(getICMServerURL))),
// transform Link elements to API Observables
map(([links, icmServerURL]) => links.map(item => this.get<T>(`${icmServerURL}/${item.uri}`))),
// flatten to API requests O<O<T>[]> -> O<T[]>
concatMap(obsArray => iif(() => !!obsArray.length, forkJoin(obsArray), of([])))
);
}
}
1 change: 0 additions & 1 deletion src/app/core/services/order/order.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ describe('Order Service', () => {

beforeEach(() => {
apiService = mock(ApiService);
when(apiService.icmServerURL).thenReturn('http://server');

TestBed.configureTestingModule({
providers: [
Expand Down
3 changes: 2 additions & 1 deletion src/app/core/services/payment/payment.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ describe('Payment Service', () => {
describe('user payment service', () => {
it("should get a user's payment method data when 'getUserPaymentMethods' is called", done => {
when(apiService.get(anyString())).thenReturn(of([]));
when(apiService.resolveLinks()).thenReturn(() => of([]));
when(apiService.options(anyString())).thenReturn(of([]));
const customer = {
customerNo: '4711',
Expand All @@ -208,7 +209,7 @@ describe('Payment Service', () => {
when(apiService.post(`customers/${customerNo}/payments`, anything())).thenReturn(
of({ type: 'Link', uri: 'site/-/customers/-/payments/paymentid' })
);
when(apiService.get(anything())).thenReturn(of(undefined));
when(apiService.resolveLink()).thenReturn(() => of(undefined));

paymentService.createUserPayment(customerNo, newPaymentInstrument).subscribe(() => {
verify(apiService.post(`customers/${customerNo}/payments`, anything())).once();
Expand Down
6 changes: 3 additions & 3 deletions src/app/core/services/payment/payment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import { PaymentMethodMapper } from 'ish-core/models/payment-method/payment-method.mapper';
import { PaymentMethod } from 'ish-core/models/payment-method/payment-method.model';
import { Payment } from 'ish-core/models/payment/payment.model';
import { ApiService, resolveLink, resolveLinks, unpackEnvelope } from 'ish-core/services/api/api.service';
import { ApiService, unpackEnvelope } from 'ish-core/services/api/api.service';
import { getCurrentLocale } from 'ish-core/store/core/configuration';

/**
Expand Down Expand Up @@ -250,7 +250,7 @@ export class PaymentService {

return this.apiService.get(`customers/${customer.customerNo}/payments`).pipe(
unpackEnvelope<Link>(),
resolveLinks<PaymentInstrumentData>(this.apiService),
this.apiService.resolveLinks<PaymentInstrumentData>(),
concatMap(instruments =>
this.apiService.options(`customers/${customer.customerNo}/payments`).pipe(
unpackEnvelope<PaymentMethodOptionsDataType>('methods'),
Expand Down Expand Up @@ -291,7 +291,7 @@ export class PaymentService {

return this.apiService
.post(`customers/${customerNo}/payments`, body)
.pipe(resolveLink<PaymentInstrument>(this.apiService));
.pipe(this.apiService.resolveLink<PaymentInstrument>());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ describe('Quote Request Service', () => {

beforeEach(() => {
apiService = mock(ApiService);
when(apiService.icmServerURL).thenReturn('BASE');

TestBed.configureTestingModule({
imports: [
Expand Down Expand Up @@ -163,13 +162,12 @@ describe('Quote Request Service', () => {
elements: [{ type: 'Link', uri: 'customers/CID/users/UID/quoterequests/QRID' }],
})
);
when(apiService.get(`BASE/customers/CID/users/UID/quoterequests/QRID`)).thenReturn(of({ id: 'QRID' }));
when(apiService.resolveLinks()).thenReturn(() => of([{ id: 'QRID' }]));

quoteRequestService.getQuoteRequests().subscribe(data => {
expect(data).toHaveLength(1);
expect(data[0].id).toEqual('QRID');
verify(apiService.get(`customers/CID/users/UID/quoterequests`)).once();
verify(apiService.get(`BASE/customers/CID/users/UID/quoterequests/QRID`)).once();
done();
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { concatMap, map, mapTo, shareReplay, take } from 'rxjs/operators';

import { LineItemUpdate } from 'ish-core/models/line-item-update/line-item-update.model';
import { Link } from 'ish-core/models/link/link.model';
import { ApiService, resolveLinks, unpackEnvelope } from 'ish-core/services/api/api.service';
import { ApiService, unpackEnvelope } from 'ish-core/services/api/api.service';
import { getLoggedInCustomer, getLoggedInUser } from 'ish-core/store/customer/user';
import { waitForFeatureStore, whenFalsy } from 'ish-core/utils/operators';

Expand Down Expand Up @@ -61,7 +61,7 @@ export class QuoteRequestService {
concatMap(({ userId, customerId }) =>
this.apiService
.get(`customers/${customerId}/users/${userId}/quoterequests`)
.pipe(unpackEnvelope(), resolveLinks<QuoteRequestData>(this.apiService))
.pipe(unpackEnvelope(), this.apiService.resolveLinks<QuoteRequestData>())
)
);
}
Expand Down
Loading

0 comments on commit 82e0de7

Please sign in to comment.