From 010b21c6431a464211061ddd7422935aa5e8c257 Mon Sep 17 00:00:00 2001 From: Asad Ali Date: Mon, 15 Jul 2024 11:44:57 +0500 Subject: [PATCH] fix: requestBuilder schema validation and numeric value handling issues --- packages/core-interfaces/src/httpClient.ts | 15 +++ packages/core-interfaces/src/index.ts | 1 + packages/core/src/http/requestBuilder.ts | 94 +++++++++---------- .../core/test/http/requestBuilder.test.ts | 2 +- 4 files changed, 63 insertions(+), 49 deletions(-) create mode 100644 packages/core-interfaces/src/httpClient.ts diff --git a/packages/core-interfaces/src/httpClient.ts b/packages/core-interfaces/src/httpClient.ts new file mode 100644 index 00000000..b6aee6c1 --- /dev/null +++ b/packages/core-interfaces/src/httpClient.ts @@ -0,0 +1,15 @@ +import { HttpRequest, RequestOptions } from './httpRequest'; +import { HttpResponse } from './httpResponse'; + +/** + * Interface defining the contract for an HTTP client. + * Implementations of this interface should handle making HTTP requests + * and returning promises that resolve to HTTP responses. + * @param request The HTTP request to be sent. + * @param requestOptions Optional additional options for the request. + * @returns A promise that resolves to an HTTP response. + */ +export type HttpClientInterface = ( + request: HttpRequest, + requestOptions?: RequestOptions +) => Promise; diff --git a/packages/core-interfaces/src/index.ts b/packages/core-interfaces/src/index.ts index 38e5eb9a..bf92723f 100644 --- a/packages/core-interfaces/src/index.ts +++ b/packages/core-interfaces/src/index.ts @@ -1,6 +1,7 @@ export * from './apiResponse'; export * from './authentication'; export * from './httpContext'; +export * from './httpClient'; export * from './httpInterceptor'; export * from './httpRequest'; export * from './httpResponse'; diff --git a/packages/core/src/http/requestBuilder.ts b/packages/core/src/http/requestBuilder.ts index d22421a4..358b8f5b 100644 --- a/packages/core/src/http/requestBuilder.ts +++ b/packages/core/src/http/requestBuilder.ts @@ -9,16 +9,17 @@ import { HttpRequest, HttpRequestMultipartFormBody, HttpRequestUrlEncodedFormBody, - HttpResponse, HttpInterceptorInterface, RequestOptions, RetryConfiguration, ApiLoggerInterface, + HttpClientInterface, } from '../coreInterfaces'; import { ArgumentsValidationError } from '../errors/argumentsValidationError'; import { ResponseValidationError } from '../errors/responseValidationError'; import { Schema, + SchemaValidationError, validateAndMap, validateAndMapXml, validateAndUnmapXml, @@ -81,11 +82,6 @@ export function skipEncode( return new SkipEncode(value); } -export type HttpClientInterface = ( - request: HttpRequest, - requestOptions?: RequestOptions -) => Promise; - export type ApiErrorConstructor = new ( response: HttpContext, message: string @@ -120,7 +116,7 @@ export interface RequestBuilder { headers(headersToMerge: Record): void; query( name: string, - value: QueryValue, + value: QueryValue | Record, prefixFormat?: ArrayPrefixFunction ): void; query( @@ -135,7 +131,7 @@ export interface RequestBuilder { parameters: Record, prefixFormat?: ArrayPrefixFunction ): void; - text(body: string): void; + text(body: string | number | bigint | boolean | null | undefined): void; json(data: unknown): void; requestRetryOption(option: RequestRetryOption): void; xml( @@ -289,7 +285,7 @@ export class DefaultRequestBuilder } public query( name: string, - value: QueryValue, + value: QueryValue | Record, prefixFormat?: ArrayPrefixFunction ): void; public query( @@ -317,8 +313,10 @@ export class DefaultRequestBuilder this._query.push(queryString); } } - public text(body: string): void { - this._body = body; + public text( + body: string | number | bigint | boolean | null | undefined + ): void { + this._body = body?.toString() ?? undefined; this._setContentTypeIfNotSet(TEXT_CONTENT_TYPE); } public json(data: unknown): void { @@ -499,41 +497,9 @@ export class DefaultRequestBuilder return { ...request, headers }; }); const result = await this.call(requestOptions); - if (typeof result.body !== 'string') { - throw new Error( - 'Could not parse body as JSON. The response body is not a string.' - ); - } - if (result.body.trim() === '') { - // Try mapping the missing body as null - return this.tryMappingAsNull(schema, result); - } - let parsed: unknown; - try { - parsed = JSON.parse(result.body); - } catch (error) { - throw new Error(`Could not parse body as JSON.\n\n${error.message}`); - } - const mappingResult = validateAndMap(parsed, schema); - if (mappingResult.errors) { - throw new ResponseValidationError(result, mappingResult.errors); - } - return { ...result, result: mappingResult.result }; - } - private tryMappingAsNull( - schema: Schema, - result: ApiResponse - ) { - const nullMappingResult = validateAndMap(null, schema); - if (nullMappingResult.errors) { - throw new Error( - 'Could not parse body as JSON. The response body is empty.' - ); - } - return { ...result, result: nullMappingResult.result }; + return { ...result, result: parseJsonResult(schema, result) }; } - public async callAsXml( rootName: string, schema: Schema, @@ -593,7 +559,6 @@ export class DefaultRequestBuilder return context; }); } - private _addApiLoggerInterceptors(): void { if (this._apiLogger) { const apiLogger = this._apiLogger; @@ -606,14 +571,12 @@ export class DefaultRequestBuilder }); } } - private _addAuthentication() { this.intercept((...args) => { const handler = this._authenticationProvider(this._authParams); return handler(...args); }); } - private _addRetryInterceptor() { this.intercept(async (request, options, next) => { let context: HttpContext | undefined; @@ -659,7 +622,6 @@ export class DefaultRequestBuilder return { request, response: context.response }; }); } - private _addErrorHandlingInterceptor() { this.interceptResponse((context) => { const { response } = context; @@ -720,3 +682,39 @@ function mergePath(left: string, right?: string): string { return `${left}/${right}`; } } +function parseJsonResult(schema: Schema, res: ApiResponse): T { + if (typeof res.body !== 'string') { + throw new Error( + 'Could not parse body as JSON. The response body is not a string.' + ); + } + if (res.body.trim() === '') { + const resEmptyErr = new Error( + 'Could not parse body as JSON. The response body is empty.' + ); + return validateJson(schema, null, (_) => resEmptyErr); + } + let parsed: unknown; + try { + parsed = JSON.parse(res.body); + } catch (error) { + const resUnParseErr = new Error( + `Could not parse body as JSON.\n\n${error.message}` + ); + return validateJson(schema, res.body, (_) => resUnParseErr); + } + const resInvalidErr = (errors: SchemaValidationError[]) => + new ResponseValidationError(res, errors); + return validateJson(schema, parsed, (errors) => resInvalidErr(errors)); +} +function validateJson( + schema: Schema, + value: any, + errorCreater: (errors: SchemaValidationError[]) => Error +): T { + const mappingResult = validateAndMap(value, schema); + if (mappingResult.errors) { + throw errorCreater(mappingResult.errors); + } + return mappingResult.result; +} diff --git a/packages/core/test/http/requestBuilder.test.ts b/packages/core/test/http/requestBuilder.test.ts index 2d3b0c4a..5b34bc76 100644 --- a/packages/core/test/http/requestBuilder.test.ts +++ b/packages/core/test/http/requestBuilder.test.ts @@ -1,11 +1,11 @@ import { - HttpClientInterface, RequestBuilder, createRequestBuilderFactory, skipEncode, } from '../../src/http/requestBuilder'; import { AuthenticatorInterface, + HttpClientInterface, HttpMethod, HttpRequest, HttpResponse,