mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[AAE-12501] Align JS API (#8344)
* [AAE-12501] Provide an AlfrescoApiService implementation that disable the AlfrescoApi oauth initialization when we use new oidc implementation * [AAE-12501] Replace oauth2Auth with authentication service, define get username as abstract * [AAE-12501] Replace sitesService with authentication service since sitesService get the username from oauth2Auth * [AAE-12501] Call implicitLogin by authentication service * [AAE-12501] Replace Oauth2Auth with AlfrescoApi and call the custom api without using authentication * [AAE-12501] Replace oauth2Auth with authentication service to get the token * [AAE-12501] Replace oauth2Auth with alfrescoApi * remove unneeded JS-API dep move auth in the right place * [AAE-10501] Rename alfresco-api.http-client to adf-http-client * [AAE-10501] Remove config from a CoreModule, a different service is provided in AuthModule to use angular http client instead of super agent * [AAE-10501] Disable AlfrescoApi oauth initialization while using new adf oidc authentication * [AAE-12501] Replace alfresco api client with AdfHttpClient * [AAE-12501] Restore get username methods * [AAE-12501] Get username with authentication service * [AAE-12501] removee unused method * [AAE-12501] Trigger on login when token is received * [AAE-12501] Fix content-services unit test * [AAE-12501] Fix import * [AAE-12501] Fix core unit tests * [AAE-12501] Fix process-services-cloud unit tests * [AAE-12501] Create a request options interface with the needed props, remove the import from js-api, return the body from request * [AAE-12501] Fix process-services-cloud unit tests without Expectation * [AAE-12501] Fix Core secondary entrypoints unit tests are not executed: move test.ts one level up in order to find all the spec files into the secondary entrypoints folders and update path * [AAE-12501] Fix Core unit tests that weren't executed because of the previous test.ts wrong location * [AAE-12501] Fix authentication token_issued subscription * add emitters * [AAE-12501] Replace Math.random() to fix hospot security issue, fix lint issues * [AAE-12501] Install event-emitter dependency * [AAE-12501] Comment temporary setCsrfToken because is not possible to import app config service from core due to circular dependencies * [AAE-12501] Get disableCsrf from app config serviice when app configuration is loaded * [AAE-12501] Fix license-header lint issue * [AAE-14221] Regenerate lock file * [AAE-14221] Fix sonarcloud issues * [AAE-12501] Remove wrong character * [AAE-12501] Regenerate lock file * [AAE-12501] Fix BC: update alfresco api response error --------- Co-authored-by: eromano <eugenioromano16@gmail.com>
This commit is contained in:
@@ -19,4 +19,5 @@ export * from './lib/api-client.factory';
|
||||
export * from './lib/api-clients.service';
|
||||
export * from './lib/clients';
|
||||
export * from './lib/types';
|
||||
export * from './lib/alfresco-api/alfresco-api.http-client';
|
||||
export * from './lib/adf-http-client.service';
|
||||
export * from './lib/interfaces';
|
||||
|
@@ -19,8 +19,8 @@ import { Emitters, RequestOptions, ResultListDataRepresentationTaskRepresentatio
|
||||
import { HttpParams } from '@angular/common/http';
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { AlfrescoApiHttpClient } from './alfresco-api.http-client';
|
||||
import { AlfrescoApiResponseError } from './alfresco-api.response-error';
|
||||
import { AdfHttpClient } from './adf-http-client.service';
|
||||
import { AlfrescoApiResponseError } from './alfresco-api/alfresco-api.response-error';
|
||||
|
||||
const securityOptions: SecurityOptions = {
|
||||
authentications: {},
|
||||
@@ -52,8 +52,8 @@ const mockResponse = {
|
||||
]
|
||||
};
|
||||
|
||||
describe('AlfrescoApiHttpClient', () => {
|
||||
let angularHttpClient: AlfrescoApiHttpClient;
|
||||
describe('AdfHttpClient', () => {
|
||||
let angularHttpClient: AdfHttpClient;
|
||||
let controller: HttpTestingController;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -62,7 +62,7 @@ describe('AlfrescoApiHttpClient', () => {
|
||||
HttpClientTestingModule
|
||||
]
|
||||
});
|
||||
angularHttpClient = TestBed.inject(AlfrescoApiHttpClient);
|
||||
angularHttpClient = TestBed.inject(AdfHttpClient);
|
||||
controller = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
@@ -90,7 +90,7 @@ describe('AlfrescoApiHttpClient', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
expect(res.data![0].created instanceof Date).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
}).catch(error=> fail(error));
|
||||
|
||||
const req = controller.expectOne('http://example.com');
|
||||
expect(req.request.method).toEqual('POST');
|
||||
@@ -110,7 +110,7 @@ describe('AlfrescoApiHttpClient', () => {
|
||||
angularHttpClient.request('http://example.com', options, securityOptions, emitters).then((res) => {
|
||||
expect(res).toEqual(mockResponse);
|
||||
done();
|
||||
});
|
||||
}).catch(error=> fail(error));
|
||||
|
||||
const req = controller.expectOne('http://example.com');
|
||||
expect(req.request.method).toEqual('POST');
|
||||
@@ -177,7 +177,9 @@ describe('AlfrescoApiHttpClient', () => {
|
||||
returnType: null
|
||||
};
|
||||
|
||||
angularHttpClient.request('http://example.com', requestOptions, securityOptions, emitters);
|
||||
angularHttpClient.request('http://example.com', requestOptions, securityOptions, emitters).catch(error =>
|
||||
fail(error)
|
||||
);
|
||||
const req = controller.expectOne('http://example.com?autoRename=true&include=allowableOperations');
|
||||
expect(req.request.method).toEqual('POST');
|
||||
|
||||
@@ -232,9 +234,9 @@ describe('AlfrescoApiHttpClient', () => {
|
||||
|
||||
const errorResponse = new Blob();
|
||||
|
||||
angularHttpClient.request('http://example.com', options, securityOptions, emitters).catch((res: AlfrescoApiResponseError) => {
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.error.response.body instanceof Blob).toBeTruthy();
|
||||
angularHttpClient.request('http://example.com', options, securityOptions, emitters).catch((err: AlfrescoApiResponseError) => {
|
||||
expect(err.status).toBe(400);
|
||||
expect(err.response.body instanceof Blob).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -257,7 +259,9 @@ describe('AlfrescoApiHttpClient', () => {
|
||||
}
|
||||
};
|
||||
|
||||
angularHttpClient.request('http://example.com/candidatebaseapp/query/v1/process-instances', options, securityOptions, emitters);
|
||||
angularHttpClient.request('http://example.com/candidatebaseapp/query/v1/process-instances', options, securityOptions, emitters).catch(error =>
|
||||
fail(error)
|
||||
);
|
||||
|
||||
const req = controller.expectOne('http://example.com/candidatebaseapp/query/v1/process-instances?skipCount=0&status=RUNNING&status=SUSPENDED&sort=startDate%2CDESC');
|
||||
expect(req.request.method).toEqual('POST');
|
||||
@@ -274,7 +278,7 @@ describe('AlfrescoApiHttpClient', () => {
|
||||
angularHttpClient.request('http://example.com', options, securityOptions, emitters).then((res) => {
|
||||
expect(res).toEqual('');
|
||||
done();
|
||||
});
|
||||
}).catch(error=> fail(error));
|
||||
|
||||
const req = controller.expectOne('http://example.com');
|
||||
|
||||
@@ -290,7 +294,9 @@ describe('AlfrescoApiHttpClient', () => {
|
||||
}
|
||||
};
|
||||
|
||||
angularHttpClient.request('http://example.com', options, securityOptions, emitters);
|
||||
angularHttpClient.request('http://example.com', options, securityOptions, emitters).catch(error =>
|
||||
fail(error)
|
||||
);
|
||||
|
||||
const req = controller.expectOne('http://example.com?lastModifiedFrom=2022-08-17T00%3A00%3A00.000%2B02%3A00');
|
||||
|
||||
@@ -306,7 +312,9 @@ describe('AlfrescoApiHttpClient', () => {
|
||||
}
|
||||
};
|
||||
|
||||
angularHttpClient.request('http://example.com', options, securityOptions, emitters);
|
||||
angularHttpClient.request('http://example.com', options, securityOptions, emitters).catch(error =>
|
||||
fail(error)
|
||||
);
|
||||
|
||||
const req = controller.expectOne('http://example.com?lastModifiedFrom=2022-08-17T00%3A00%3A00.000Z');
|
||||
|
353
lib/core/api/src/lib/adf-http-client.service.ts
Normal file
353
lib/core/api/src/lib/adf-http-client.service.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { SHOULD_ADD_AUTH_TOKEN } from '@alfresco/adf-core/auth';
|
||||
import { Emitters as JsApiEmitters, HttpClient as JsApiHttpClient } from '@alfresco/js-api';
|
||||
import {
|
||||
HttpClient,
|
||||
HttpContext,
|
||||
HttpErrorResponse,
|
||||
HttpEvent,
|
||||
HttpHeaders,
|
||||
HttpParams,
|
||||
HttpResponse
|
||||
} from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, of, Subject, throwError } from 'rxjs';
|
||||
import { catchError, map, takeUntil } from 'rxjs/operators';
|
||||
import {
|
||||
convertObjectToFormData,
|
||||
getQueryParamsWithCustomEncoder,
|
||||
isBlobResponse,
|
||||
isConstructor,
|
||||
isHttpResponseEvent,
|
||||
isHttpUploadProgressEvent,
|
||||
removeNilValues
|
||||
} from './alfresco-api/alfresco-api.utils';
|
||||
import { AlfrescoApiParamEncoder } from './alfresco-api/alfresco-api.param-encoder';
|
||||
import { AlfrescoApiResponseError } from './alfresco-api/alfresco-api.response-error';
|
||||
import { Constructor } from './types';
|
||||
import { RequestOptions, SecurityOptions } from './interfaces';
|
||||
import ee, { Emitter } from 'event-emitter';
|
||||
|
||||
export interface Emitters {
|
||||
readonly eventEmitter: Emitter;
|
||||
readonly apiClientEmitter: Emitter;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AdfHttpClient implements ee.Emitter,JsApiHttpClient {
|
||||
|
||||
on: ee.EmitterMethod;
|
||||
off: ee.EmitterMethod;
|
||||
once: ee.EmitterMethod;
|
||||
emit: (type: string, ...args: any[]) => void;
|
||||
|
||||
private _disableCsrf = false;
|
||||
|
||||
private defaultSecurityOptions = {
|
||||
withCredentials: true,
|
||||
isBpmRequest: false,
|
||||
authentications: {},
|
||||
defaultHeaders: {}
|
||||
};
|
||||
|
||||
get disableCsrf(): boolean {
|
||||
return this._disableCsrf;
|
||||
}
|
||||
|
||||
set disableCsrf(disableCsrf: boolean) {
|
||||
this._disableCsrf = disableCsrf;
|
||||
}
|
||||
|
||||
constructor(private httpClient: HttpClient
|
||||
) {
|
||||
ee(this);
|
||||
}
|
||||
|
||||
setDefaultSecurityOption(options: any) {
|
||||
this.defaultSecurityOptions = this.merge(this.defaultSecurityOptions, options);
|
||||
}
|
||||
|
||||
merge(...objects): any {
|
||||
const result = {};
|
||||
|
||||
objects.forEach((source) => {
|
||||
Object.keys(source).forEach((prop) => {
|
||||
if (prop in result && Array.isArray(result[prop])) {
|
||||
result[prop] = result[prop].concat(source[prop]);
|
||||
} else if (prop in result && typeof result[prop] === 'object') {
|
||||
result[prop] = this.merge(result[prop], source[prop]);
|
||||
} else {
|
||||
result[prop] = source[prop];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
request<T = any>(url: string, options?: RequestOptions, sc: SecurityOptions = this.defaultSecurityOptions, emitters?: JsApiEmitters): Promise<T> {
|
||||
const body = AdfHttpClient.getBody(options);
|
||||
const params = getQueryParamsWithCustomEncoder(options.queryParams, new AlfrescoApiParamEncoder());
|
||||
const responseType = AdfHttpClient.getResponseType(options);
|
||||
const context = new HttpContext().set(SHOULD_ADD_AUTH_TOKEN, true);
|
||||
const security: SecurityOptions = {...this.defaultSecurityOptions, ...sc};
|
||||
const headers = this.getHeaders(options);
|
||||
if (!emitters) {
|
||||
emitters = this.getEventEmitters();
|
||||
}
|
||||
|
||||
const request = this.httpClient.request(
|
||||
options.httpMethod,
|
||||
url,
|
||||
{
|
||||
context,
|
||||
...(body && {body}),
|
||||
...(responseType && {responseType}),
|
||||
...security,
|
||||
...(params && {params}),
|
||||
headers,
|
||||
observe: 'events',
|
||||
reportProgress: true
|
||||
}
|
||||
);
|
||||
|
||||
return this.requestWithLegacyEventEmitters<T>(request, emitters, options.returnType);
|
||||
}
|
||||
|
||||
post<T = any>(url: string, options?: RequestOptions, sc?: SecurityOptions, emitters?: JsApiEmitters): Promise<T> {
|
||||
return this.request<T>(url, {...options, httpMethod: 'POST'}, sc, emitters);
|
||||
}
|
||||
|
||||
put<T = any>(url: string, options?: RequestOptions, sc?: SecurityOptions, emitters?: JsApiEmitters): Promise<T> {
|
||||
return this.request<T>(url, {...options, httpMethod: 'PUT'}, sc, emitters);
|
||||
}
|
||||
|
||||
get<T = any>(url: string, options?: RequestOptions, sc?: SecurityOptions, emitters?: JsApiEmitters): Promise<T> {
|
||||
return this.request<T>(url, {...options, httpMethod: 'GET'}, sc, emitters);
|
||||
}
|
||||
|
||||
delete<T = void>(url: string, options?: RequestOptions, sc?: SecurityOptions, emitters?: JsApiEmitters): Promise<T> {
|
||||
return this.request<T>(url, {...options, httpMethod: 'DELETE'}, sc, emitters);
|
||||
}
|
||||
|
||||
private addPromiseListeners<T = any>(promise: Promise<T>, eventEmitter: any) {
|
||||
const eventPromise = Object.assign(promise, {
|
||||
on() {
|
||||
eventEmitter.on.apply(eventEmitter, arguments);
|
||||
return this;
|
||||
},
|
||||
once() {
|
||||
eventEmitter.once.apply(eventEmitter, arguments);
|
||||
return this;
|
||||
},
|
||||
emit() {
|
||||
eventEmitter.emit.apply(eventEmitter, arguments);
|
||||
return this;
|
||||
},
|
||||
off() {
|
||||
eventEmitter.off.apply(eventEmitter, arguments);
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
return eventPromise;
|
||||
}
|
||||
|
||||
private getEventEmitters(): Emitters {
|
||||
const apiClientEmitter = {
|
||||
on: this.on.bind(this),
|
||||
off: this.off.bind(this),
|
||||
once: this.once.bind(this),
|
||||
emit: this.emit.bind(this)
|
||||
};
|
||||
|
||||
return {
|
||||
apiClientEmitter,
|
||||
eventEmitter: ee({})
|
||||
};
|
||||
}
|
||||
|
||||
private requestWithLegacyEventEmitters<T = any>(request$: Observable<HttpEvent<T>>, emitters: JsApiEmitters, returnType: any): Promise<T> {
|
||||
|
||||
const abort$ = new Subject<void>();
|
||||
const {eventEmitter, apiClientEmitter} = emitters;
|
||||
|
||||
const promise = request$.pipe(
|
||||
map((res) => {
|
||||
if (isHttpUploadProgressEvent(res)) {
|
||||
const percent = Math.round((res.loaded / res.total) * 100);
|
||||
eventEmitter.emit('progress', {loaded: res.loaded, total: res.total, percent});
|
||||
}
|
||||
|
||||
if (isHttpResponseEvent(res)) {
|
||||
eventEmitter.emit('success', res.body);
|
||||
return AdfHttpClient.deserialize(res, returnType);
|
||||
}
|
||||
}),
|
||||
catchError((err: HttpErrorResponse): Observable<AlfrescoApiResponseError> => {
|
||||
|
||||
// since we can't always determinate ahead of time if the response is going to be xml or plain text response
|
||||
// we need to handle false positive cases here.
|
||||
|
||||
if (err.status === 200) {
|
||||
eventEmitter.emit('success', err.error.text);
|
||||
return of(err.error.text);
|
||||
}
|
||||
|
||||
eventEmitter.emit('error', err);
|
||||
apiClientEmitter.emit('error', err);
|
||||
|
||||
if (err.status === 401) {
|
||||
eventEmitter.emit('unauthorized');
|
||||
apiClientEmitter.emit('unauthorized');
|
||||
}
|
||||
|
||||
// for backwards compatibility we need to convert it to error class as the HttpErrorResponse only implements Error interface, not extending it,
|
||||
// and we need to be able to correctly pass instanceof Error conditions used inside repository
|
||||
// we also need to pass error as Stringify string as we are detecting statusCodes using JSON.parse(error.message) in some places
|
||||
const msg = typeof err.error === 'string' ? err.error : JSON.stringify(err.error);
|
||||
|
||||
// for backwards compatibility to handle cases in code where we try read response.error.response.body;
|
||||
|
||||
const error = {
|
||||
response: {...err, body: err.error}
|
||||
};
|
||||
|
||||
const alfrescoApiError = new AlfrescoApiResponseError(msg, err.status, error.response);
|
||||
return throwError(alfrescoApiError);
|
||||
}),
|
||||
takeUntil(abort$)
|
||||
).toPromise();
|
||||
|
||||
(promise as any).abort = function() {
|
||||
eventEmitter.emit('abort');
|
||||
abort$.next();
|
||||
abort$.complete();
|
||||
return this;
|
||||
};
|
||||
|
||||
return this.addPromiseListeners(promise, eventEmitter);
|
||||
}
|
||||
|
||||
private static getBody(options: RequestOptions): any {
|
||||
const contentType = options.contentType;
|
||||
const isFormData = contentType === 'multipart/form-data';
|
||||
const isFormUrlEncoded = contentType === 'application/x-www-form-urlencoded';
|
||||
const body = options.bodyParam;
|
||||
|
||||
if (isFormData) {
|
||||
return convertObjectToFormData(options.formParams);
|
||||
}
|
||||
|
||||
if (isFormUrlEncoded) {
|
||||
return new HttpParams({fromObject: removeNilValues(options.formParams)});
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
private getHeaders(options: RequestOptions): HttpHeaders {
|
||||
const optionsHeaders = {
|
||||
...options.headerParams,
|
||||
...(options.accept && {Accept: options.accept}),
|
||||
...((options.contentType) && {'Content-Type': options.contentType})
|
||||
};
|
||||
|
||||
if (!this.disableCsrf) {
|
||||
this.setCsrfToken(optionsHeaders);
|
||||
|
||||
}
|
||||
|
||||
return new HttpHeaders(optionsHeaders);
|
||||
}
|
||||
|
||||
private setCsrfToken(optionsHeaders: any) {
|
||||
const token = this.createCSRFToken();
|
||||
optionsHeaders['X-CSRF-TOKEN'] = token;
|
||||
|
||||
try {
|
||||
document.cookie = 'CSRF-TOKEN=' + token + ';path=/';
|
||||
} catch (err) {
|
||||
/* continue regardless of error */
|
||||
}
|
||||
}
|
||||
|
||||
private createCSRFToken(a?: any): string {
|
||||
const randomValue = window.crypto.getRandomValues(new Uint32Array(1))[0];
|
||||
return a ? (a ^ ((randomValue * 16) >> (a / 4))).toString(16) : ([1e16] + (1e16).toString()).replace(/[01]/g, this.createCSRFToken);
|
||||
}
|
||||
|
||||
private static getResponseType(options: RequestOptions): 'blob' | 'json' | 'text' {
|
||||
|
||||
const isBlobType = options.returnType?.toString().toLowerCase() === 'blob' || options.responseType?.toString().toLowerCase() === 'blob';
|
||||
|
||||
if (isBlobType) {
|
||||
return 'blob';
|
||||
}
|
||||
|
||||
if (options.returnType === 'String') {
|
||||
return 'text';
|
||||
}
|
||||
|
||||
return 'json';
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize an HTTP response body into a value of the specified type.
|
||||
*/
|
||||
private static deserialize<T>(response: HttpResponse<T>, returnType?: Constructor<unknown> | 'blob'): any {
|
||||
|
||||
if (response === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const body = response.body;
|
||||
|
||||
if (!returnType) {
|
||||
// for backwards compatibility we need to return empty string instead of null,
|
||||
// to avoid issues when accessing null response would break application [C309878]
|
||||
// cannot read property 'entry' of null in cases like
|
||||
// return this.post(apiUrl, saveFormRepresentation).pipe(map((res: any) => res.entry))
|
||||
|
||||
return body !== null ? body : '';
|
||||
}
|
||||
|
||||
if (isBlobResponse(response, returnType)) {
|
||||
return AdfHttpClient.deserializeBlobResponse(response);
|
||||
}
|
||||
|
||||
if (!isConstructor(returnType)) {
|
||||
return body;
|
||||
}
|
||||
|
||||
if (Array.isArray(body)) {
|
||||
return body.map((element) => new returnType(element));
|
||||
}
|
||||
|
||||
return new returnType(body);
|
||||
}
|
||||
|
||||
|
||||
private static deserializeBlobResponse(response: HttpResponse<Blob>) {
|
||||
return new Blob([response.body], {type: response.headers.get('Content-Type')});
|
||||
}
|
||||
}
|
||||
|
@@ -1,227 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { SHOULD_ADD_AUTH_TOKEN } from '@alfresco/adf-core/auth';
|
||||
import { Emitters as JsApiEmitters, HttpClient as JsApiHttpClient, RequestOptions, SecurityOptions, isBrowser } from '@alfresco/js-api';
|
||||
import { HttpClient, HttpContext, HttpErrorResponse, HttpEvent, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, of, Subject, throwError } from 'rxjs';
|
||||
import { catchError, map, takeUntil } from 'rxjs/operators';
|
||||
import { convertObjectToFormData, getQueryParamsWithCustomEncoder, isBlobResponse, isConstructor, isHttpResponseEvent, isHttpUploadProgressEvent, removeNilValues } from './alfresco-api.utils';
|
||||
import { AlfrescoApiParamEncoder } from './alfresco-api.param-encoder';
|
||||
import { AlfrescoApiResponseError } from './alfresco-api.response-error';
|
||||
import { Constructor } from '../types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AlfrescoApiHttpClient implements JsApiHttpClient {
|
||||
|
||||
constructor(private httpClient: HttpClient) {}
|
||||
|
||||
request<T = any>(url: string, options: RequestOptions, sc: SecurityOptions, emitters: JsApiEmitters): Promise<T> {
|
||||
const body = AlfrescoApiHttpClient.getBody(options);
|
||||
const params = getQueryParamsWithCustomEncoder(options.queryParams, new AlfrescoApiParamEncoder());
|
||||
const headers = AlfrescoApiHttpClient.getHeaders(options);
|
||||
const responseType = AlfrescoApiHttpClient.getResponseType(options);
|
||||
const context = new HttpContext().set(SHOULD_ADD_AUTH_TOKEN, true);
|
||||
|
||||
const request = this.httpClient.request(
|
||||
options.httpMethod,
|
||||
url,
|
||||
{
|
||||
context,
|
||||
...(body && { body }),
|
||||
...(responseType && { responseType }),
|
||||
...(sc.withCredentials && { withCredentials: true }),
|
||||
...(params && { params }),
|
||||
headers,
|
||||
observe: 'events',
|
||||
reportProgress: true
|
||||
}
|
||||
);
|
||||
|
||||
return this.requestWithLegacyEventEmitters<T>(request, emitters, options.returnType);
|
||||
}
|
||||
|
||||
post<T = any>(url: string, options: RequestOptions, sc: SecurityOptions, emitters: JsApiEmitters): Promise<T> {
|
||||
return this.request<T>(url, { ...options, httpMethod: 'POST' }, sc, emitters);
|
||||
}
|
||||
|
||||
put<T = any>(url: string, options: RequestOptions, sc: SecurityOptions, emitters: JsApiEmitters): Promise<T> {
|
||||
return this.request<T>(url, { ...options, httpMethod: 'PUT' }, sc, emitters);
|
||||
}
|
||||
|
||||
get<T = any>(url: string, options: RequestOptions, sc: SecurityOptions, emitters: JsApiEmitters): Promise<T> {
|
||||
return this.request<T>(url, { ...options, httpMethod: 'GET' }, sc, emitters);
|
||||
}
|
||||
|
||||
delete<T = void>(url: string, options: RequestOptions, sc: SecurityOptions, emitters: JsApiEmitters): Promise<T> {
|
||||
return this.request<T>(url, { ...options, httpMethod: 'DELETE' }, sc, emitters);
|
||||
}
|
||||
|
||||
private requestWithLegacyEventEmitters<T = any>(request$: Observable<HttpEvent<T>>, emitters: JsApiEmitters, returnType: any): Promise<T> {
|
||||
|
||||
const abort$ = new Subject<void>();
|
||||
const { eventEmitter, apiClientEmitter } = emitters;
|
||||
|
||||
const promise = request$.pipe(
|
||||
map((res) => {
|
||||
if (isHttpUploadProgressEvent(res)) {
|
||||
const percent = Math.round((res.loaded / res.total) * 100);
|
||||
eventEmitter.emit('progress', { loaded: res.loaded, total: res.total, percent });
|
||||
}
|
||||
|
||||
if (isHttpResponseEvent(res)) {
|
||||
eventEmitter.emit('success', res.body);
|
||||
return AlfrescoApiHttpClient.deserialize(res, returnType);
|
||||
}
|
||||
}),
|
||||
catchError((err: HttpErrorResponse): Observable<AlfrescoApiResponseError> => {
|
||||
|
||||
// since we can't always determinate ahead of time if the response is going to be xml or plain text response
|
||||
// we need to handle false positive cases here.
|
||||
|
||||
if (err.status === 200) {
|
||||
eventEmitter.emit('success', err.error.text);
|
||||
return of(err.error.text);
|
||||
}
|
||||
|
||||
eventEmitter.emit('error', err);
|
||||
apiClientEmitter.emit('error', err);
|
||||
|
||||
if (err.status === 401) {
|
||||
eventEmitter.emit('unauthorized');
|
||||
apiClientEmitter.emit('unauthorized');
|
||||
}
|
||||
|
||||
// for backwards compatibility we need to convert it to error class as the HttpErrorResponse only implements Error interface, not extending it,
|
||||
// and we need to be able to correctly pass instanceof Error conditions used inside repository
|
||||
// we also need to pass error as Stringify string as we are detecting statusCodes using JSON.parse(error.message) in some places
|
||||
const msg = typeof err.error === 'string' ? err.error : JSON.stringify(err.error);
|
||||
|
||||
// for backwards compatibility to handle cases in code where we try read response.error.response.body;
|
||||
|
||||
const error = {
|
||||
response: { ...err, body: err.error }
|
||||
};
|
||||
|
||||
const alfrescoApiError = new AlfrescoApiResponseError(msg, err.status, error);
|
||||
|
||||
return throwError(alfrescoApiError);
|
||||
}),
|
||||
takeUntil(abort$)
|
||||
).toPromise();
|
||||
|
||||
(promise as any).abort = function() {
|
||||
eventEmitter.emit('abort');
|
||||
abort$.next();
|
||||
abort$.complete();
|
||||
return this;
|
||||
};
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
private static getBody(options: RequestOptions): any {
|
||||
const contentType = options.contentType;
|
||||
const isFormData = contentType === 'multipart/form-data';
|
||||
const isFormUrlEncoded = contentType === 'application/x-www-form-urlencoded';
|
||||
const body = options.bodyParam;
|
||||
|
||||
if (isFormData) {
|
||||
return convertObjectToFormData(options.formParams);
|
||||
}
|
||||
|
||||
if (isFormUrlEncoded) {
|
||||
return new HttpParams({ fromObject: removeNilValues(options.formParams) });
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
private static getHeaders(options: RequestOptions): HttpHeaders {
|
||||
const optionsHeaders = {
|
||||
...options.headerParams,
|
||||
...(options.accept && { Accept: options.accept }),
|
||||
...((options.contentType) && { 'Content-Type': options.contentType })
|
||||
};
|
||||
|
||||
return new HttpHeaders(optionsHeaders);
|
||||
}
|
||||
|
||||
private static getResponseType(options: RequestOptions): 'blob' | 'json' | 'text' {
|
||||
|
||||
const isBlobType = options.returnType?.toString().toLowerCase() === 'blob' || options.responseType?.toString().toLowerCase() === 'blob';
|
||||
|
||||
if (isBlobType) {
|
||||
return 'blob';
|
||||
}
|
||||
|
||||
if (options.returnType === 'String') {
|
||||
return 'text';
|
||||
}
|
||||
|
||||
return 'json';
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize an HTTP response body into a value of the specified type.
|
||||
*/
|
||||
private static deserialize<T>(response: HttpResponse<T>, returnType?: Constructor<unknown> | 'blob'): any {
|
||||
|
||||
if (response === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const body = response.body;
|
||||
|
||||
if (!returnType) {
|
||||
// for backwards compatibility we need to return empty string instead of null,
|
||||
// to avoid issues when accessing null response would break application [C309878]
|
||||
// cannot read property 'entry' of null in cases like
|
||||
// return this.post(apiUrl, saveFormRepresentation).pipe(map((res: any) => res.entry))
|
||||
|
||||
return body !== null ? body : '';
|
||||
}
|
||||
|
||||
if (isBlobResponse(response, returnType)) {
|
||||
return AlfrescoApiHttpClient.deserializeBlobResponse(response);
|
||||
}
|
||||
|
||||
if (!isConstructor(returnType)) {
|
||||
return body;
|
||||
}
|
||||
|
||||
if (Array.isArray(body)) {
|
||||
return body.map((element) => new returnType(element));
|
||||
}
|
||||
|
||||
return new returnType(body);
|
||||
}
|
||||
|
||||
|
||||
private static deserializeBlobResponse(response: HttpResponse<Blob>) {
|
||||
|
||||
if (isBrowser()) {
|
||||
return new Blob([response.body], { type: response.headers.get('Content-Type') });
|
||||
}
|
||||
|
||||
return Buffer.from(response.body as unknown as WithImplicitCoercion<string>, 'binary');
|
||||
}
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ export class AlfrescoApiResponseError extends Error {
|
||||
|
||||
public name = 'AlfrescoApiResponseError';
|
||||
|
||||
constructor(msg: string, public status: number, public error: { response: Record<string, any> }) {
|
||||
constructor(msg: string, public status: number, public response: Record<string, any> ) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
36
lib/core/api/src/lib/interfaces.ts
Normal file
36
lib/core/api/src/lib/interfaces.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface RequestOptions {
|
||||
httpMethod?: string;
|
||||
queryParams?: any;
|
||||
headerParams?: any;
|
||||
formParams?: any;
|
||||
bodyParam?: any;
|
||||
returnType?: any;
|
||||
responseType?: string;
|
||||
readonly accept?: string;
|
||||
readonly contentType?: string;
|
||||
}
|
||||
|
||||
export interface SecurityOptions {
|
||||
readonly isBpmRequest: boolean;
|
||||
readonly enableCsrf?: boolean;
|
||||
readonly withCredentials?: boolean;
|
||||
readonly authentications: any;
|
||||
readonly defaultHeaders: Record<string, string>;
|
||||
}
|
Reference in New Issue
Block a user