initial codebase
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
|
||||
|
||||
import { environment } from '@env/environment';
|
||||
import { ApiPrefixInterceptor } from './api-prefix.interceptor';
|
||||
|
||||
describe('ApiPrefixInterceptor', () => {
|
||||
let http: HttpClient;
|
||||
let httpMock: HttpTestingController;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
providers: [{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: ApiPrefixInterceptor,
|
||||
multi: true
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(inject([
|
||||
HttpClient,
|
||||
HttpTestingController
|
||||
], (_http: HttpClient,
|
||||
_httpMock: HttpTestingController) => {
|
||||
|
||||
http = _http;
|
||||
httpMock = _httpMock;
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
it('should prepend environment.serverUrl to the request url', () => {
|
||||
// Act
|
||||
http.get('/toto').subscribe();
|
||||
|
||||
// Assert
|
||||
httpMock.expectOne({ url: environment.serverUrl + '/toto' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { environment } from '@env/environment';
|
||||
|
||||
/**
|
||||
* Prefixes all requests with `environment.serverUrl`.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ApiPrefixInterceptor implements HttpInterceptor {
|
||||
|
||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
request = request.clone({ url: environment.serverUrl + request.url });
|
||||
return next.handle(request);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
import { HTTP_INTERCEPTORS, HttpClient, HttpResponse } from '@angular/common/http';
|
||||
|
||||
import { CacheInterceptor } from './cache.interceptor';
|
||||
import { HttpCacheService } from './http-cache.service';
|
||||
|
||||
describe('CacheInterceptor', () => {
|
||||
let interceptorOptions: Object | null = {};
|
||||
let httpCacheService: HttpCacheService;
|
||||
let cacheInterceptor: CacheInterceptor;
|
||||
let http: HttpClient;
|
||||
let httpMock: HttpTestingController;
|
||||
|
||||
function createInterceptor(_httpCacheService: HttpCacheService) {
|
||||
cacheInterceptor = new CacheInterceptor(_httpCacheService).configure(interceptorOptions);
|
||||
return cacheInterceptor;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
providers: [
|
||||
HttpCacheService,
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useFactory: createInterceptor,
|
||||
deps: [HttpCacheService],
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpCacheService.cleanCache();
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
describe('with default configuration', () => {
|
||||
beforeEach(() => {
|
||||
interceptorOptions = null;
|
||||
});
|
||||
|
||||
beforeEach(inject([
|
||||
HttpClient,
|
||||
HttpTestingController,
|
||||
HttpCacheService
|
||||
], (_http: HttpClient,
|
||||
_httpMock: HttpTestingController,
|
||||
_httpCacheService: HttpCacheService) => {
|
||||
|
||||
http = _http;
|
||||
httpMock = _httpMock;
|
||||
httpCacheService = _httpCacheService;
|
||||
}));
|
||||
|
||||
it('should cache the request', () => {
|
||||
// Act
|
||||
http.get('/toto').subscribe(() => {
|
||||
// Assert
|
||||
const cachedData = httpCacheService.getCacheData('/toto');
|
||||
expect(cachedData).toBeDefined();
|
||||
expect(cachedData ? cachedData.body : null).toEqual('someData');
|
||||
});
|
||||
|
||||
httpMock.expectOne({ url: '/toto' }).flush('someData');
|
||||
});
|
||||
|
||||
it('should respond from the cache', () => {
|
||||
// Arrange
|
||||
httpCacheService.setCacheData('/toto', new HttpResponse({ body: 'cachedData' }));
|
||||
|
||||
// Act
|
||||
http.get('/toto').subscribe(response => {
|
||||
// Assert
|
||||
expect(response).toEqual('cachedData');
|
||||
});
|
||||
|
||||
httpMock.expectNone({ url: '/toto' });
|
||||
});
|
||||
|
||||
it('should not cache the request in case of error', () => {
|
||||
// Act
|
||||
http.get('/toto').subscribe(() => {}, () => {
|
||||
// Assert
|
||||
expect(httpCacheService.getCacheData('/toto')).toBeNull();
|
||||
});
|
||||
|
||||
httpMock.expectOne({}).flush(null, {
|
||||
status: 404,
|
||||
statusText: 'error'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with update forced configuration', () => {
|
||||
beforeEach(() => {
|
||||
interceptorOptions = { update: true };
|
||||
});
|
||||
|
||||
beforeEach(inject([
|
||||
HttpClient,
|
||||
HttpTestingController,
|
||||
HttpCacheService
|
||||
], (_http: HttpClient,
|
||||
_httpMock: HttpTestingController,
|
||||
_httpCacheService: HttpCacheService) => {
|
||||
|
||||
http = _http;
|
||||
httpMock = _httpMock;
|
||||
httpCacheService = _httpCacheService;
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
httpCacheService.cleanCache();
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
it('should force cache update', () => {
|
||||
// Arrange
|
||||
httpCacheService.setCacheData('/toto', new HttpResponse({ body: 'oldCachedData' }));
|
||||
cacheInterceptor.configure({ update: true });
|
||||
|
||||
// Act
|
||||
http.get('/toto').subscribe(response => {
|
||||
// Assert
|
||||
expect(response).toEqual('newData');
|
||||
|
||||
});
|
||||
|
||||
httpMock.expectOne({ url: '/toto' }).flush('newData');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse } from '@angular/common/http';
|
||||
import { Observable, Subscriber } from 'rxjs';
|
||||
|
||||
import { HttpCacheService } from './http-cache.service';
|
||||
|
||||
/**
|
||||
* Caches HTTP requests.
|
||||
* Use ExtendedHttpClient fluent API to configure caching for each request.
|
||||
*/
|
||||
@Injectable()
|
||||
export class CacheInterceptor implements HttpInterceptor {
|
||||
|
||||
private forceUpdate = false;
|
||||
|
||||
constructor(private httpCacheService: HttpCacheService) { }
|
||||
|
||||
/**
|
||||
* Configures interceptor options
|
||||
* @param {{update: boolean}} options If update option is enabled, forces request to be made and updates cache entry.
|
||||
* @return {CacheInterceptor} The configured instance.
|
||||
*/
|
||||
configure(options?: { update?: boolean } | null): CacheInterceptor {
|
||||
const instance = new CacheInterceptor(this.httpCacheService);
|
||||
if (options && options.update) {
|
||||
instance.forceUpdate = true;
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
if (request.method !== 'GET') {
|
||||
return next.handle(request);
|
||||
}
|
||||
|
||||
return new Observable((subscriber: Subscriber<HttpEvent<any>>) => {
|
||||
const cachedData = this.forceUpdate ? null : this.httpCacheService.getCacheData(request.urlWithParams);
|
||||
if (cachedData !== null) {
|
||||
// Create new response to avoid side-effects
|
||||
subscriber.next(new HttpResponse(cachedData as Object));
|
||||
subscriber.complete();
|
||||
} else {
|
||||
next.handle(request)
|
||||
.subscribe(
|
||||
event => {
|
||||
if (event instanceof HttpResponse) {
|
||||
this.httpCacheService.setCacheData(request.urlWithParams, event);
|
||||
}
|
||||
subscriber.next(event);
|
||||
},
|
||||
error => subscriber.error(error),
|
||||
() => subscriber.complete()
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
|
||||
|
||||
import { ErrorHandlerInterceptor } from './error-handler.interceptor';
|
||||
|
||||
describe('ErrorHandlerInterceptor', () => {
|
||||
let errorHandlerInterceptor: ErrorHandlerInterceptor;
|
||||
let http: HttpClient;
|
||||
let httpMock: HttpTestingController;
|
||||
|
||||
function createInterceptor() {
|
||||
errorHandlerInterceptor = new ErrorHandlerInterceptor();
|
||||
return errorHandlerInterceptor;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
providers: [{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useFactory: createInterceptor,
|
||||
multi: true
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(inject([
|
||||
HttpClient,
|
||||
HttpTestingController
|
||||
], (_http: HttpClient,
|
||||
_httpMock: HttpTestingController) => {
|
||||
|
||||
http = _http;
|
||||
httpMock = _httpMock;
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
it('should catch error and call error handler', () => {
|
||||
// Arrange
|
||||
// Note: here we spy on private method since target is customization here,
|
||||
// but you should replace it by actual behavior in your app
|
||||
spyOn(ErrorHandlerInterceptor.prototype as any, 'errorHandler').and.callThrough();
|
||||
|
||||
// Act
|
||||
http.get('/toto').subscribe(() => fail('should error'), () => {
|
||||
// Assert
|
||||
expect(ErrorHandlerInterceptor.prototype['errorHandler']).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
httpMock.expectOne({}).flush(null, {
|
||||
status: 404,
|
||||
statusText: 'error'
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
import { environment } from '@env/environment';
|
||||
import { Logger } from '../logger.service';
|
||||
|
||||
const log = new Logger('ErrorHandlerInterceptor');
|
||||
|
||||
/**
|
||||
* Adds a default error handler to all requests.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ErrorHandlerInterceptor implements HttpInterceptor {
|
||||
|
||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
return next.handle(request).pipe(catchError(error => this.errorHandler(error)));
|
||||
}
|
||||
|
||||
// Customize the default error handler here if needed
|
||||
private errorHandler(response: HttpEvent<any>): Observable<HttpEvent<any>> {
|
||||
if (!environment.production) {
|
||||
// Do something with the error
|
||||
log.error('Request error', response);
|
||||
}
|
||||
throw response;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
import { HttpResponse } from '@angular/common/http';
|
||||
|
||||
import { HttpCacheService, HttpCacheEntry } from './http-cache.service';
|
||||
|
||||
const cachePersistenceKey = 'httpCache';
|
||||
|
||||
describe('HttpCacheService', () => {
|
||||
let httpCacheService: HttpCacheService;
|
||||
let response: HttpResponse<any>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [HttpCacheService]
|
||||
});
|
||||
|
||||
// Start fresh
|
||||
window.sessionStorage.removeItem(cachePersistenceKey);
|
||||
window.localStorage.removeItem(cachePersistenceKey);
|
||||
});
|
||||
|
||||
beforeEach(inject([HttpCacheService], (_httpCacheService: HttpCacheService) => {
|
||||
httpCacheService = _httpCacheService;
|
||||
|
||||
response = new HttpResponse({ body: 'data' });
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
httpCacheService.cleanCache();
|
||||
});
|
||||
|
||||
describe('setCacheData', () => {
|
||||
it('should set cache data', () => {
|
||||
// Act
|
||||
httpCacheService.setCacheData('/popo', response);
|
||||
|
||||
// Assert
|
||||
expect(httpCacheService.getCacheData('/popo')).toEqual(response);
|
||||
});
|
||||
|
||||
it('should replace existing data', () => {
|
||||
// Arrange
|
||||
const newResponse = new HttpResponse({ body: 'new data' });
|
||||
|
||||
// Act
|
||||
httpCacheService.setCacheData('/popo', response);
|
||||
httpCacheService.setCacheData('/popo', newResponse);
|
||||
|
||||
// Assert
|
||||
expect(httpCacheService.getCacheData('/popo')).toEqual(newResponse);
|
||||
});
|
||||
|
||||
it('should set cache date correctly', () => {
|
||||
// Act
|
||||
const date = new Date(123);
|
||||
httpCacheService.setCacheData('/popo', response, date);
|
||||
httpCacheService.setCacheData('/hoho', response);
|
||||
|
||||
// Assert
|
||||
expect((<HttpCacheEntry>httpCacheService.getHttpCacheEntry('/popo')).lastUpdated).toBe(date);
|
||||
expect((<HttpCacheEntry>httpCacheService.getHttpCacheEntry('/hoho')).lastUpdated).not.toBe(date);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCacheData', () => {
|
||||
it('should return null if no cache', () => {
|
||||
expect(httpCacheService.getCacheData('/hoho')).toBe(null);
|
||||
});
|
||||
|
||||
it('should return cached data if exists', () => {
|
||||
// Act
|
||||
httpCacheService.setCacheData('/hoho', response);
|
||||
|
||||
// Assert
|
||||
expect(httpCacheService.getCacheData('/hoho')).toEqual(response);
|
||||
});
|
||||
|
||||
it('should return cached data with url parameters if exists', () => {
|
||||
// Act
|
||||
httpCacheService.setCacheData('/hoho?pif=paf', response);
|
||||
|
||||
// Assert
|
||||
expect(httpCacheService.getCacheData('/hoho?pif=paf')).toEqual(response);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHttpCacheEntry', () => {
|
||||
it('should return null if no cache', () => {
|
||||
expect(httpCacheService.getHttpCacheEntry('/hoho')).toBe(null);
|
||||
});
|
||||
|
||||
it('should return cached data date if exists', () => {
|
||||
// Arrange
|
||||
const date = new Date(123);
|
||||
|
||||
// Act
|
||||
httpCacheService.setCacheData('/hoho', response, date);
|
||||
const entry = httpCacheService.getHttpCacheEntry('/hoho');
|
||||
|
||||
// Assert
|
||||
expect(entry).not.toBeNull();
|
||||
expect((<HttpCacheEntry>entry).lastUpdated).toEqual(date);
|
||||
expect((<HttpCacheEntry>entry).data).toEqual(response);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearCacheData', () => {
|
||||
it('should clear existing cache data', () => {
|
||||
// Set cache
|
||||
httpCacheService.setCacheData('/hoho', response);
|
||||
expect(httpCacheService.getCacheData('/hoho')).toEqual(response);
|
||||
|
||||
// Clear cache
|
||||
httpCacheService.clearCache('/hoho');
|
||||
expect(httpCacheService.getCacheData('/hoho')).toBe(null);
|
||||
});
|
||||
|
||||
it('should do nothing if no cache exists', () => {
|
||||
expect(httpCacheService.getCacheData('/lolo')).toBe(null);
|
||||
httpCacheService.clearCache('/hoho');
|
||||
expect(httpCacheService.getCacheData('/lolo')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanCache', () => {
|
||||
it('should clear all cache if no date is specified', () => {
|
||||
// Set cache
|
||||
httpCacheService.setCacheData('/hoho', response);
|
||||
httpCacheService.setCacheData('/popo', response);
|
||||
expect(httpCacheService.getCacheData('/hoho')).toBe(response);
|
||||
expect(httpCacheService.getCacheData('/popo')).toBe(response);
|
||||
|
||||
// Clean cache
|
||||
httpCacheService.cleanCache();
|
||||
expect(httpCacheService.getCacheData('/hoho')).toBe(null);
|
||||
expect(httpCacheService.getCacheData('/popo')).toBe(null);
|
||||
});
|
||||
|
||||
it('should clear existing since specified date', () => {
|
||||
// Set cache
|
||||
httpCacheService.setCacheData('/hoho', response);
|
||||
expect(httpCacheService.getCacheData('/hoho')).toBe(response);
|
||||
|
||||
// Clean cache
|
||||
httpCacheService.cleanCache(new Date());
|
||||
expect(httpCacheService.getCacheData('/hoho')).toBe(null);
|
||||
});
|
||||
|
||||
it('should not affect cache entries newer than specified date', () => {
|
||||
// Set cache
|
||||
httpCacheService.setCacheData('/hoho', response);
|
||||
expect(httpCacheService.getCacheData('/hoho')).toBe(response);
|
||||
|
||||
// Clean cache
|
||||
const date = new Date();
|
||||
httpCacheService.setCacheData('/lolo', response, new Date(date.getTime() + 10));
|
||||
httpCacheService.cleanCache(date);
|
||||
|
||||
// Assert
|
||||
expect(httpCacheService.getCacheData('/hoho')).toBe(null);
|
||||
expect(httpCacheService.getCacheData('/lolo')).toBe(response);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setPersistence', () => {
|
||||
beforeEach(() => {
|
||||
httpCacheService.setPersistence();
|
||||
httpCacheService.cleanCache = jasmine.createSpy('cleanCache');
|
||||
});
|
||||
|
||||
it('should clear previous cache data when persistence value change', () => {
|
||||
httpCacheService.setPersistence('local');
|
||||
expect(httpCacheService.cleanCache).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('should persist cache to local storage', () => {
|
||||
expect(localStorage.getItem(cachePersistenceKey)).toBeNull();
|
||||
|
||||
httpCacheService.setPersistence('local');
|
||||
httpCacheService.setCacheData('/hoho', response);
|
||||
|
||||
expect(localStorage.getItem(cachePersistenceKey)).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should persist cache to session storage', () => {
|
||||
expect(sessionStorage.getItem(cachePersistenceKey)).toBeNull();
|
||||
|
||||
httpCacheService.setPersistence('session');
|
||||
httpCacheService.setCacheData('/hoho', response);
|
||||
|
||||
expect(sessionStorage.getItem(cachePersistenceKey)).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpResponse } from '@angular/common/http';
|
||||
import { each } from 'lodash';
|
||||
|
||||
import { Logger } from '../logger.service';
|
||||
|
||||
const log = new Logger('HttpCacheService');
|
||||
const cachePersistenceKey = 'httpCache';
|
||||
|
||||
export interface HttpCacheEntry {
|
||||
lastUpdated: Date;
|
||||
data: HttpResponse<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a cache facility for HTTP requests with configurable persistence policy.
|
||||
*/
|
||||
@Injectable()
|
||||
export class HttpCacheService {
|
||||
|
||||
private cachedData: { [key: string]: HttpCacheEntry; } = {};
|
||||
private storage: Storage | null = null;
|
||||
|
||||
constructor() {
|
||||
this.loadCacheData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the cache data for the specified request.
|
||||
* @param {!string} url The request URL.
|
||||
* @param {ResponseOptions} data The received data.
|
||||
* @param {Date=} lastUpdated The cache last update, current date is used if not specified.
|
||||
*/
|
||||
setCacheData(url: string, data: HttpResponse<any>, lastUpdated?: Date) {
|
||||
this.cachedData[url] = {
|
||||
lastUpdated: lastUpdated || new Date(),
|
||||
data: data
|
||||
};
|
||||
log.debug(`Cache set for key: "${url}"`);
|
||||
this.saveCacheData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cached data for the specified request.
|
||||
* @param {!string} url The request URL.
|
||||
* @return {?ResponseOptions} The cached data or null if no cached data exists for this request.
|
||||
*/
|
||||
getCacheData(url: string): HttpResponse<any> | null {
|
||||
const cacheEntry = this.cachedData[url];
|
||||
|
||||
if (cacheEntry) {
|
||||
log.debug(`Cache hit for key: "${url}"`);
|
||||
return cacheEntry.data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cached entry for the specified request.
|
||||
* @param {!string} url The request URL.
|
||||
* @return {?HttpCacheEntry} The cache entry or null if no cache entry exists for this request.
|
||||
*/
|
||||
getHttpCacheEntry(url: string): HttpCacheEntry | null {
|
||||
return this.cachedData[url] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the cached entry (if exists) for the specified request.
|
||||
* @param {!string} url The request URL.
|
||||
*/
|
||||
clearCache(url: string): void {
|
||||
delete this.cachedData[url];
|
||||
log.debug(`Cache cleared for key: "${url}"`);
|
||||
this.saveCacheData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans cache entries older than the specified date.
|
||||
* @param {date=} expirationDate The cache expiration date. If no date is specified, all cache is cleared.
|
||||
*/
|
||||
cleanCache(expirationDate?: Date) {
|
||||
if (expirationDate) {
|
||||
each(this.cachedData, (value: HttpCacheEntry, key: string) => {
|
||||
if (expirationDate >= value.lastUpdated) {
|
||||
delete this.cachedData[key];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.cachedData = {};
|
||||
}
|
||||
this.saveCacheData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the cache persistence policy.
|
||||
* Note that changing the cache persistence will also clear the cache from its previous storage.
|
||||
* @param {'local'|'session'=} persistence How the cache should be persisted, it can be either local or session
|
||||
* storage, or if no value is provided it will be only in-memory (default).
|
||||
*/
|
||||
setPersistence(persistence?: 'local' | 'session') {
|
||||
this.cleanCache();
|
||||
this.storage = persistence === 'local' || persistence === 'session' ? window[persistence + 'Storage'] : null;
|
||||
this.loadCacheData();
|
||||
}
|
||||
|
||||
private saveCacheData() {
|
||||
if (this.storage) {
|
||||
this.storage[cachePersistenceKey] = JSON.stringify(this.cachedData);
|
||||
}
|
||||
}
|
||||
|
||||
private loadCacheData() {
|
||||
const data = this.storage ? this.storage[cachePersistenceKey] : null;
|
||||
this.cachedData = data ? JSON.parse(data) : {};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
import { HttpClient, HttpInterceptor } from '@angular/common/http';
|
||||
|
||||
import { HttpService } from './http.service';
|
||||
import { HttpCacheService } from './http-cache.service';
|
||||
import { ErrorHandlerInterceptor } from './error-handler.interceptor';
|
||||
import { CacheInterceptor } from './cache.interceptor';
|
||||
import { ApiPrefixInterceptor } from './api-prefix.interceptor';
|
||||
|
||||
describe('HttpService', () => {
|
||||
let httpCacheService: HttpCacheService;
|
||||
let http: HttpClient;
|
||||
let httpMock: HttpTestingController;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
providers: [
|
||||
ErrorHandlerInterceptor,
|
||||
CacheInterceptor,
|
||||
ApiPrefixInterceptor,
|
||||
HttpCacheService,
|
||||
{
|
||||
provide: HttpClient,
|
||||
useClass: HttpService
|
||||
},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(inject([
|
||||
HttpClient,
|
||||
HttpTestingController,
|
||||
HttpCacheService
|
||||
], (_http: HttpClient,
|
||||
_httpMock: HttpTestingController,
|
||||
_httpCacheService: HttpCacheService) => {
|
||||
|
||||
http = _http;
|
||||
httpMock = _httpMock;
|
||||
httpCacheService = _httpCacheService;
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
httpCacheService.cleanCache();
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
it('should use error handler, API prefix and no cache by default', () => {
|
||||
// Arrange
|
||||
let interceptors: HttpInterceptor[];
|
||||
const realRequest = http.request;
|
||||
spyOn(HttpService.prototype, 'request').and.callFake(function(this: any) {
|
||||
interceptors = this.interceptors;
|
||||
return realRequest.apply(this, arguments);
|
||||
});
|
||||
|
||||
// Act
|
||||
const request = http.get('/toto');
|
||||
|
||||
// Assert
|
||||
request.subscribe(() => {
|
||||
expect(http.request).toHaveBeenCalled();
|
||||
expect(interceptors.some(i => i instanceof ApiPrefixInterceptor)).toBeTruthy();
|
||||
expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeTruthy();
|
||||
expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeFalsy();
|
||||
});
|
||||
httpMock.expectOne({}).flush({});
|
||||
});
|
||||
|
||||
it('should use cache', () => {
|
||||
// Arrange
|
||||
let interceptors: HttpInterceptor[];
|
||||
const realRequest = http.request;
|
||||
spyOn(HttpService.prototype, 'request').and.callFake(function(this: any) {
|
||||
interceptors = this.interceptors;
|
||||
return realRequest.apply(this, arguments);
|
||||
});
|
||||
|
||||
// Act
|
||||
const request = http
|
||||
.cache()
|
||||
.get('/toto');
|
||||
|
||||
// Assert
|
||||
request.subscribe(() => {
|
||||
expect(interceptors.some(i => i instanceof ApiPrefixInterceptor)).toBeTruthy();
|
||||
expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeTruthy();
|
||||
expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeTruthy();
|
||||
});
|
||||
httpMock.expectOne({}).flush({});
|
||||
});
|
||||
|
||||
it('should skip error handler', () => {
|
||||
// Arrange
|
||||
let interceptors: HttpInterceptor[];
|
||||
const realRequest = http.request;
|
||||
spyOn(HttpService.prototype, 'request').and.callFake(function(this: any) {
|
||||
interceptors = this.interceptors;
|
||||
return realRequest.apply(this, arguments);
|
||||
});
|
||||
|
||||
// Act
|
||||
const request = http
|
||||
.skipErrorHandler()
|
||||
.get('/toto');
|
||||
|
||||
// Assert
|
||||
request.subscribe(() => {
|
||||
expect(interceptors.some(i => i instanceof ApiPrefixInterceptor)).toBeTruthy();
|
||||
expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeFalsy();
|
||||
expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeFalsy();
|
||||
});
|
||||
httpMock.expectOne({}).flush({});
|
||||
});
|
||||
|
||||
it('should not use API prefix', () => {
|
||||
// Arrange
|
||||
let interceptors: HttpInterceptor[];
|
||||
const realRequest = http.request;
|
||||
spyOn(HttpService.prototype, 'request').and.callFake(function(this: any) {
|
||||
interceptors = this.interceptors;
|
||||
return realRequest.apply(this, arguments);
|
||||
});
|
||||
|
||||
// Act
|
||||
const request = http
|
||||
.disableApiPrefix()
|
||||
.get('/toto');
|
||||
|
||||
// Assert
|
||||
request.subscribe(() => {
|
||||
expect(interceptors.some(i => i instanceof ApiPrefixInterceptor)).toBeFalsy();
|
||||
expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeTruthy();
|
||||
expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeFalsy();
|
||||
});
|
||||
httpMock.expectOne({}).flush({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import { Inject, Injectable, InjectionToken, Injector, Optional } from '@angular/core';
|
||||
import { HttpClient, HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { ErrorHandlerInterceptor } from './error-handler.interceptor';
|
||||
import { CacheInterceptor } from './cache.interceptor';
|
||||
import { ApiPrefixInterceptor } from './api-prefix.interceptor';
|
||||
|
||||
// HttpClient is declared in a re-exported module, so we have to extend the original module to make it work properly
|
||||
// (see https://github.com/Microsoft/TypeScript/issues/13897)
|
||||
declare module '@angular/common/http/src/client' {
|
||||
|
||||
// Augment HttpClient with the added configuration methods from HttpService, to allow in-place replacement of
|
||||
// HttpClient with HttpService using dependency injection
|
||||
export interface HttpClient {
|
||||
|
||||
/**
|
||||
* Enables caching for this request.
|
||||
* @param {boolean} forceUpdate Forces request to be made and updates cache entry.
|
||||
* @return {HttpClient} The new instance.
|
||||
*/
|
||||
cache(forceUpdate?: boolean): HttpClient;
|
||||
|
||||
/**
|
||||
* Skips default error handler for this request.
|
||||
* @return {HttpClient} The new instance.
|
||||
*/
|
||||
skipErrorHandler(): HttpClient;
|
||||
|
||||
/**
|
||||
* Do not use API prefix for this request.
|
||||
* @return {HttpClient} The new instance.
|
||||
*/
|
||||
disableApiPrefix(): HttpClient;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// From @angular/common/http/src/interceptor: allows to chain interceptors
|
||||
class HttpInterceptorHandler implements HttpHandler {
|
||||
|
||||
constructor(private next: HttpHandler, private interceptor: HttpInterceptor) { }
|
||||
|
||||
handle(request: HttpRequest<any>): Observable<HttpEvent<any>> {
|
||||
return this.interceptor.intercept(request, this.next);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to override default dynamic interceptors that can be disabled with the HttpService extension.
|
||||
* Except for very specific needs, you should better configure these interceptors directly in the constructor below
|
||||
* for better readability.
|
||||
*
|
||||
* For static interceptors that should always be enabled (like ApiPrefixInterceptor), use the standard
|
||||
* HTTP_INTERCEPTORS token.
|
||||
*/
|
||||
export const HTTP_DYNAMIC_INTERCEPTORS = new InjectionToken<HttpInterceptor>('HTTP_DYNAMIC_INTERCEPTORS');
|
||||
|
||||
/**
|
||||
* Extends HttpClient with per request configuration using dynamic interceptors.
|
||||
*/
|
||||
@Injectable()
|
||||
export class HttpService extends HttpClient {
|
||||
|
||||
constructor(private httpHandler: HttpHandler,
|
||||
private injector: Injector,
|
||||
@Optional() @Inject(HTTP_DYNAMIC_INTERCEPTORS) private interceptors: HttpInterceptor[] = []) {
|
||||
super(httpHandler);
|
||||
|
||||
if (!this.interceptors) {
|
||||
// Configure default interceptors that can be disabled here
|
||||
this.interceptors = [
|
||||
this.injector.get(ApiPrefixInterceptor),
|
||||
this.injector.get(ErrorHandlerInterceptor)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
cache(forceUpdate?: boolean): HttpClient {
|
||||
const cacheInterceptor = this.injector.get(CacheInterceptor).configure({ update: forceUpdate });
|
||||
return this.addInterceptor(cacheInterceptor);
|
||||
}
|
||||
|
||||
skipErrorHandler(): HttpClient {
|
||||
return this.removeInterceptor(ErrorHandlerInterceptor);
|
||||
}
|
||||
|
||||
disableApiPrefix(): HttpClient {
|
||||
return this.removeInterceptor(ApiPrefixInterceptor);
|
||||
}
|
||||
|
||||
// Override the original method to wire interceptors when triggering the request.
|
||||
request(method?: any, url?: any, options?: any): any {
|
||||
const handler = this.interceptors.reduceRight(
|
||||
(next, interceptor) => new HttpInterceptorHandler(next, interceptor),
|
||||
this.httpHandler
|
||||
);
|
||||
return new HttpClient(handler).request(method, url, options);
|
||||
}
|
||||
|
||||
private removeInterceptor(interceptorType: Function): HttpService {
|
||||
return new HttpService(
|
||||
this.httpHandler,
|
||||
this.injector,
|
||||
this.interceptors.filter(i => !(i instanceof interceptorType))
|
||||
);
|
||||
}
|
||||
|
||||
private addInterceptor(interceptor: HttpInterceptor): HttpService {
|
||||
return new HttpService(
|
||||
this.httpHandler,
|
||||
this.injector,
|
||||
this.interceptors.concat([interceptor])
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user