From c09cd590b1049d75c5086f1d0c667467054b447b Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Mon, 27 Sep 2021 21:03:43 +0300 Subject: [PATCH] update generic service --- src/lib/listing/services/entities.service.ts | 4 +- src/lib/search/search.service.ts | 3 - src/lib/services/generic.service.ts | 111 +++++++------------ src/lib/utils/headers-configuration.ts | 77 +++++++++++++ src/lib/utils/index.ts | 1 + src/lib/utils/types/iqser-types.ts | 1 + 6 files changed, 118 insertions(+), 79 deletions(-) create mode 100644 src/lib/utils/headers-configuration.ts create mode 100644 src/lib/utils/types/iqser-types.ts diff --git a/src/lib/listing/services/entities.service.ts b/src/lib/listing/services/entities.service.ts index 3a38e53..1d4697d 100644 --- a/src/lib/listing/services/entities.service.ts +++ b/src/lib/listing/services/entities.service.ts @@ -38,8 +38,8 @@ export class EntitiesService extends GenericService< private _displayed: E[] = []; private readonly _selected$ = new BehaviorSubject<(string | number)[]>([]); - constructor(protected readonly _injector: Injector, @Optional() @Inject(ENTITY_PATH) protected readonly _defaultEntityPath = '') { - super(_injector, _defaultEntityPath); + constructor(protected readonly _injector: Injector, @Optional() @Inject(ENTITY_PATH) protected readonly _defaultModelPath = '') { + super(_injector, _defaultModelPath); this.all$ = this._all$.asObservable(); this.allLength$ = this._all$.pipe(getLength); diff --git a/src/lib/search/search.service.ts b/src/lib/search/search.service.ts index 8c006ae..a9f6b7f 100644 --- a/src/lib/search/search.service.ts +++ b/src/lib/search/search.service.ts @@ -1,6 +1,5 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; -import { KeysOf } from '../utils'; import { IListable } from '../listing'; @Injectable() @@ -24,8 +23,6 @@ export class SearchService { return entities.filter(entity => entity.searchKey?.includes(searchValue)); } - setSearchKey(value: KeysOf): void {} - reset(): void { this._query$.next(''); } diff --git a/src/lib/services/generic.service.ts b/src/lib/services/generic.service.ts index 19ae0a1..8095ca8 100644 --- a/src/lib/services/generic.service.ts +++ b/src/lib/services/generic.service.ts @@ -1,8 +1,8 @@ -import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; -import { BASE_PATH, Configuration, List } from '@redaction/red-ui-http'; +import { HttpClient, HttpParams } from '@angular/common/http'; import { Injector } from '@angular/core'; import { Observable } from 'rxjs'; -import { CustomHttpUrlEncodingCodec, RequiredParam, Validate } from '../utils'; +import { CustomHttpUrlEncodingCodec, List, RequiredParam, Validate } from '../utils'; +import { HeadersConfiguration } from '../utils/headers-configuration'; export interface HeaderOptions { readonly authorization?: boolean; @@ -10,34 +10,29 @@ export interface HeaderOptions { readonly contentType?: boolean; } -export abstract class GenericService { - protected readonly _defaultHeaders = new HttpHeaders(); - protected readonly _configuration = this._injector.get(Configuration, new Configuration()); - protected readonly _http = this._injector.get(HttpClient); - protected readonly _basePath = this._injector.get(BASE_PATH, this._configuration.basePath); +export interface QueryParam { + readonly key: string; + readonly value: string; +} - protected constructor(protected readonly _injector: Injector, protected readonly _defaultEntityPath: string) {} +export abstract class GenericService { + protected readonly _http = this._injector.get(HttpClient); + + protected constructor(protected readonly _injector: Injector, protected readonly _defaultModelPath: string) {} @Validate() - post(@RequiredParam() body: unknown | List<[string, string]>, entityPath = this._defaultEntityPath): Observable { - let queryParams; + getAll(modelPath = this._defaultModelPath): Observable { + console.log(`GET request from ${this.constructor.name}`); - if (Array.isArray(body) && Array.isArray(body[0]) && body[0].length === 2) { - queryParams = this._queryParams(body); - } - - console.log(`POST request from ${this.constructor.name} with body `, body); - return this._http.post(`${this._basePath}/${entityPath}`, body, { - withCredentials: this._configuration.withCredentials, - params: queryParams, - headers: this._headers(), + return this._http.get(`/${encodeURI(modelPath)}`, { + headers: HeadersConfiguration.getHeaders({ contentType: false }), observe: 'body' }); } @Validate() - delete(@RequiredParam() body: string | List<[string, string]>, entityPath = this._defaultEntityPath): Observable { - let path = `${this._basePath}/${entityPath}`; + delete(@RequiredParam() body: string | List, modelPath = this._defaultModelPath): Observable { + let path = `/${encodeURI(modelPath)}`; let queryParams; if (typeof body === 'string') { @@ -48,77 +43,45 @@ export abstract class GenericService { console.log(`DELETE request from ${this.constructor.name} with body `, body); return this._http.delete(path, { - withCredentials: this._configuration.withCredentials, params: queryParams, - headers: this._headers({ contentType: false }), + headers: HeadersConfiguration.getHeaders({ contentType: false }), observe: 'body' }); } @Validate() - getAll(entityPath = this._defaultEntityPath): Observable { - console.log(`GET request from ${this.constructor.name}`); - return this._http.get(`${this._basePath}/${entityPath}`, { - withCredentials: this._configuration.withCredentials, - headers: this._headers({ contentType: false }), + protected _post( + @RequiredParam() body: unknown, + modelPath = this._defaultModelPath, + queryParams?: List + ): Observable { + console.log(`POST request from ${this.constructor.name} with body `, body); + console.log('To path ', `/${encodeURI(modelPath)}`); + return this._http.post(`/${encodeURI(modelPath)}`, body, { + params: queryParams ? this._queryParams(queryParams) : undefined, + headers: HeadersConfiguration.getHeaders(), observe: 'body' }); } @Validate() - getOne(@RequiredParam() entityId: string, entityPath = this._defaultEntityPath): Observable { - const path = `${this._basePath}/${entityPath}/${encodeURIComponent(String(entityId))}`; + protected _getOne(@RequiredParam() path: List, modelPath = this._defaultModelPath): Observable { + const entityPath = path.map(item => encodeURIComponent(item)).join('/'); - console.log(`GET request from ${this.constructor.name} with id ${entityId}`); - return this._http.get(path, { - withCredentials: this._configuration.withCredentials, - headers: this._headers({ contentType: false }), + console.log(`GET request from ${this.constructor.name} with path ${encodeURI(modelPath)}/${entityPath}`); + + return this._http.get(`/${encodeURI(modelPath)}/${entityPath}`, { + headers: HeadersConfiguration.getHeaders({ contentType: false }), observe: 'body' }); } - get(): Observable; - get(entityId: string, entityPath?: string): Observable; - get(entityId?: string, entityPath = this._defaultEntityPath): Observable { - if (entityId) { - return this.getOne(entityId, entityPath); - } - return this.getAll(entityPath); - } - - protected _queryParams(queryParams: List<[string, string]>): HttpParams { + protected _queryParams(queryParams: List): HttpParams { let queryParameters = new HttpParams({ encoder: new CustomHttpUrlEncodingCodec() }); - for (const [key, value] of queryParams) { - queryParameters = queryParameters.set(key, value); + for (const param of queryParams) { + queryParameters = queryParameters.set(param.key, param.value); } return queryParameters; } - - protected _headers(options?: HeaderOptions): HttpHeaders { - let headers = this._defaultHeaders; - - // authentication (RED-OAUTH) required - if ((options?.authorization === undefined || options.authorization) && this._configuration.accessToken) { - const accessToken = - typeof this._configuration.accessToken === 'function' ? this._configuration.accessToken() : this._configuration.accessToken; - headers = headers.set('Authorization', 'Bearer ' + accessToken); - } - - if (options?.accept === undefined || options.accept) { - const httpHeaderAcceptSelected = this._configuration.selectHeaderAccept(['application/json']); - if (httpHeaderAcceptSelected !== undefined) { - headers = headers.set('Accept', httpHeaderAcceptSelected); - } - } - - if (options?.contentType === undefined || options.contentType) { - const httpContentTypeSelected = this._configuration.selectHeaderContentType(['application/json']); - if (httpContentTypeSelected !== undefined) { - headers = headers.set('Content-Type', httpContentTypeSelected); - } - } - - return headers; - } } diff --git a/src/lib/utils/headers-configuration.ts b/src/lib/utils/headers-configuration.ts new file mode 100644 index 0000000..ca95a51 --- /dev/null +++ b/src/lib/utils/headers-configuration.ts @@ -0,0 +1,77 @@ +import { HttpHeaders } from '@angular/common/http'; +import { HeaderOptions } from '@iqser/common-ui'; + +export class HeadersConfiguration { + static getHeaders(options?: HeaderOptions): HttpHeaders { + let headers = new HttpHeaders(); + + if (options?.accept === undefined || options.accept) { + const httpHeaderAcceptSelected = HeadersConfiguration.selectHeaderAccept(['application/json']); + if (httpHeaderAcceptSelected !== undefined) { + headers = headers.set('Accept', httpHeaderAcceptSelected); + } + } + + if (options?.contentType === undefined || options.contentType) { + const httpContentTypeSelected = HeadersConfiguration.selectHeaderContentType(['application/json']); + if (httpContentTypeSelected !== undefined) { + headers = headers.set('Content-Type', httpContentTypeSelected); + } + } + + return headers; + } + + /** + * Select the correct content-type to use for a request. + * Uses {@link HeadersConfiguration#isJsonMime} to determine the correct content-type. + * If no content type is found return the first found type if the contentTypes is not empty + * @param contentTypes - the array of content types that are available for selection + * @returns the selected content-type or undefined if no selection could be made. + */ + static selectHeaderContentType(contentTypes: string[]): string | undefined { + if (contentTypes.length === 0) { + return undefined; + } + + const type = contentTypes.find(x => this.isJsonMime(x)); + if (type === undefined) { + return contentTypes[0]; + } + return type; + } + + /** + * Select the correct accept content-type to use for a request. + * Uses {@link HeadersConfiguration#isJsonMime} to determine the correct accept content-type. + * If no content type is found return the first found type if the contentTypes is not empty + * @param accepts - the array of content types that are available for selection. + * @returns the selected content-type or undefined if no selection could be made. + */ + static selectHeaderAccept(accepts: string[]): string | undefined { + if (accepts.length === 0) { + return undefined; + } + + const type = accepts.find(x => this.isJsonMime(x)); + if (type === undefined) { + return accepts[0]; + } + return type; + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + static isJsonMime(mime: string): boolean { + const jsonMime = new RegExp('^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i'); + return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json'); + } +} diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index eacedbc..9edaef4 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -11,3 +11,4 @@ export * from './decorators/required-param.decorator'; export * from './decorators/debounce.decorator'; export * from './decorators/on-change.decorator'; export * from './http-encoder'; +export * from './types/iqser-types'; diff --git a/src/lib/utils/types/iqser-types.ts b/src/lib/utils/types/iqser-types.ts new file mode 100644 index 0000000..114f10e --- /dev/null +++ b/src/lib/utils/types/iqser-types.ts @@ -0,0 +1 @@ +export type List = readonly T[];