From e07ceca8290cd00d870737a6f987541bde307d81 Mon Sep 17 00:00:00 2001 From: Mikhail Kormanovskii Date: Fri, 8 Nov 2024 00:06:59 +0300 Subject: [PATCH 1/5] feat: support status and headers propagation in composition engine --- CHANGELOG.md | 7 +++ .../request/engines/composition/CHANGELOG.md | 7 +++ src/core/request/engines/composition/index.ts | 51 ++++++++++++++----- .../request/engines/composition/interface.ts | 15 ++++++ 4 files changed, 66 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d90540a0..033bc4ff2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,13 @@ _Note: Gaps between patch versions are faulty, broken or test releases._ ## v?.??.? (????-??-??) +#### :rocket: New Feature + +* Added support for propagating status code and headers from exactly one request of the composition +via `propagateStatusAndHeaders` option `core/request/engines/composition` + +## v?.??.? (????-??-??) + #### :rocket: New Feature * Added support for `redirect` option `core/request/engines/fetch` diff --git a/src/core/request/engines/composition/CHANGELOG.md b/src/core/request/engines/composition/CHANGELOG.md index 472842725..b55256e38 100644 --- a/src/core/request/engines/composition/CHANGELOG.md +++ b/src/core/request/engines/composition/CHANGELOG.md @@ -9,6 +9,13 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v?.??.? (????-??-??) + +#### :rocket: New Feature + +* Added support for propagating status code and headers from exactly one request of the composition +via `propagateStatusAndHeaders` option + ## v4.0.0-alpha.40 (2024-07-02) #### :bug: Bug Fix diff --git a/src/core/request/engines/composition/index.ts b/src/core/request/engines/composition/index.ts index 5d6632c98..62eb30a29 100644 --- a/src/core/request/engines/composition/index.ts +++ b/src/core/request/engines/composition/index.ts @@ -13,10 +13,10 @@ import Async from 'core/async'; import type { Provider } from 'core/data'; -import statusCodes from 'core/status-codes'; +import statusCodes, { StatusCodes } from 'core/status-codes'; import AbortablePromise from 'core/promise/abortable'; import { SyncPromise } from 'core/prelude/structures'; -import { RequestOptions, Response, MiddlewareParams, RequestResponseObject } from 'core/request'; +import { RequestOptions, Response, MiddlewareParams, RequestResponseObject, ResponseOptions } from 'core/request'; import type { @@ -24,11 +24,13 @@ import type { CompositionEngineOpts, CompositionRequestEngine, CompositionRequest, - CompositionRequestOptions + CompositionRequestOptions, + GatheredRequestsData } from 'core/request/engines/composition/interface'; import { compositionEngineSpreadResult } from 'core/request/engines/composition/const'; +import { RawHeaders } from 'core/request/headers'; export * from 'core/request/engines/composition/const'; export * from 'core/request/engines/composition/interface'; @@ -80,7 +82,6 @@ export function compositionEngine( return r.request(options) .then(boundRequest.bind(null, async)) - .then((request) => isRequestResponseObject(request) ? request.data : request) .catch((err) => { if (r.failCompositionOnError) { throw err; @@ -88,13 +89,14 @@ export function compositionEngine( }); })); - gatherDataFromRequests(promises, options).then((data) => { + gatherDataFromRequests(promises, options).then(({data, status, headers}) => { resolve(new Response(data, { parent: requestOptions.parent, important: requestOptions.important, responseType: 'object', okStatuses: requestOptions.okStatuses, - status: statusCodes.OK, + status, + headers, decoder: requestOptions.decoders, noContentStatuses: requestOptions.noContentStatuses })); @@ -153,10 +155,14 @@ function boundRequest( * @param options - Options related to composition requests. */ async function gatherDataFromRequests( - promises: Array>, + promises: Array>, options: CompositionRequestOptions -): Promise { - const accumulator = {}; +): Promise { + const accumulator = { + data: {}, + status: StatusCodes.OK, + headers: {} + }; if (options.engineOptions?.aggregateErrors) { await Promise.allSettled(promises) @@ -198,18 +204,35 @@ async function gatherDataFromRequests( * @param compositionRequest */ function accumulateData( - accumulator: Dictionary, - data: unknown, + accumulator: GatheredRequestsData, + responseOrData: RequestResponseObject | unknown, compositionRequest: CompositionRequest -): Dictionary { +): GatheredRequestsData { const {as} = compositionRequest; + let + data: unknown = {}, + {status, headers} = accumulator; + + if (isRequestResponseObject(responseOrData)) { + data = responseOrData.data; + status = responseOrData.response.status; + headers = responseOrData.response.headers; + } else { + data = responseOrData; + } + if (as === compositionEngineSpreadResult) { - Object.assign(accumulator, data); + Object.assign(accumulator.data, data); } else { - Object.set(accumulator, as, data); + Object.set(accumulator.data, as, data); + } + + if (compositionRequest.propagateStatusAndHeaders === true) { + accumulator.status = status; + accumulator.headers = headers; } return accumulator; diff --git a/src/core/request/engines/composition/interface.ts b/src/core/request/engines/composition/interface.ts index c9d870780..5987bd59d 100644 --- a/src/core/request/engines/composition/interface.ts +++ b/src/core/request/engines/composition/interface.ts @@ -9,6 +9,8 @@ import type Provider from 'core/data'; import type { ProviderOptions } from 'core/data'; import type { RequestOptions, RequestResponseObject, MiddlewareParams, RequestPromise, RequestEngine } from 'core/request'; +import type { RawHeaders } from 'core/request/headers'; +import type { StatusCodes } from 'core/status-codes'; export interface CompositionEngineOpts { /** @@ -130,6 +132,13 @@ export interface CompositionRequest { * If false / undefined, request errors will be ignored. */ failCompositionOnError?: boolean; + + /** + * If true, status code and reponse headers will be propagated from this request to the whole + * composition. Note that if there are more than one request with this option set to true, + * only last request's data will be propagated. + */ + propagateStatusAndHeaders?: boolean; } export interface CompositionRequestOptions { @@ -166,3 +175,9 @@ export interface CompositionRequestEngine extends RequestEngine { dropCache: NonNullable; destroy: NonNullable; } + +export interface GatheredRequestsData { + data: Dictionary; + headers: RawHeaders; + status: StatusCodes; +} From 03ed9a860ed8743fa214b4a8e916472574e85862 Mon Sep 17 00:00:00 2001 From: Mikhail Kormanovskii Date: Fri, 8 Nov 2024 00:28:04 +0300 Subject: [PATCH 2/5] fix: fixed usage of a type instead of a const --- src/core/request/engines/composition/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/request/engines/composition/index.ts b/src/core/request/engines/composition/index.ts index 62eb30a29..c23c549b6 100644 --- a/src/core/request/engines/composition/index.ts +++ b/src/core/request/engines/composition/index.ts @@ -13,7 +13,7 @@ import Async from 'core/async'; import type { Provider } from 'core/data'; -import statusCodes, { StatusCodes } from 'core/status-codes'; +import statusCodes from 'core/status-codes'; import AbortablePromise from 'core/promise/abortable'; import { SyncPromise } from 'core/prelude/structures'; import { RequestOptions, Response, MiddlewareParams, RequestResponseObject, ResponseOptions } from 'core/request'; @@ -160,7 +160,7 @@ async function gatherDataFromRequests( ): Promise { const accumulator = { data: {}, - status: StatusCodes.OK, + status: statusCodes.OK, headers: {} }; From 8c4ca87bdd37270a34ad5da255aee237d9c95e99 Mon Sep 17 00:00:00 2001 From: Mikhail Kormanovskii Date: Fri, 8 Nov 2024 16:04:03 +0300 Subject: [PATCH 3/5] fix: fixed incorrect behaviour of propagateStatusAndHeaders in composition engine --- src/core/request/engines/composition/index.ts | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/src/core/request/engines/composition/index.ts b/src/core/request/engines/composition/index.ts index c23c549b6..e75c7a466 100644 --- a/src/core/request/engines/composition/index.ts +++ b/src/core/request/engines/composition/index.ts @@ -16,7 +16,7 @@ import type { Provider } from 'core/data'; import statusCodes from 'core/status-codes'; import AbortablePromise from 'core/promise/abortable'; import { SyncPromise } from 'core/prelude/structures'; -import { RequestOptions, Response, MiddlewareParams, RequestResponseObject, ResponseOptions } from 'core/request'; +import { RequestOptions, Response, MiddlewareParams, RequestResponseObject } from 'core/request'; import type { @@ -30,7 +30,6 @@ import type { } from 'core/request/engines/composition/interface'; import { compositionEngineSpreadResult } from 'core/request/engines/composition/const'; -import { RawHeaders } from 'core/request/headers'; export * from 'core/request/engines/composition/const'; export * from 'core/request/engines/composition/interface'; @@ -77,15 +76,37 @@ export function compositionEngine( const promises = compositionRequests.map((r) => SyncPromise.resolve(r.requestFilter?.(options)) .then((filterValue) => { if (filterValue === false) { - return; + return Promise.resolve( + {data: {}, headers: {}, status: statusCodes.NO_CONTENT} + ); } return r.request(options) .then(boundRequest.bind(null, async)) + .then( + (request) => isRequestResponseObject(request) ? + ({ + data: request.data, + headers: request.response.headers, + status: request.response.status + }) : ({ + data: request, + headers: {}, + status: statusCodes.OK + }) + ) .catch((err) => { if (r.failCompositionOnError) { throw err; } + + const details = err.details.deref()!; + + return { + data: {}, + status: details.response.status, + headers: details.response.headers + }; }); })); @@ -155,7 +176,7 @@ function boundRequest( * @param options - Options related to composition requests. */ async function gatherDataFromRequests( - promises: Array>, + promises: Array>, options: CompositionRequestOptions ): Promise { const accumulator = { @@ -197,31 +218,20 @@ async function gatherDataFromRequests( } /** - * Accumulates data into an accumulator object based on the composition request. + * Accumulates new data into an accumulator object based on the composition request. * * @param accumulator - * @param data + * @param newData * @param compositionRequest */ function accumulateData( accumulator: GatheredRequestsData, - responseOrData: RequestResponseObject | unknown, + newData: GatheredRequestsData, compositionRequest: CompositionRequest ): GatheredRequestsData { const - {as} = compositionRequest; - - let - data: unknown = {}, - {status, headers} = accumulator; - - if (isRequestResponseObject(responseOrData)) { - data = responseOrData.data; - status = responseOrData.response.status; - headers = responseOrData.response.headers; - } else { - data = responseOrData; - } + {as} = compositionRequest, + {status, headers, data} = newData; if (as === compositionEngineSpreadResult) { Object.assign(accumulator.data, data); From d255ee5c5bfd61b4281fdb9fd68168e75219d86e Mon Sep 17 00:00:00 2001 From: Mikhail Kormanovskii Date: Mon, 11 Nov 2024 01:31:22 +0300 Subject: [PATCH 4/5] fix: await request / request.data in promises array --- src/core/request/engines/composition/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/request/engines/composition/index.ts b/src/core/request/engines/composition/index.ts index e75c7a466..a62c643c9 100644 --- a/src/core/request/engines/composition/index.ts +++ b/src/core/request/engines/composition/index.ts @@ -84,13 +84,13 @@ export function compositionEngine( return r.request(options) .then(boundRequest.bind(null, async)) .then( - (request) => isRequestResponseObject(request) ? + async (request) => isRequestResponseObject(request) ? ({ - data: request.data, + data: await request.data, headers: request.response.headers, status: request.response.status }) : ({ - data: request, + data: await request, headers: {}, status: statusCodes.OK }) From 976788a10c8cde30aa514ae99caa155df46287e5 Mon Sep 17 00:00:00 2001 From: Mikhail Kormanovskii Date: Mon, 11 Nov 2024 02:00:33 +0300 Subject: [PATCH 5/5] chore: do not return meaningless status codes and data --- src/core/request/engines/composition/index.ts | 22 ++++++------------- .../request/engines/composition/interface.ts | 6 ++--- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/core/request/engines/composition/index.ts b/src/core/request/engines/composition/index.ts index a62c643c9..188fbbef0 100644 --- a/src/core/request/engines/composition/index.ts +++ b/src/core/request/engines/composition/index.ts @@ -76,9 +76,7 @@ export function compositionEngine( const promises = compositionRequests.map((r) => SyncPromise.resolve(r.requestFilter?.(options)) .then((filterValue) => { if (filterValue === false) { - return Promise.resolve( - {data: {}, headers: {}, status: statusCodes.NO_CONTENT} - ); + return {}; } return r.request(options) @@ -100,13 +98,7 @@ export function compositionEngine( throw err; } - const details = err.details.deref()!; - - return { - data: {}, - status: details.response.status, - headers: details.response.headers - }; + return {}; }); })); @@ -116,8 +108,8 @@ export function compositionEngine( important: requestOptions.important, responseType: 'object', okStatuses: requestOptions.okStatuses, - status, - headers, + status: status ?? statusCodes.OK, + headers: headers ?? {}, decoder: requestOptions.decoders, noContentStatuses: requestOptions.noContentStatuses })); @@ -180,9 +172,7 @@ async function gatherDataFromRequests( options: CompositionRequestOptions ): Promise { const accumulator = { - data: {}, - status: statusCodes.OK, - headers: {} + data: {} }; if (options.engineOptions?.aggregateErrors) { @@ -233,6 +223,8 @@ function accumulateData( {as} = compositionRequest, {status, headers, data} = newData; + accumulator.data ??= {}; + if (as === compositionEngineSpreadResult) { Object.assign(accumulator.data, data); diff --git a/src/core/request/engines/composition/interface.ts b/src/core/request/engines/composition/interface.ts index 5987bd59d..4bf2e9795 100644 --- a/src/core/request/engines/composition/interface.ts +++ b/src/core/request/engines/composition/interface.ts @@ -177,7 +177,7 @@ export interface CompositionRequestEngine extends RequestEngine { } export interface GatheredRequestsData { - data: Dictionary; - headers: RawHeaders; - status: StatusCodes; + data?: Dictionary; + headers?: RawHeaders; + status?: StatusCodes; }