From 48582e0e1c65559fb3058ca79dd91292a59f0c10 Mon Sep 17 00:00:00 2001 From: Vincent Germain Date: Thu, 27 Apr 2023 18:13:46 +0200 Subject: [PATCH 01/18] feat: start flatting all methods to one interceptors array --- packages/clients/src/index.ts | 1 + .../__tests__/{interceptor.ts => composer.ts} | 4 +- .../__tests__/{request.ts => network.ts} | 2 +- .../{interceptor.ts => composer.ts} | 0 .../interceptors/{request.ts => network.ts} | 24 +++++++++-- .../src/internal/interceptors/response.ts | 4 -- packages/clients/src/internals.ts | 9 +++- .../src/scw/__tests__/client-ini-factory.ts | 43 ++++++++++++++++--- .../src/scw/__tests__/client-settings.ts | 1 + packages/clients/src/scw/__tests__/client.ts | 4 +- packages/clients/src/scw/auth.ts | 4 +- .../clients/src/scw/client-ini-factory.ts | 28 ++++++++++-- packages/clients/src/scw/client-settings.ts | 19 ++++++-- packages/clients/src/scw/client.ts | 3 +- .../src/scw/fetch/__tests__/build-fetcher.ts | 1 + .../src/scw/fetch/__tests__/http-dumper.ts | 1 + .../clients/src/scw/fetch/build-fetcher.ts | 23 +++++++--- .../src/scw/fetch/http-interceptors.ts | 6 ++- 18 files changed, 139 insertions(+), 38 deletions(-) rename packages/clients/src/internal/interceptors/__tests__/{interceptor.ts => composer.ts} (90%) rename packages/clients/src/internal/interceptors/__tests__/{request.ts => network.ts} (99%) rename packages/clients/src/internal/interceptors/{interceptor.ts => composer.ts} (100%) rename packages/clients/src/internal/interceptors/{request.ts => network.ts} (58%) delete mode 100644 packages/clients/src/internal/interceptors/response.ts diff --git a/packages/clients/src/index.ts b/packages/clients/src/index.ts index e37e9e8f6..35716bdd7 100644 --- a/packages/clients/src/index.ts +++ b/packages/clients/src/index.ts @@ -9,6 +9,7 @@ export type { Client } from './scw/client' export type { Profile } from './scw/client-ini-profile' export type { Settings } from './scw/client-settings' export { + withAdditionalInterceptors, withDefaultPageSize, withHTTPClient, withProfile, diff --git a/packages/clients/src/internal/interceptors/__tests__/interceptor.ts b/packages/clients/src/internal/interceptors/__tests__/composer.ts similarity index 90% rename from packages/clients/src/internal/interceptors/__tests__/interceptor.ts rename to packages/clients/src/internal/interceptors/__tests__/composer.ts index dc49ada27..66a2a9601 100644 --- a/packages/clients/src/internal/interceptors/__tests__/interceptor.ts +++ b/packages/clients/src/internal/interceptors/__tests__/composer.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from '@jest/globals' -import type { Interceptor } from '../interceptor' -import { composeInterceptors } from '../interceptor' +import type { Interceptor } from '../composer' +import { composeInterceptors } from '../composer' type StringArrayInterceptor = Interceptor diff --git a/packages/clients/src/internal/interceptors/__tests__/request.ts b/packages/clients/src/internal/interceptors/__tests__/network.ts similarity index 99% rename from packages/clients/src/internal/interceptors/__tests__/request.ts rename to packages/clients/src/internal/interceptors/__tests__/network.ts index 697748b53..25c24787c 100644 --- a/packages/clients/src/internal/interceptors/__tests__/request.ts +++ b/packages/clients/src/internal/interceptors/__tests__/network.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from '@jest/globals' -import { addAsyncHeaderInterceptor, addHeaderInterceptor } from '../request' +import { addAsyncHeaderInterceptor, addHeaderInterceptor } from '../network' describe('addHeaderInterceptor', () => { it('insertsnothing if value is undefined', async () => { diff --git a/packages/clients/src/internal/interceptors/interceptor.ts b/packages/clients/src/internal/interceptors/composer.ts similarity index 100% rename from packages/clients/src/internal/interceptors/interceptor.ts rename to packages/clients/src/internal/interceptors/composer.ts diff --git a/packages/clients/src/internal/interceptors/request.ts b/packages/clients/src/internal/interceptors/network.ts similarity index 58% rename from packages/clients/src/internal/interceptors/request.ts rename to packages/clients/src/internal/interceptors/network.ts index 5b8f42509..cea694f4f 100644 --- a/packages/clients/src/internal/interceptors/request.ts +++ b/packages/clients/src/internal/interceptors/network.ts @@ -1,7 +1,25 @@ -import type { Interceptor } from './interceptor' +export interface RequestInterceptor { + (request: Readonly): Request | Promise +} -/** Request Interceptor. */ -export type RequestInterceptor = Interceptor +export interface RequestErrorInterceptor { + (error: unknown): unknown | Promise +} + +export interface ResponseInterceptor { + (response: Readonly): Response | Promise +} + +export interface ResponseErrorInterceptor { + (request: Request, error: unknown): unknown | Promise +} + +export type NetworkInterceptors = { + request?: RequestInterceptor + requestError?: RequestErrorInterceptor + response?: ResponseInterceptor + responseError?: ResponseErrorInterceptor +} /** * Adds an header to a request through an interceptor. diff --git a/packages/clients/src/internal/interceptors/response.ts b/packages/clients/src/internal/interceptors/response.ts deleted file mode 100644 index 4e78d1c83..000000000 --- a/packages/clients/src/internal/interceptors/response.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { Interceptor } from './interceptor' - -/** Response Interceptor. */ -export type ResponseInterceptor = Interceptor diff --git a/packages/clients/src/internals.ts b/packages/clients/src/internals.ts index 5b9053b9f..6e5447012 100644 --- a/packages/clients/src/internals.ts +++ b/packages/clients/src/internals.ts @@ -1,7 +1,12 @@ export { isJSONObject } from './helpers/json' export { waitForResource } from './internal/async/interval-retrier' -export type { RequestInterceptor } from './internal/interceptors/request' -export type { ResponseInterceptor } from './internal/interceptors/response' +export type { + NetworkInterceptors, + RequestInterceptor, + ResponseInterceptor, + RequestErrorInterceptor, + ResponseErrorInterceptor, +} from './internal/interceptors/network' export { API } from './scw/api' export { authenticateWithSessionToken } from './scw/auth' export type { DefaultValues } from './scw/client-settings' diff --git a/packages/clients/src/scw/__tests__/client-ini-factory.ts b/packages/clients/src/scw/__tests__/client-ini-factory.ts index dd6d9b0ff..18f53c50b 100644 --- a/packages/clients/src/scw/__tests__/client-ini-factory.ts +++ b/packages/clients/src/scw/__tests__/client-ini-factory.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from '@jest/globals' import { + withAdditionalInterceptors, withDefaultPageSize, withHTTPClient, withProfile, @@ -29,11 +30,34 @@ const DEFAULT_SETTINGS: Readonly = { defaultRegion: 'nl-ams', defaultZone: 'fr-par-1', httpClient: fetch, + interceptors: [], requestInterceptors: [], responseInterceptors: [], userAgent: 'scaleway-sdk-js/v1.0.0-beta', } +describe('withAdditionalInterceptors', () => { + it('appends interceptors to existing ones', () => { + const oneInterProfile = withAdditionalInterceptors([ + { + request: req => req, + }, + ])(DEFAULT_SETTINGS) + const twoInterProfile = withAdditionalInterceptors([ + { + requestError: err => err, + response: res => res, + responseError: err => err, + }, + ])(oneInterProfile) + expect(twoInterProfile.interceptors.length).toEqual(2) + expect(twoInterProfile.interceptors[1].requestError).toBeDefined() + expect(twoInterProfile.interceptors[1].response).toBeDefined() + expect(twoInterProfile.interceptors[1].responseError).toBeDefined() + expect(twoInterProfile.interceptors[1].request).toBeUndefined() + }) +}) + describe('withProfile', () => { it(`doesn't modify Settings object with empty Profile object`, () => { expect(withProfile(EMPTY_PROFILE)(DEFAULT_SETTINGS)).toStrictEqual( @@ -53,6 +77,7 @@ describe('withProfile', () => { defaultRegion: undefined, defaultZone: undefined, httpClient: undefined, + interceptors: undefined, requestInterceptors: undefined, responseInterceptors: undefined, userAgent: undefined, @@ -67,6 +92,7 @@ describe('withProfile', () => { defaultRegion: null, defaultZone: null, httpClient: null, + interceptors: null, requestInterceptors: null, responseInterceptors: null, userAgent: null, @@ -81,6 +107,7 @@ describe('withProfile', () => { defaultRegion: '', defaultZone: '', httpClient: '', + interceptors: '', requestInterceptors: '', responseInterceptors: '', userAgent: '', @@ -95,6 +122,7 @@ describe('withProfile', () => { defaultRegion: 0, defaultZone: 0, httpClient: 0, + interceptors: 0, requestInterceptors: 0, responseInterceptors: 0, userAgent: 0, @@ -163,13 +191,18 @@ describe('withProfile', () => { }) it('modifies authentication', async () => { - const { headers } = await withProfile({ + const request = new Request(DEFAULT_SETTINGS.apiURL) + const reqInterceptor = withProfile({ accessKey: FILLED_PROFILE.accessKey, secretKey: FILLED_PROFILE.secretKey, - })(DEFAULT_SETTINGS).requestInterceptors[0]( - new Request(DEFAULT_SETTINGS.apiURL), - ) - expect(headers.get('x-auth-token')).toStrictEqual(FILLED_PROFILE.secretKey) + })(DEFAULT_SETTINGS).interceptors[0].request + expect(reqInterceptor).toBeDefined() + if (reqInterceptor) { + const { headers } = await reqInterceptor(request) + expect(headers.get('x-auth-token')).toStrictEqual( + FILLED_PROFILE.secretKey, + ) + } }) }) diff --git a/packages/clients/src/scw/__tests__/client-settings.ts b/packages/clients/src/scw/__tests__/client-settings.ts index 3d49376d5..35852c36b 100644 --- a/packages/clients/src/scw/__tests__/client-settings.ts +++ b/packages/clients/src/scw/__tests__/client-settings.ts @@ -10,6 +10,7 @@ const VALID_SETTINGS: Settings = { defaultRegion: 'fr-par', defaultZone: 'fr-par-1', httpClient: fetch, + interceptors: [], requestInterceptors: [], responseInterceptors: [], userAgent: 'scaleway-sdk-js/v1.0.0-beta', diff --git a/packages/clients/src/scw/__tests__/client.ts b/packages/clients/src/scw/__tests__/client.ts index b15f5de39..046fed9aa 100644 --- a/packages/clients/src/scw/__tests__/client.ts +++ b/packages/clients/src/scw/__tests__/client.ts @@ -36,7 +36,7 @@ describe('createAdvancedClient', () => { secretKey: '3aef5281-13eb-4705-b858-eb64dd5da24c', }), ) - expect(client.settings.requestInterceptors.length).toBe(1) + expect(client.settings.interceptors.length).toBe(1) }) it('does not mutate default requestInterceptors', () => { @@ -52,7 +52,7 @@ describe('createAdvancedClient', () => { secretKey: '3aef5281-13eb-4705-b858-eb64dd5da24c', }), ) - expect(client.settings.requestInterceptors.length).toBe(1) + expect(client.settings.interceptors.length).toBe(1) }) it('contains override from custom option', () => { diff --git a/packages/clients/src/scw/auth.ts b/packages/clients/src/scw/auth.ts index d9e76f68f..fe34bef1b 100644 --- a/packages/clients/src/scw/auth.ts +++ b/packages/clients/src/scw/auth.ts @@ -1,8 +1,8 @@ -import type { RequestInterceptor } from '../internal/interceptors/request' +import type { RequestInterceptor } from '../internal/interceptors/network' import { addAsyncHeaderInterceptor, addHeaderInterceptor, -} from '../internal/interceptors/request' +} from '../internal/interceptors/network' import { assertValidAuthenticationSecrets } from './client-ini-profile' import type { AuthenticationSecrets } from './client-ini-profile' diff --git a/packages/clients/src/scw/client-ini-factory.ts b/packages/clients/src/scw/client-ini-factory.ts index 700850bd1..910a14477 100644 --- a/packages/clients/src/scw/client-ini-factory.ts +++ b/packages/clients/src/scw/client-ini-factory.ts @@ -1,3 +1,4 @@ +import type { NetworkInterceptors } from '../internals' import { authenticateWithSecrets } from './auth' import { hasAuthenticationSecrets } from './client-ini-profile' import type { Profile } from './client-ini-profile' @@ -40,9 +41,11 @@ export const withProfile = newSettings.defaultZone = profile.defaultZone } if (hasAuthenticationSecrets(profile)) { - newSettings.requestInterceptors = [ - authenticateWithSecrets(profile), - ...settings.requestInterceptors, + newSettings.interceptors = [ + { + request: authenticateWithSecrets(profile), + }, + ...newSettings.interceptors, ] } @@ -109,3 +112,22 @@ export const withUserAgentSuffix = ? `${settings.userAgent} ${userAgent}` : userAgent, }) + +/** + * Instantiates the SDK with additional interceptors. + * + * @param interceptors - The additional interceptors + * @returns A factory {@link ClientConfig} + * + * @remarks + * It doesn't override the existing interceptors, but instead push more to the list. + * This method should be used in conjunction with the initializer `createAdvancedClient`. + * + * @public + */ +export const withAdditionalInterceptors = + (interceptors: NetworkInterceptors[]) => + (settings: Readonly): Settings => ({ + ...settings, + interceptors: settings.interceptors.concat(interceptors), + }) diff --git a/packages/clients/src/scw/client-settings.ts b/packages/clients/src/scw/client-settings.ts index fe90f9520..e55d4483d 100644 --- a/packages/clients/src/scw/client-settings.ts +++ b/packages/clients/src/scw/client-settings.ts @@ -1,5 +1,8 @@ -import type { RequestInterceptor } from '../internal/interceptors/request' -import type { ResponseInterceptor } from '../internal/interceptors/response' +import type { + NetworkInterceptors, + RequestInterceptor, + ResponseInterceptor, +} from '../internal/interceptors/network' import { isOrganizationId, isProjectId, @@ -37,12 +40,20 @@ export interface Settings extends DefaultValues { httpClient: typeof fetch /** * The Request interceptors. + * + * @deprecated Please use `interceptors` instead. */ - requestInterceptors: RequestInterceptor[] + requestInterceptors?: RequestInterceptor[] /** * The Response interceptors. + * + * @deprecated Please use `interceptors` instead. + */ + responseInterceptors?: ResponseInterceptor[] + /** + * The Network interceptors. */ - responseInterceptors: ResponseInterceptor[] + interceptors: NetworkInterceptors[] /** * The User-Agent sent with each request. * diff --git a/packages/clients/src/scw/client.ts b/packages/clients/src/scw/client.ts index 2455be74e..c4a675b65 100644 --- a/packages/clients/src/scw/client.ts +++ b/packages/clients/src/scw/client.ts @@ -12,8 +12,7 @@ import type { Fetcher } from './fetch/build-fetcher' const DEFAULT_SETTINGS: Settings = { apiURL: 'https://api.scaleway.com', httpClient: fetch, - requestInterceptors: [], - responseInterceptors: [], + interceptors: [], userAgent, } diff --git a/packages/clients/src/scw/fetch/__tests__/build-fetcher.ts b/packages/clients/src/scw/fetch/__tests__/build-fetcher.ts index f7fc53ee9..ac74e01dc 100644 --- a/packages/clients/src/scw/fetch/__tests__/build-fetcher.ts +++ b/packages/clients/src/scw/fetch/__tests__/build-fetcher.ts @@ -9,6 +9,7 @@ const DEFAULT_SETTINGS: Settings = { defaultRegion: 'fr-par', defaultZone: 'fr-par-1', httpClient: global.fetch, + interceptors: [], requestInterceptors: [], responseInterceptors: [], userAgent: 'scaleway-sdk-js/v1.0.0', diff --git a/packages/clients/src/scw/fetch/__tests__/http-dumper.ts b/packages/clients/src/scw/fetch/__tests__/http-dumper.ts index c41e04498..0dd51b6a4 100644 --- a/packages/clients/src/scw/fetch/__tests__/http-dumper.ts +++ b/packages/clients/src/scw/fetch/__tests__/http-dumper.ts @@ -9,6 +9,7 @@ const DEFAULT_SETTINGS: Settings = { defaultRegion: 'fr-par', defaultZone: 'fr-par-1', httpClient: fetch, + interceptors: [], requestInterceptors: [], responseInterceptors: [], userAgent: 'scaleway-sdk-js/v1.0.0', diff --git a/packages/clients/src/scw/fetch/build-fetcher.ts b/packages/clients/src/scw/fetch/build-fetcher.ts index 6fe1d0d65..31161f8b9 100644 --- a/packages/clients/src/scw/fetch/build-fetcher.ts +++ b/packages/clients/src/scw/fetch/build-fetcher.ts @@ -1,5 +1,6 @@ import { isBrowser } from '../../helpers/is-browser' -import { composeInterceptors } from '../../internal/interceptors/interceptor' +import { composeInterceptors } from '../../internal/interceptors/composer' +import type { RequestInterceptor, ResponseInterceptor } from '../../internals' import { obfuscateAuthHeadersEntry } from '../auth' import type { Settings } from '../client-settings' import { @@ -61,12 +62,16 @@ export const buildFetcher = (settings: Settings, httpClient: typeof fetch) => { let requestNumber = 0 const prepareRequest = (requestId: string) => composeInterceptors([ - ...settings.requestInterceptors, + ...(settings.interceptors + .map(obj => obj.request) + .filter(obj => obj) as RequestInterceptor[]), logRequest(requestId, obfuscateInterceptor(obfuscateAuthHeadersEntry)), ]) const prepareResponse = (requestId: string) => composeInterceptors([ - ...settings.responseInterceptors, + ...(settings.interceptors + .map(obj => obj.response) + .filter(obj => obj) as ResponseInterceptor[]), logResponse(requestId), ]) @@ -75,11 +80,17 @@ export const buildFetcher = (settings: Settings, httpClient: typeof fetch) => { unwrapper: ResponseUnmarshaller = asIs, ): Promise => { const requestId = `${(requestNumber += 1)}` + const finalRequest = buildRequest(request, settings) + const reqInterceptors = prepareRequest(requestId) + const resInterceptors = responseParser( + unwrapper, + request.responseType ?? 'json', + ) - return Promise.resolve(buildRequest(request, settings)) - .then(prepareRequest(requestId)) + return Promise.resolve(finalRequest) + .then(reqInterceptors) .then(httpClient) .then(prepareResponse(requestId)) - .then(responseParser(unwrapper, request.responseType ?? 'json')) + .then(resInterceptors) } } diff --git a/packages/clients/src/scw/fetch/http-interceptors.ts b/packages/clients/src/scw/fetch/http-interceptors.ts index c9d914391..64dc670f0 100644 --- a/packages/clients/src/scw/fetch/http-interceptors.ts +++ b/packages/clients/src/scw/fetch/http-interceptors.ts @@ -1,5 +1,7 @@ -import type { RequestInterceptor } from '../../internal/interceptors/request' -import type { ResponseInterceptor } from '../../internal/interceptors/response' +import type { + RequestInterceptor, + ResponseInterceptor, +} from '../../internal/interceptors/network' import { getLogger } from '../../internal/logger' import { LevelResolver, shouldLog } from '../../internal/logger/level-resolver' import { dumpRequest, dumpResponse } from './http-dumper' From ad1008c84b62d0cdb08aa49ed0bb1d3ac89b89b3 Mon Sep 17 00:00:00 2001 From: Vincent Germain Date: Fri, 28 Apr 2023 12:14:48 +0200 Subject: [PATCH 02/18] feat: request error composer --- .../__tests__/{network.ts => helpers.ts} | 2 +- .../src/internal/interceptors/composer.ts | 24 +++++++++++++++++++ .../interceptors/{network.ts => helpers.ts} | 23 +----------------- .../src/internal/interceptors/types.ts | 22 +++++++++++++++++ packages/clients/src/internals.ts | 2 +- packages/clients/src/scw/auth.ts | 4 ++-- packages/clients/src/scw/client-settings.ts | 2 +- .../src/scw/fetch/http-interceptors.ts | 2 +- 8 files changed, 53 insertions(+), 28 deletions(-) rename packages/clients/src/internal/interceptors/__tests__/{network.ts => helpers.ts} (99%) rename packages/clients/src/internal/interceptors/{network.ts => helpers.ts} (58%) create mode 100644 packages/clients/src/internal/interceptors/types.ts diff --git a/packages/clients/src/internal/interceptors/__tests__/network.ts b/packages/clients/src/internal/interceptors/__tests__/helpers.ts similarity index 99% rename from packages/clients/src/internal/interceptors/__tests__/network.ts rename to packages/clients/src/internal/interceptors/__tests__/helpers.ts index 25c24787c..94800c81f 100644 --- a/packages/clients/src/internal/interceptors/__tests__/network.ts +++ b/packages/clients/src/internal/interceptors/__tests__/helpers.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from '@jest/globals' -import { addAsyncHeaderInterceptor, addHeaderInterceptor } from '../network' +import { addAsyncHeaderInterceptor, addHeaderInterceptor } from '../helpers' describe('addHeaderInterceptor', () => { it('insertsnothing if value is undefined', async () => { diff --git a/packages/clients/src/internal/interceptors/composer.ts b/packages/clients/src/internal/interceptors/composer.ts index fd2dab2fc..5cea46d1d 100644 --- a/packages/clients/src/internal/interceptors/composer.ts +++ b/packages/clients/src/internal/interceptors/composer.ts @@ -1,3 +1,5 @@ +import type { RequestErrorInterceptor } from './types' + export interface Interceptor { (instance: Readonly): T | Promise } @@ -19,3 +21,25 @@ export const composeInterceptors = resolve(instance) }), ) + +/** + * Compose request error interceptors. + * + * @internal + */ +export const composeRequestErrorInterceptors = + (interceptors: RequestErrorInterceptor[]) => + async (error: unknown): Promise => { + let prevError = error + for (const interceptor of interceptors) { + try { + const res = await interceptor(prevError) + + return res + } catch (err) { + prevError = err + } + } + + throw error + } diff --git a/packages/clients/src/internal/interceptors/network.ts b/packages/clients/src/internal/interceptors/helpers.ts similarity index 58% rename from packages/clients/src/internal/interceptors/network.ts rename to packages/clients/src/internal/interceptors/helpers.ts index cea694f4f..f579a974c 100644 --- a/packages/clients/src/internal/interceptors/network.ts +++ b/packages/clients/src/internal/interceptors/helpers.ts @@ -1,25 +1,4 @@ -export interface RequestInterceptor { - (request: Readonly): Request | Promise -} - -export interface RequestErrorInterceptor { - (error: unknown): unknown | Promise -} - -export interface ResponseInterceptor { - (response: Readonly): Response | Promise -} - -export interface ResponseErrorInterceptor { - (request: Request, error: unknown): unknown | Promise -} - -export type NetworkInterceptors = { - request?: RequestInterceptor - requestError?: RequestErrorInterceptor - response?: ResponseInterceptor - responseError?: ResponseErrorInterceptor -} +import type { RequestInterceptor } from './types' /** * Adds an header to a request through an interceptor. diff --git a/packages/clients/src/internal/interceptors/types.ts b/packages/clients/src/internal/interceptors/types.ts new file mode 100644 index 000000000..e6cd8b58f --- /dev/null +++ b/packages/clients/src/internal/interceptors/types.ts @@ -0,0 +1,22 @@ +export interface RequestInterceptor { + (request: Readonly): Request | Promise +} + +export interface RequestErrorInterceptor { + (error: unknown): unknown | Promise +} + +export interface ResponseInterceptor { + (response: Readonly): Response | Promise +} + +export interface ResponseErrorInterceptor { + (request: Request, error: unknown): unknown | Promise +} + +export type NetworkInterceptors = { + request?: RequestInterceptor + requestError?: RequestErrorInterceptor + response?: ResponseInterceptor + responseError?: ResponseErrorInterceptor +} diff --git a/packages/clients/src/internals.ts b/packages/clients/src/internals.ts index 6e5447012..896662c91 100644 --- a/packages/clients/src/internals.ts +++ b/packages/clients/src/internals.ts @@ -6,7 +6,7 @@ export type { ResponseInterceptor, RequestErrorInterceptor, ResponseErrorInterceptor, -} from './internal/interceptors/network' +} from './internal/interceptors/types' export { API } from './scw/api' export { authenticateWithSessionToken } from './scw/auth' export type { DefaultValues } from './scw/client-settings' diff --git a/packages/clients/src/scw/auth.ts b/packages/clients/src/scw/auth.ts index fe34bef1b..911a0f8fa 100644 --- a/packages/clients/src/scw/auth.ts +++ b/packages/clients/src/scw/auth.ts @@ -1,8 +1,8 @@ -import type { RequestInterceptor } from '../internal/interceptors/network' import { addAsyncHeaderInterceptor, addHeaderInterceptor, -} from '../internal/interceptors/network' +} from '../internal/interceptors/helpers' +import type { RequestInterceptor } from '../internal/interceptors/types' import { assertValidAuthenticationSecrets } from './client-ini-profile' import type { AuthenticationSecrets } from './client-ini-profile' diff --git a/packages/clients/src/scw/client-settings.ts b/packages/clients/src/scw/client-settings.ts index e55d4483d..f2fe7f49b 100644 --- a/packages/clients/src/scw/client-settings.ts +++ b/packages/clients/src/scw/client-settings.ts @@ -2,7 +2,7 @@ import type { NetworkInterceptors, RequestInterceptor, ResponseInterceptor, -} from '../internal/interceptors/network' +} from '../internal/interceptors/types' import { isOrganizationId, isProjectId, diff --git a/packages/clients/src/scw/fetch/http-interceptors.ts b/packages/clients/src/scw/fetch/http-interceptors.ts index 64dc670f0..6a1412883 100644 --- a/packages/clients/src/scw/fetch/http-interceptors.ts +++ b/packages/clients/src/scw/fetch/http-interceptors.ts @@ -1,7 +1,7 @@ import type { RequestInterceptor, ResponseInterceptor, -} from '../../internal/interceptors/network' +} from '../../internal/interceptors/types' import { getLogger } from '../../internal/logger' import { LevelResolver, shouldLog } from '../../internal/logger/level-resolver' import { dumpRequest, dumpResponse } from './http-dumper' From ef598a154092786db91d0bc133018df689bdda45 Mon Sep 17 00:00:00 2001 From: Vincent Germain Date: Fri, 28 Apr 2023 15:56:59 +0200 Subject: [PATCH 03/18] feat: starting with response error interceptors --- .../src/internal/interceptors/composer.ts | 26 ++++++++++++++++- .../src/internal/interceptors/types.ts | 8 ++--- packages/clients/src/internals.ts | 2 +- .../src/scw/__tests__/client-ini-factory.ts | 4 +-- .../clients/src/scw/fetch/build-fetcher.ts | 29 +++++++++++++++++-- 5 files changed, 58 insertions(+), 11 deletions(-) diff --git a/packages/clients/src/internal/interceptors/composer.ts b/packages/clients/src/internal/interceptors/composer.ts index 5cea46d1d..381459b15 100644 --- a/packages/clients/src/internal/interceptors/composer.ts +++ b/packages/clients/src/internal/interceptors/composer.ts @@ -1,4 +1,4 @@ -import type { RequestErrorInterceptor } from './types' +import type { ResponseErrorInterceptor } from './types' export interface Interceptor { (instance: Readonly): T | Promise @@ -22,11 +22,34 @@ export const composeInterceptors = }), ) +/** + * Compose response error interceptors. + * + * @internal + */ +export const composeResponseErrorInterceptors = + (interceptors: ResponseErrorInterceptor[]) => + async (request: Request, error: unknown): Promise => { + let prevError = error + for (const interceptor of interceptors) { + try { + const res = await interceptor(request, prevError) + + return res + } catch (err) { + prevError = err + } + } + + throw error + } + /** * Compose request error interceptors. * * @internal */ +/* export const composeRequestErrorInterceptors = (interceptors: RequestErrorInterceptor[]) => async (error: unknown): Promise => { @@ -43,3 +66,4 @@ export const composeRequestErrorInterceptors = throw error } +*/ diff --git a/packages/clients/src/internal/interceptors/types.ts b/packages/clients/src/internal/interceptors/types.ts index e6cd8b58f..c00db33a2 100644 --- a/packages/clients/src/internal/interceptors/types.ts +++ b/packages/clients/src/internal/interceptors/types.ts @@ -2,9 +2,9 @@ export interface RequestInterceptor { (request: Readonly): Request | Promise } -export interface RequestErrorInterceptor { - (error: unknown): unknown | Promise -} +// export interface RequestErrorInterceptor { +// (error: unknown): unknown | Promise +// } export interface ResponseInterceptor { (response: Readonly): Response | Promise @@ -16,7 +16,7 @@ export interface ResponseErrorInterceptor { export type NetworkInterceptors = { request?: RequestInterceptor - requestError?: RequestErrorInterceptor + // requestError?: RequestErrorInterceptor response?: ResponseInterceptor responseError?: ResponseErrorInterceptor } diff --git a/packages/clients/src/internals.ts b/packages/clients/src/internals.ts index 896662c91..6d3d333c9 100644 --- a/packages/clients/src/internals.ts +++ b/packages/clients/src/internals.ts @@ -4,7 +4,7 @@ export type { NetworkInterceptors, RequestInterceptor, ResponseInterceptor, - RequestErrorInterceptor, + // RequestErrorInterceptor, ResponseErrorInterceptor, } from './internal/interceptors/types' export { API } from './scw/api' diff --git a/packages/clients/src/scw/__tests__/client-ini-factory.ts b/packages/clients/src/scw/__tests__/client-ini-factory.ts index 18f53c50b..2eb76dbce 100644 --- a/packages/clients/src/scw/__tests__/client-ini-factory.ts +++ b/packages/clients/src/scw/__tests__/client-ini-factory.ts @@ -45,13 +45,13 @@ describe('withAdditionalInterceptors', () => { ])(DEFAULT_SETTINGS) const twoInterProfile = withAdditionalInterceptors([ { - requestError: err => err, + // requestError: err => err, response: res => res, responseError: err => err, }, ])(oneInterProfile) expect(twoInterProfile.interceptors.length).toEqual(2) - expect(twoInterProfile.interceptors[1].requestError).toBeDefined() + // expect(twoInterProfile.interceptors[1].requestError).toBeDefined() expect(twoInterProfile.interceptors[1].response).toBeDefined() expect(twoInterProfile.interceptors[1].responseError).toBeDefined() expect(twoInterProfile.interceptors[1].request).toBeUndefined() diff --git a/packages/clients/src/scw/fetch/build-fetcher.ts b/packages/clients/src/scw/fetch/build-fetcher.ts index 31161f8b9..e2bb320ea 100644 --- a/packages/clients/src/scw/fetch/build-fetcher.ts +++ b/packages/clients/src/scw/fetch/build-fetcher.ts @@ -1,6 +1,13 @@ import { isBrowser } from '../../helpers/is-browser' -import { composeInterceptors } from '../../internal/interceptors/composer' -import type { RequestInterceptor, ResponseInterceptor } from '../../internals' +import { + composeInterceptors, + composeResponseErrorInterceptors, +} from '../../internal/interceptors/composer' +import type { + RequestInterceptor, + ResponseErrorInterceptor, + ResponseInterceptor, +} from '../../internals' import { obfuscateAuthHeadersEntry } from '../auth' import type { Settings } from '../client-settings' import { @@ -74,6 +81,18 @@ export const buildFetcher = (settings: Settings, httpClient: typeof fetch) => { .filter(obj => obj) as ResponseInterceptor[]), logResponse(requestId), ]) + // const prepareRequestErrors = () => + // composeRequestErrorInterceptors( + // settings.interceptors + // .map(obj => obj.requestError) + // .filter(obj => obj) as RequestErrorInterceptor[] + // ) + const prepareResponseErrors = () => + composeResponseErrorInterceptors( + settings.interceptors + .map(obj => obj.responseError) + .filter(obj => obj) as ResponseErrorInterceptor[], + ) return async ( request: Readonly, @@ -86,11 +105,15 @@ export const buildFetcher = (settings: Settings, httpClient: typeof fetch) => { unwrapper, request.responseType ?? 'json', ) + const resErrorInterceptors = prepareResponseErrors() - return Promise.resolve(finalRequest) + const output = Promise.resolve(finalRequest) .then(reqInterceptors) .then(httpClient) .then(prepareResponse(requestId)) .then(resInterceptors) + .catch(obj => resErrorInterceptors(finalRequest, obj) as T) + + return output } } From b534b06aa844452f186b9f568a5757c692191e51 Mon Sep 17 00:00:00 2001 From: Vincent Germain Date: Wed, 3 May 2023 12:42:49 +0200 Subject: [PATCH 04/18] feat: merge legacy interceptors --- .../clients/src/scw/client-ini-factory.ts | 26 +++++++++++++++++++ packages/clients/src/scw/client.ts | 14 +++++----- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/packages/clients/src/scw/client-ini-factory.ts b/packages/clients/src/scw/client-ini-factory.ts index 910a14477..84feeb515 100644 --- a/packages/clients/src/scw/client-ini-factory.ts +++ b/packages/clients/src/scw/client-ini-factory.ts @@ -131,3 +131,29 @@ export const withAdditionalInterceptors = ...settings, interceptors: settings.interceptors.concat(interceptors), }) + +/** + * Instantiates the SDK with legacy interceptors. + */ +/* eslint-disable deprecation/deprecation */ +export const withLegacyInterceptors = + () => + (settings: Readonly): Settings => { + if (!settings.requestInterceptors && !settings.responseInterceptors) { + return settings + } + const allInterceptors = settings.interceptors.concat( + (settings.requestInterceptors ?? []).map(obj => ({ + request: obj, + })), + (settings.responseInterceptors ?? []).map(obj => ({ + response: obj, + })), + ) + + return { + ...settings, + interceptors: allInterceptors, + } + } +/* eslint-enable deprecation/deprecation */ diff --git a/packages/clients/src/scw/client.ts b/packages/clients/src/scw/client.ts index c4a675b65..defc6b660 100644 --- a/packages/clients/src/scw/client.ts +++ b/packages/clients/src/scw/client.ts @@ -1,6 +1,6 @@ import { getLogger } from '../internal/logger' -import type { ClientConfig } from './client-ini-factory' -import { withProfile } from './client-ini-factory' +import type { ClientConfig} from './client-ini-factory'; +import { withLegacyInterceptors , withProfile } from './client-ini-factory' import type { Profile } from './client-ini-profile' import type { Settings } from './client-settings' import { assertValidSettings } from './client-settings' @@ -49,10 +49,12 @@ export type Client = { * @public */ export const createAdvancedClient = (...configs: ClientConfig[]): Client => { - const settings = configs.reduce( - (currentSettings, config) => config(currentSettings), - DEFAULT_SETTINGS, - ) + const settings = configs + .concat([withLegacyInterceptors()]) + .reduce( + (currentSettings, config) => config(currentSettings), + DEFAULT_SETTINGS, + ) assertValidSettings(settings) getLogger().info(`init Scaleway SDK version ${version}`) From 871fc6643199e64289a09de1788b993726c530d3 Mon Sep 17 00:00:00 2001 From: Vincent Germain Date: Wed, 3 May 2023 13:15:51 +0200 Subject: [PATCH 05/18] test: composeResponseErrorInterceptors --- .../interceptors/__tests__/composer.ts | 50 ++++++++++++++++++- .../src/internal/interceptors/composer.ts | 24 --------- packages/clients/src/scw/client.ts | 4 +- 3 files changed, 50 insertions(+), 28 deletions(-) diff --git a/packages/clients/src/internal/interceptors/__tests__/composer.ts b/packages/clients/src/internal/interceptors/__tests__/composer.ts index 66a2a9601..e626a18a8 100644 --- a/packages/clients/src/internal/interceptors/__tests__/composer.ts +++ b/packages/clients/src/internal/interceptors/__tests__/composer.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from '@jest/globals' import type { Interceptor } from '../composer' -import { composeInterceptors } from '../composer' +import { + composeInterceptors, + composeResponseErrorInterceptors, +} from '../composer' type StringArrayInterceptor = Interceptor @@ -14,7 +17,7 @@ const removeFirstStrInterceptor = (list: Readonly): string[] => list.slice(1) -describe('applyInterceptors', () => { +describe('composeInterceptors', () => { it('calls one interceptor', async () => { const interceptor = composeInterceptors([addStrInterceptor('Fusion')]) expect(await interceptor(['Tree', 'Animal'])).toStrictEqual([ @@ -35,3 +38,46 @@ describe('applyInterceptors', () => { ]) }) }) + +describe('composeResponseErrorInterceptors', () => { + it('passes the error to all interceptors if they all throw', () => { + class NumberError extends Error { + counter: number + + constructor(obj: number) { + super() + this.counter = obj + Object.setPrototypeOf(this, NumberError.prototype) + } + } + + const interceptors = composeResponseErrorInterceptors([ + (_: Request, error: unknown): Promise => { + throw error instanceof NumberError + ? new NumberError(error.counter + 1) + : error + }, + (_: Request, error: unknown): Promise => { + throw error instanceof NumberError + ? new NumberError(error.counter + 2) + : error + }, + ])(new Request('https://api.scaleway.com'), new NumberError(42)) + + return expect(interceptors).rejects.toThrow(new NumberError(45)) + }) + + it('stops at the second interceptor (amongst three) if it resolves', () => { + const interceptors = composeResponseErrorInterceptors([ + (_: Request, error: unknown): Promise => { + throw error + }, + (): Promise => Promise.resolve(42), + (_: Request, error: unknown): Promise => { + throw error + }, + ])(new Request('https://api.scaleway.com'), new TypeError('')) + + return expect(interceptors).resolves.toBe(42) + }) +}) diff --git a/packages/clients/src/internal/interceptors/composer.ts b/packages/clients/src/internal/interceptors/composer.ts index 381459b15..8c636e255 100644 --- a/packages/clients/src/internal/interceptors/composer.ts +++ b/packages/clients/src/internal/interceptors/composer.ts @@ -43,27 +43,3 @@ export const composeResponseErrorInterceptors = throw error } - -/** - * Compose request error interceptors. - * - * @internal - */ -/* -export const composeRequestErrorInterceptors = - (interceptors: RequestErrorInterceptor[]) => - async (error: unknown): Promise => { - let prevError = error - for (const interceptor of interceptors) { - try { - const res = await interceptor(prevError) - - return res - } catch (err) { - prevError = err - } - } - - throw error - } -*/ diff --git a/packages/clients/src/scw/client.ts b/packages/clients/src/scw/client.ts index defc6b660..059ad7d15 100644 --- a/packages/clients/src/scw/client.ts +++ b/packages/clients/src/scw/client.ts @@ -1,6 +1,6 @@ import { getLogger } from '../internal/logger' -import type { ClientConfig} from './client-ini-factory'; -import { withLegacyInterceptors , withProfile } from './client-ini-factory' +import type { ClientConfig } from './client-ini-factory' +import { withLegacyInterceptors, withProfile } from './client-ini-factory' import type { Profile } from './client-ini-profile' import type { Settings } from './client-settings' import { assertValidSettings } from './client-settings' From 315666d6e642b9fd27f0a7d6a5abb5a08196425f Mon Sep 17 00:00:00 2001 From: Vincent Germain Date: Wed, 3 May 2023 13:44:31 +0200 Subject: [PATCH 06/18] test: withLegacyInterceptors --- .../src/scw/__tests__/client-ini-factory.ts | 84 +++++++++++++------ 1 file changed, 60 insertions(+), 24 deletions(-) diff --git a/packages/clients/src/scw/__tests__/client-ini-factory.ts b/packages/clients/src/scw/__tests__/client-ini-factory.ts index 2eb76dbce..57f470214 100644 --- a/packages/clients/src/scw/__tests__/client-ini-factory.ts +++ b/packages/clients/src/scw/__tests__/client-ini-factory.ts @@ -1,8 +1,10 @@ import { describe, expect, it } from '@jest/globals' +import type { ClientConfig } from '../client-ini-factory' import { withAdditionalInterceptors, withDefaultPageSize, withHTTPClient, + withLegacyInterceptors, withProfile, withUserAgent, withUserAgentSuffix, @@ -31,33 +33,9 @@ const DEFAULT_SETTINGS: Readonly = { defaultZone: 'fr-par-1', httpClient: fetch, interceptors: [], - requestInterceptors: [], - responseInterceptors: [], userAgent: 'scaleway-sdk-js/v1.0.0-beta', } -describe('withAdditionalInterceptors', () => { - it('appends interceptors to existing ones', () => { - const oneInterProfile = withAdditionalInterceptors([ - { - request: req => req, - }, - ])(DEFAULT_SETTINGS) - const twoInterProfile = withAdditionalInterceptors([ - { - // requestError: err => err, - response: res => res, - responseError: err => err, - }, - ])(oneInterProfile) - expect(twoInterProfile.interceptors.length).toEqual(2) - // expect(twoInterProfile.interceptors[1].requestError).toBeDefined() - expect(twoInterProfile.interceptors[1].response).toBeDefined() - expect(twoInterProfile.interceptors[1].responseError).toBeDefined() - expect(twoInterProfile.interceptors[1].request).toBeUndefined() - }) -}) - describe('withProfile', () => { it(`doesn't modify Settings object with empty Profile object`, () => { expect(withProfile(EMPTY_PROFILE)(DEFAULT_SETTINGS)).toStrictEqual( @@ -271,3 +249,61 @@ describe('withUserAgentSuffix', () => { ).toStrictEqual(JSON.stringify(expectedSettings)) }) }) + +describe('withAdditionalInterceptors', () => { + it('appends interceptors to existing ones', () => { + const oneInterProfile = withAdditionalInterceptors([ + { + request: req => req, + }, + ])(DEFAULT_SETTINGS) + const twoInterProfile = withAdditionalInterceptors([ + { + response: res => res, + responseError: err => err, + }, + ])(oneInterProfile) + expect(twoInterProfile.interceptors.length).toEqual(2) + expect(twoInterProfile.interceptors[1].response).toBeDefined() + expect(twoInterProfile.interceptors[1].responseError).toBeDefined() + expect(twoInterProfile.interceptors[1].request).toBeUndefined() + }) +}) + +describe('withLegacyInterceptors', () => { + it('changes nothing if no legacy interceptor', () => { + expect( + JSON.stringify(withLegacyInterceptors()(DEFAULT_SETTINGS)), + ).toStrictEqual(JSON.stringify(DEFAULT_SETTINGS)) + }) + + it('appends the legacy request and response interceptors', () => { + const legacyInterceptors: ClientConfig = (obj: Settings): Settings => ({ + ...obj, + requestInterceptors: [(req): Request => req, (req): Request => req], + responseInterceptors: [(res): Response => res], + }) + expect( + withLegacyInterceptors()(legacyInterceptors(DEFAULT_SETTINGS)) + .interceptors.length, + ).toBe(3) + + const legacyReqInterceptors: ClientConfig = (obj: Settings): Settings => ({ + ...obj, + requestInterceptors: [(req): Request => req, (req): Request => req], + }) + expect( + withLegacyInterceptors()(legacyReqInterceptors(DEFAULT_SETTINGS)) + .interceptors.length, + ).toBe(2) + + const legacyResInterceptors: ClientConfig = (obj: Settings): Settings => ({ + ...obj, + responseInterceptors: [(res): Response => res], + }) + expect( + withLegacyInterceptors()(legacyResInterceptors(DEFAULT_SETTINGS)) + .interceptors.length, + ).toBe(1) + }) +}) From 16ae2ef3d0b45cb1370c13f4a2e95c6839e06979 Mon Sep 17 00:00:00 2001 From: Vincent Germain Date: Wed, 3 May 2023 16:32:23 +0200 Subject: [PATCH 07/18] test: buildFetcher --- .../src/scw/fetch/__tests__/build-fetcher.ts | 21 +++++++++++++++++++ .../clients/src/scw/fetch/build-fetcher.ts | 6 ------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/clients/src/scw/fetch/__tests__/build-fetcher.ts b/packages/clients/src/scw/fetch/__tests__/build-fetcher.ts index ac74e01dc..1152bd4ea 100644 --- a/packages/clients/src/scw/fetch/__tests__/build-fetcher.ts +++ b/packages/clients/src/scw/fetch/__tests__/build-fetcher.ts @@ -111,4 +111,25 @@ describe(`buildFetcher (mock)`, () => { }), ).resolves.toMatchObject({ any_parameter: 'any-value' }) }) + + it('gets a response with response error interceptor despite the error', () => { + mockedFetch.mockRejectedValue(new TypeError('')) + + return expect( + buildFetcher( + { + ...DEFAULT_SETTINGS, + interceptors: [ + { + responseError: () => Promise.resolve(42), + }, + ], + }, + global.fetch, + )({ + method: 'GET', + path: '/will-trigger-an-error', + }), + ).resolves.toBe(42) + }) }) diff --git a/packages/clients/src/scw/fetch/build-fetcher.ts b/packages/clients/src/scw/fetch/build-fetcher.ts index e2bb320ea..0a222c973 100644 --- a/packages/clients/src/scw/fetch/build-fetcher.ts +++ b/packages/clients/src/scw/fetch/build-fetcher.ts @@ -81,12 +81,6 @@ export const buildFetcher = (settings: Settings, httpClient: typeof fetch) => { .filter(obj => obj) as ResponseInterceptor[]), logResponse(requestId), ]) - // const prepareRequestErrors = () => - // composeRequestErrorInterceptors( - // settings.interceptors - // .map(obj => obj.requestError) - // .filter(obj => obj) as RequestErrorInterceptor[] - // ) const prepareResponseErrors = () => composeResponseErrorInterceptors( settings.interceptors From 57d8f5aa1348b91b269a30b297a46e8d0a654925 Mon Sep 17 00:00:00 2001 From: Vincent Germain Date: Wed, 3 May 2023 16:33:19 +0200 Subject: [PATCH 08/18] chore: clean --- packages/clients/src/internal/interceptors/types.ts | 5 ----- packages/clients/src/internals.ts | 1 - 2 files changed, 6 deletions(-) diff --git a/packages/clients/src/internal/interceptors/types.ts b/packages/clients/src/internal/interceptors/types.ts index c00db33a2..2751ab9ef 100644 --- a/packages/clients/src/internal/interceptors/types.ts +++ b/packages/clients/src/internal/interceptors/types.ts @@ -2,10 +2,6 @@ export interface RequestInterceptor { (request: Readonly): Request | Promise } -// export interface RequestErrorInterceptor { -// (error: unknown): unknown | Promise -// } - export interface ResponseInterceptor { (response: Readonly): Response | Promise } @@ -16,7 +12,6 @@ export interface ResponseErrorInterceptor { export type NetworkInterceptors = { request?: RequestInterceptor - // requestError?: RequestErrorInterceptor response?: ResponseInterceptor responseError?: ResponseErrorInterceptor } diff --git a/packages/clients/src/internals.ts b/packages/clients/src/internals.ts index 6d3d333c9..f9df13387 100644 --- a/packages/clients/src/internals.ts +++ b/packages/clients/src/internals.ts @@ -4,7 +4,6 @@ export type { NetworkInterceptors, RequestInterceptor, ResponseInterceptor, - // RequestErrorInterceptor, ResponseErrorInterceptor, } from './internal/interceptors/types' export { API } from './scw/api' From 7a7566acdf0bc8bf24e05ee9b754520bc7b418b1 Mon Sep 17 00:00:00 2001 From: Vincent Germain Date: Wed, 3 May 2023 17:03:42 +0200 Subject: [PATCH 09/18] refactor: simplify --- packages/clients/src/scw/fetch/build-fetcher.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/clients/src/scw/fetch/build-fetcher.ts b/packages/clients/src/scw/fetch/build-fetcher.ts index 0a222c973..95f8ba06e 100644 --- a/packages/clients/src/scw/fetch/build-fetcher.ts +++ b/packages/clients/src/scw/fetch/build-fetcher.ts @@ -101,13 +101,11 @@ export const buildFetcher = (settings: Settings, httpClient: typeof fetch) => { ) const resErrorInterceptors = prepareResponseErrors() - const output = Promise.resolve(finalRequest) + return Promise.resolve(finalRequest) .then(reqInterceptors) .then(httpClient) .then(prepareResponse(requestId)) .then(resInterceptors) .catch(obj => resErrorInterceptors(finalRequest, obj) as T) - - return output } } From 5518c4fc8c048627e427352312e78da66299e2b6 Mon Sep 17 00:00:00 2001 From: Vincent Germain Date: Wed, 3 May 2023 17:10:05 +0200 Subject: [PATCH 10/18] chore: export types in main index --- packages/clients/src/index.ts | 6 ++++++ packages/clients/src/internals.ts | 6 ------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/clients/src/index.ts b/packages/clients/src/index.ts index 35716bdd7..4d31a12d9 100644 --- a/packages/clients/src/index.ts +++ b/packages/clients/src/index.ts @@ -2,6 +2,12 @@ export type { WaitForOptions, WaitForStopCondition, } from './internal/async/interval-retrier' +export type { + NetworkInterceptors, + RequestInterceptor, + ResponseInterceptor, + ResponseErrorInterceptor, +} from './internal/interceptors/types' export { enableConsoleLogger, setLogger } from './internal/logger' export type { Logger } from './internal/logger/logger' export { createClient, createAdvancedClient } from './scw/client' diff --git a/packages/clients/src/internals.ts b/packages/clients/src/internals.ts index f9df13387..d912b7cf3 100644 --- a/packages/clients/src/internals.ts +++ b/packages/clients/src/internals.ts @@ -1,11 +1,5 @@ export { isJSONObject } from './helpers/json' export { waitForResource } from './internal/async/interval-retrier' -export type { - NetworkInterceptors, - RequestInterceptor, - ResponseInterceptor, - ResponseErrorInterceptor, -} from './internal/interceptors/types' export { API } from './scw/api' export { authenticateWithSessionToken } from './scw/auth' export type { DefaultValues } from './scw/client-settings' From 537b8c9453a5a02d3344b8f2073b7ddc199b612d Mon Sep 17 00:00:00 2001 From: Vincent Germain Date: Wed, 3 May 2023 17:27:27 +0200 Subject: [PATCH 11/18] fix: type import --- packages/clients/src/scw/client-ini-factory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clients/src/scw/client-ini-factory.ts b/packages/clients/src/scw/client-ini-factory.ts index 84feeb515..5ba4228ad 100644 --- a/packages/clients/src/scw/client-ini-factory.ts +++ b/packages/clients/src/scw/client-ini-factory.ts @@ -1,4 +1,4 @@ -import type { NetworkInterceptors } from '../internals' +import type { NetworkInterceptors } from '../index' import { authenticateWithSecrets } from './auth' import { hasAuthenticationSecrets } from './client-ini-profile' import type { Profile } from './client-ini-profile' From 4c4292de5231d72cdb7017030613ad38560be207 Mon Sep 17 00:00:00 2001 From: Vincent Germain Date: Wed, 3 May 2023 17:31:34 +0200 Subject: [PATCH 12/18] fix: imports --- packages/clients/src/scw/fetch/build-fetcher.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/clients/src/scw/fetch/build-fetcher.ts b/packages/clients/src/scw/fetch/build-fetcher.ts index 95f8ba06e..bca6ff100 100644 --- a/packages/clients/src/scw/fetch/build-fetcher.ts +++ b/packages/clients/src/scw/fetch/build-fetcher.ts @@ -1,13 +1,13 @@ import { isBrowser } from '../../helpers/is-browser' -import { - composeInterceptors, - composeResponseErrorInterceptors, -} from '../../internal/interceptors/composer' import type { RequestInterceptor, ResponseErrorInterceptor, ResponseInterceptor, -} from '../../internals' +} from '../../index' +import { + composeInterceptors, + composeResponseErrorInterceptors, +} from '../../internal/interceptors/composer' import { obfuscateAuthHeadersEntry } from '../auth' import type { Settings } from '../client-settings' import { From 9d27728909b1691249b5e8b45b2721913ca18be4 Mon Sep 17 00:00:00 2001 From: Vincent Germain Date: Fri, 5 May 2023 14:31:14 +0200 Subject: [PATCH 13/18] feat: use object for interceptor parameters --- .../interceptors/__tests__/composer.ts | 52 +++++++------------ .../interceptors/__tests__/helpers.ts | 27 ++++++---- .../src/internal/interceptors/composer.ts | 44 +++++++++++----- .../src/internal/interceptors/helpers.ts | 2 +- .../src/internal/interceptors/types.ts | 8 +-- packages/clients/src/scw/__tests__/auth.ts | 10 ++-- .../src/scw/__tests__/client-ini-factory.ts | 20 ++++--- .../scw/fetch/__tests__/http-interceptors.ts | 20 +++---- .../clients/src/scw/fetch/build-fetcher.ts | 12 +++-- .../src/scw/fetch/http-interceptors.ts | 11 ++-- 10 files changed, 115 insertions(+), 91 deletions(-) diff --git a/packages/clients/src/internal/interceptors/__tests__/composer.ts b/packages/clients/src/internal/interceptors/__tests__/composer.ts index e626a18a8..4d341d913 100644 --- a/packages/clients/src/internal/interceptors/__tests__/composer.ts +++ b/packages/clients/src/internal/interceptors/__tests__/composer.ts @@ -1,41 +1,25 @@ import { describe, expect, it } from '@jest/globals' -import type { Interceptor } from '../composer' import { - composeInterceptors, + composeRequestInterceptors, composeResponseErrorInterceptors, } from '../composer' -type StringArrayInterceptor = Interceptor +describe('composeRequestInterceptors', () => { + it('modifies the request header', async () => { + const interceptor = composeRequestInterceptors([ + ({ request }): Request => { + const clone = request.clone() + clone.headers.set('new-header', '42') -const addStrInterceptor = - (str: string): StringArrayInterceptor => - (list: Readonly): string[] => - list.concat(str) - -const removeFirstStrInterceptor = - (): StringArrayInterceptor => - (list: Readonly): string[] => - list.slice(1) - -describe('composeInterceptors', () => { - it('calls one interceptor', async () => { - const interceptor = composeInterceptors([addStrInterceptor('Fusion')]) - expect(await interceptor(['Tree', 'Animal'])).toStrictEqual([ - 'Tree', - 'Animal', - 'Fusion', + return clone + }, ]) - }) - it('compose two interceptors', async () => { - const interceptor = composeInterceptors([ - addStrInterceptor('Fusion'), - removeFirstStrInterceptor(), - ]) - expect(await interceptor(['Tree', 'Animal'])).toStrictEqual([ - 'Animal', - 'Fusion', - ]) + return expect( + interceptor(new Request('https://api.scaleway.com')).then(obj => + obj.headers.get('new-header'), + ), + ).resolves.toBe('42') }) }) @@ -52,12 +36,12 @@ describe('composeResponseErrorInterceptors', () => { } const interceptors = composeResponseErrorInterceptors([ - (_: Request, error: unknown): Promise => { + ({ error }): Promise => { throw error instanceof NumberError ? new NumberError(error.counter + 1) : error }, - (_: Request, error: unknown): Promise => { + ({ error }): Promise => { throw error instanceof NumberError ? new NumberError(error.counter + 2) : error @@ -69,11 +53,11 @@ describe('composeResponseErrorInterceptors', () => { it('stops at the second interceptor (amongst three) if it resolves', () => { const interceptors = composeResponseErrorInterceptors([ - (_: Request, error: unknown): Promise => { + ({ error }): Promise => { throw error }, (): Promise => Promise.resolve(42), - (_: Request, error: unknown): Promise => { + ({ error }): Promise => { throw error }, ])(new Request('https://api.scaleway.com'), new TypeError('')) diff --git a/packages/clients/src/internal/interceptors/__tests__/helpers.ts b/packages/clients/src/internal/interceptors/__tests__/helpers.ts index 94800c81f..e623016e5 100644 --- a/packages/clients/src/internal/interceptors/__tests__/helpers.ts +++ b/packages/clients/src/internal/interceptors/__tests__/helpers.ts @@ -3,30 +3,39 @@ import { addAsyncHeaderInterceptor, addHeaderInterceptor } from '../helpers' describe('addHeaderInterceptor', () => { it('insertsnothing if value is undefined', async () => { - const req = new Request('https://api.scaleway.com/my/path') - const updatedReq = await addHeaderInterceptor('my-key', undefined)(req) + const request = new Request('https://api.scaleway.com/my/path') + const updatedReq = await addHeaderInterceptor( + 'my-key', + undefined, + )({ request }) expect(updatedReq.headers.has('my-key')).toBe(false) }) it('inserts 1 key/value in the request', async () => { - const req = new Request('https://api.scaleway.com/my/path') - const updatedReq = await addHeaderInterceptor('my-key', 'my-value')(req) + const request = new Request('https://api.scaleway.com/my/path') + const updatedReq = await addHeaderInterceptor( + 'my-key', + 'my-value', + )({ request }) expect(updatedReq.headers.get('my-key')).toBe('my-value') }) it(`desn't modify the input request`, async () => { - const req = new Request('https://api.scaleway.com/my/path') - const updatedReq = await addHeaderInterceptor('my-key', 'my-value')(req) - expect(req).not.toStrictEqual(updatedReq) + const request = new Request('https://api.scaleway.com/my/path') + const updatedReq = await addHeaderInterceptor( + 'my-key', + 'my-value', + )({ request }) + expect(request).not.toStrictEqual(updatedReq) }) }) describe('addAsyncHeaderInterceptor', () => { it('inserts 1 key/value in the request', async () => { - const req = new Request('https://api.scaleway.com/my/path') + const request = new Request('https://api.scaleway.com/my/path') const updatedReq = await addAsyncHeaderInterceptor('my-key', async () => Promise.resolve('my-value'), - )(req) + )({ request }) expect(updatedReq.headers.get('my-key')).toBe('my-value') }) }) diff --git a/packages/clients/src/internal/interceptors/composer.ts b/packages/clients/src/internal/interceptors/composer.ts index 8c636e255..c62f80359 100644 --- a/packages/clients/src/internal/interceptors/composer.ts +++ b/packages/clients/src/internal/interceptors/composer.ts @@ -1,25 +1,41 @@ -import type { ResponseErrorInterceptor } from './types' +import type { + RequestInterceptor, + ResponseErrorInterceptor, + ResponseInterceptor, +} from './types' -export interface Interceptor { - (instance: Readonly): T | Promise -} +/** + * Composes request interceptors. + * + * @param interceptors - A list of request interceptors + * @returns An async composed interceptor + * + * @internal + */ +export const composeRequestInterceptors = + (interceptors: RequestInterceptor[]) => + async (request: Request): Promise => + interceptors.reduce( + async (asyncResult, interceptor): Promise => + interceptor({ request: await asyncResult }), + Promise.resolve(request), + ) /** - * Composes interceptors. + * Composes response interceptors. * - * @param interceptors - A list of interceptors (that modify an object instance) + * @param interceptors - A list of response interceptors * @returns An async composed interceptor * * @internal */ -export const composeInterceptors = - (interceptors: Interceptor[]) => - async (instance: T): Promise => +export const composeResponseInterceptors = + (interceptors: ResponseInterceptor[]) => + async (response: Response): Promise => interceptors.reduce( - async (asyncResult, interceptor) => interceptor(await asyncResult), - new Promise(resolve => { - resolve(instance) - }), + async (asyncResult, interceptor): Promise => + interceptor({ response: await asyncResult }), + Promise.resolve(response), ) /** @@ -33,7 +49,7 @@ export const composeResponseErrorInterceptors = let prevError = error for (const interceptor of interceptors) { try { - const res = await interceptor(request, prevError) + const res = await interceptor({ request, error: prevError }) return res } catch (err) { diff --git a/packages/clients/src/internal/interceptors/helpers.ts b/packages/clients/src/internal/interceptors/helpers.ts index f579a974c..bff3af14b 100644 --- a/packages/clients/src/internal/interceptors/helpers.ts +++ b/packages/clients/src/internal/interceptors/helpers.ts @@ -11,7 +11,7 @@ import type { RequestInterceptor } from './types' */ export const addHeaderInterceptor = (key: string, value?: string): RequestInterceptor => - request => { + ({ request }) => { const clone = request.clone() if (value !== undefined) { clone.headers.append(key, value) diff --git a/packages/clients/src/internal/interceptors/types.ts b/packages/clients/src/internal/interceptors/types.ts index 2751ab9ef..eb19fb454 100644 --- a/packages/clients/src/internal/interceptors/types.ts +++ b/packages/clients/src/internal/interceptors/types.ts @@ -1,13 +1,15 @@ export interface RequestInterceptor { - (request: Readonly): Request | Promise + ({ request }: { request: Readonly }): Request | Promise } export interface ResponseInterceptor { - (response: Readonly): Response | Promise + ({ response }: { response: Readonly }): Response | Promise } export interface ResponseErrorInterceptor { - (request: Request, error: unknown): unknown | Promise + ({ request, error }: { request: Request; error: unknown }): + | unknown + | Promise } export type NetworkInterceptors = { diff --git a/packages/clients/src/scw/__tests__/auth.ts b/packages/clients/src/scw/__tests__/auth.ts index d7e918669..71b3a7236 100644 --- a/packages/clients/src/scw/__tests__/auth.ts +++ b/packages/clients/src/scw/__tests__/auth.ts @@ -67,7 +67,7 @@ describe('authenticateWithSessionToken', () => { const sourceReq = new Request('https://api.scaleway.com/my/path') const updatedReq = await authenticateWithSessionToken( (): Promise => Promise.resolve(dummyToken), - )(sourceReq) + )({ request: sourceReq }) const expectedReq = sourceReq.clone() expectedReq.headers.append('x-session-token', 'dummy') expect(updatedReq).toStrictEqual(expectedReq) @@ -82,7 +82,9 @@ describe('authenticateWithSecrets', () => { accessKey: 'SCW01234567890123456', secretKey: 'e4b83996-4c60-449a-98d2-38f5de7b4e6b', } - const updatedReq = await authenticateWithSecrets(validSecrets)(sourceReq) + const updatedReq = await authenticateWithSecrets(validSecrets)({ + request: sourceReq, + }) const expectedReq = sourceReq.clone() expectedReq.headers.append('x-auth-token', validSecrets.secretKey) expect(updatedReq).toStrictEqual(expectedReq) @@ -93,6 +95,8 @@ describe('authenticateWithSecrets', () => { accessKey: '', secretKey: '', } - expect(() => authenticateWithSecrets(invalidSecrets)(sourceReq)).toThrow() + expect(() => + authenticateWithSecrets(invalidSecrets)({ request: sourceReq }), + ).toThrow() }) }) diff --git a/packages/clients/src/scw/__tests__/client-ini-factory.ts b/packages/clients/src/scw/__tests__/client-ini-factory.ts index 57f470214..77068f89f 100644 --- a/packages/clients/src/scw/__tests__/client-ini-factory.ts +++ b/packages/clients/src/scw/__tests__/client-ini-factory.ts @@ -176,7 +176,7 @@ describe('withProfile', () => { })(DEFAULT_SETTINGS).interceptors[0].request expect(reqInterceptor).toBeDefined() if (reqInterceptor) { - const { headers } = await reqInterceptor(request) + const { headers } = await reqInterceptor({ request }) expect(headers.get('x-auth-token')).toStrictEqual( FILLED_PROFILE.secretKey, ) @@ -254,12 +254,12 @@ describe('withAdditionalInterceptors', () => { it('appends interceptors to existing ones', () => { const oneInterProfile = withAdditionalInterceptors([ { - request: req => req, + request: ({ request }) => request, }, ])(DEFAULT_SETTINGS) const twoInterProfile = withAdditionalInterceptors([ { - response: res => res, + response: ({ response }) => response, responseError: err => err, }, ])(oneInterProfile) @@ -280,8 +280,11 @@ describe('withLegacyInterceptors', () => { it('appends the legacy request and response interceptors', () => { const legacyInterceptors: ClientConfig = (obj: Settings): Settings => ({ ...obj, - requestInterceptors: [(req): Request => req, (req): Request => req], - responseInterceptors: [(res): Response => res], + requestInterceptors: [ + ({ request }): Request => request, + ({ request }): Request => request, + ], + responseInterceptors: [({ response }): Response => response], }) expect( withLegacyInterceptors()(legacyInterceptors(DEFAULT_SETTINGS)) @@ -290,7 +293,10 @@ describe('withLegacyInterceptors', () => { const legacyReqInterceptors: ClientConfig = (obj: Settings): Settings => ({ ...obj, - requestInterceptors: [(req): Request => req, (req): Request => req], + requestInterceptors: [ + ({ request }): Request => request, + ({ request }): Request => request, + ], }) expect( withLegacyInterceptors()(legacyReqInterceptors(DEFAULT_SETTINGS)) @@ -299,7 +305,7 @@ describe('withLegacyInterceptors', () => { const legacyResInterceptors: ClientConfig = (obj: Settings): Settings => ({ ...obj, - responseInterceptors: [(res): Response => res], + responseInterceptors: [({ response }): Response => response], }) expect( withLegacyInterceptors()(legacyResInterceptors(DEFAULT_SETTINGS)) diff --git a/packages/clients/src/scw/fetch/__tests__/http-interceptors.ts b/packages/clients/src/scw/fetch/__tests__/http-interceptors.ts index 79c6e9518..cb24914c6 100644 --- a/packages/clients/src/scw/fetch/__tests__/http-interceptors.ts +++ b/packages/clients/src/scw/fetch/__tests__/http-interceptors.ts @@ -43,13 +43,13 @@ describe(`logRequest`, () => { it(`logs nothing if the level isn't high enough`, async () => { enableDummyLogger('warn') - await logRequest('1')(request) + await logRequest('1')({ request }) expect(latestMessage).toBe('') }) it(`logs something if the level is high enough`, async () => { enableDummyLogger('debug') - await logRequest('1')(request) + await logRequest('1')({ request }) expect( latestMessage.startsWith('--------------- Scaleway SDK REQUEST 1'), ).toBe(true) @@ -61,13 +61,13 @@ describe(`logResponse`, () => { it(`logs nothing if the level isn't high enough`, async () => { enableDummyLogger('warn') - await logResponse('1')(response) + await logResponse('1')({ response }) expect(latestMessage).toBe('') }) it(`logs something if the level is high enough`, async () => { enableDummyLogger('debug') - await logResponse('1')(response) + await logResponse('1')({ response }) expect( latestMessage.startsWith('--------------- Scaleway SDK RESPONSE 1'), ).toBe(true) @@ -81,22 +81,22 @@ describe('obfuscateInterceptor', () => { [name, `${preprendValue}${value}`] it('changes the request headers', async () => { - const obfRequest = await obfuscateInterceptor(prependInterceptor('obj-'))( - new Request('https://api.scaleway.com', { + const obfRequest = await obfuscateInterceptor(prependInterceptor('obj-'))({ + request: new Request('https://api.scaleway.com', { headers: { 'X-Random-Key': 'value' }, }), - ) + }) expect(obfRequest).toBeInstanceOf(Request) expect(obfRequest.headers.get('X-Random-Key')).toBe('obj-value') }) it('clones the request without altering the headers', async () => { - const obfRequest = await obfuscateInterceptor(prependInterceptor('obj-'))( - new Request('https://api.scaleway.com', { + const obfRequest = await obfuscateInterceptor(prependInterceptor('obj-'))({ + request: new Request('https://api.scaleway.com', { headers: { 'X-Random-Key': 'value' }, }), - ) + }) expect(obfRequest.clone().headers.get('X-Random-Key')).toBe('obj-value') }) diff --git a/packages/clients/src/scw/fetch/build-fetcher.ts b/packages/clients/src/scw/fetch/build-fetcher.ts index bca6ff100..47349a501 100644 --- a/packages/clients/src/scw/fetch/build-fetcher.ts +++ b/packages/clients/src/scw/fetch/build-fetcher.ts @@ -5,8 +5,9 @@ import type { ResponseInterceptor, } from '../../index' import { - composeInterceptors, + composeRequestInterceptors, composeResponseErrorInterceptors, + composeResponseInterceptors, } from '../../internal/interceptors/composer' import { obfuscateAuthHeadersEntry } from '../auth' import type { Settings } from '../client-settings' @@ -68,14 +69,14 @@ export type Fetcher = ( export const buildFetcher = (settings: Settings, httpClient: typeof fetch) => { let requestNumber = 0 const prepareRequest = (requestId: string) => - composeInterceptors([ + composeRequestInterceptors([ ...(settings.interceptors .map(obj => obj.request) .filter(obj => obj) as RequestInterceptor[]), logRequest(requestId, obfuscateInterceptor(obfuscateAuthHeadersEntry)), ]) const prepareResponse = (requestId: string) => - composeInterceptors([ + composeResponseInterceptors([ ...(settings.interceptors .map(obj => obj.response) .filter(obj => obj) as ResponseInterceptor[]), @@ -95,7 +96,8 @@ export const buildFetcher = (settings: Settings, httpClient: typeof fetch) => { const requestId = `${(requestNumber += 1)}` const finalRequest = buildRequest(request, settings) const reqInterceptors = prepareRequest(requestId) - const resInterceptors = responseParser( + const resInterceptors = prepareResponse(requestId) + const resUnmarshaller = responseParser( unwrapper, request.responseType ?? 'json', ) @@ -104,8 +106,8 @@ export const buildFetcher = (settings: Settings, httpClient: typeof fetch) => { return Promise.resolve(finalRequest) .then(reqInterceptors) .then(httpClient) - .then(prepareResponse(requestId)) .then(resInterceptors) + .then(resUnmarshaller) .catch(obj => resErrorInterceptors(finalRequest, obj) as T) } } diff --git a/packages/clients/src/scw/fetch/http-interceptors.ts b/packages/clients/src/scw/fetch/http-interceptors.ts index 6a1412883..b1a03abc4 100644 --- a/packages/clients/src/scw/fetch/http-interceptors.ts +++ b/packages/clients/src/scw/fetch/http-interceptors.ts @@ -44,10 +44,11 @@ class ObfuscatedRequest extends Request { */ export const obfuscateInterceptor = (obfuscate: HeaderEntryMapper): RequestInterceptor => - request => + ({ request }) => new ObfuscatedRequest(request, obfuscate) -const identity = (instance: T) => instance +const identity: RequestInterceptor = ({ request }: { request: Request }) => + request /** * Creates an interceptor to log the requests. @@ -63,11 +64,11 @@ export const logRequest = identifier: string, obfuscate: RequestInterceptor = identity, ): RequestInterceptor => - async request => { + async ({ request }) => { if (shouldLog(LevelResolver[getLogger().logLevel], 'debug')) { getLogger().debug( `--------------- Scaleway SDK REQUEST ${identifier} --------------- -${await dumpRequest(await obfuscate(request))} +${await dumpRequest(await obfuscate({ request }))} ---------------------------------------------------------`, ) } @@ -85,7 +86,7 @@ ${await dumpRequest(await obfuscate(request))} */ export const logResponse = (identifier: string): ResponseInterceptor => - async response => { + async ({ response }) => { if (shouldLog(LevelResolver[getLogger().logLevel], 'debug')) { getLogger().debug( `--------------- Scaleway SDK RESPONSE ${identifier} --------------- From 8352ea1fcff075cb6f542e8626a5235f3d3615fe Mon Sep 17 00:00:00 2001 From: Vincent Germain Date: Fri, 5 May 2023 17:39:48 +0200 Subject: [PATCH 14/18] docs: add some examples --- .../src/internal/interceptors/types.ts | 68 +++++++++++++++++++ .../clients/src/scw/client-ini-factory.ts | 32 ++++++++- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/packages/clients/src/internal/interceptors/types.ts b/packages/clients/src/internal/interceptors/types.ts index eb19fb454..98465b398 100644 --- a/packages/clients/src/internal/interceptors/types.ts +++ b/packages/clients/src/internal/interceptors/types.ts @@ -1,17 +1,85 @@ +/** + * Defines the interceptor for a `Request`. + * This allows you to intercept requests before starting. + * + * @example + * Adds a custom header to a request: + * ``` + * const addCustomHeaderInterceptor + * ({ key, value }: { key: string; value: string }): RequestInterceptor => + * ({ request }) => { + * const clone = request.clone() + * clone.headers.set(key, value) + * + * return clone + * } + * ``` + * + * @public + */ export interface RequestInterceptor { ({ request }: { request: Readonly }): Request | Promise } +/** + * Defines the interceptor for a `Response`. + * This allows you to intercept responses before unmarshalling. + * + * @example + * Adds a delay before sending the response: + * ``` + * const addDelayInterceptor: ResponseInterceptor = ({ response }) => + * new Promise(resolve => { + * setTimeout(() => resolve(response), 1000) + * }) + * ``` + * + * @public + */ export interface ResponseInterceptor { ({ response }: { response: Readonly }): Response | Promise } +/** + * Defines the interceptor for a `Response` error. + * This allows you to intercept a response error before exiting the whole process. + * You can either rethrow an error, and resolve with a different response. + * + * @remarks + * You must return either: + * 1. An error (`throw error` or `Promise.reject(error)`) + * 2. Data (directly, or via a Promise) + * + * @example + * Reports error to tracking service: + * ``` + * const reportErrorToTrackingService: ResponseErrorInterceptor = async ({ + * request, + * error, + * }: { + * request: Request + * error: unknown + * }) => { + * await sendErrorToErrorService(request, error) + * throw error + * } + * ``` + * + * @public + */ export interface ResponseErrorInterceptor { ({ request, error }: { request: Request; error: unknown }): | unknown | Promise } +/** + * Defines the network interceptors. + * Please check the documentation of {@link RequestInterceptor}, + * {@link ResponseInterceptor} and {@link ResponseErrorInterceptor} for examples. + * + * @public + */ export type NetworkInterceptors = { request?: RequestInterceptor response?: ResponseInterceptor diff --git a/packages/clients/src/scw/client-ini-factory.ts b/packages/clients/src/scw/client-ini-factory.ts index 5ba4228ad..1a4114fe0 100644 --- a/packages/clients/src/scw/client-ini-factory.ts +++ b/packages/clients/src/scw/client-ini-factory.ts @@ -116,13 +116,43 @@ export const withUserAgentSuffix = /** * Instantiates the SDK with additional interceptors. * - * @param interceptors - The additional interceptors + * @param interceptors - The additional {@link NetworkInterceptors} interceptors * @returns A factory {@link ClientConfig} * * @remarks * It doesn't override the existing interceptors, but instead push more to the list. * This method should be used in conjunction with the initializer `createAdvancedClient`. * + * @example + * ``` + * withAdditionalInterceptors([ + * { + * request: ({ request }) => { + * console.log(`Do something with ${JSON.stringify(request)}`) + * return request + * }, + * response: ({ response }) => { + * console.log(`Do something with ${JSON.stringify(response)}`) + * return response + * }, + * responseError: async ({ + * request, + * error, + * }: { + * request: Request + * error: unknown + * }) => { + * console.log( + * `Do something with ${JSON.stringify(request)} and ${JSON.stringify( + * error, + * )}`, + * ) + * throw error // or return Promise.resolve(someData) + * }, + * }, + * ]) + * ``` + * * @public */ export const withAdditionalInterceptors = From 3318bc33172bc4fefbe45524c419693ba5b7826b Mon Sep 17 00:00:00 2001 From: Vincent Germain Date: Tue, 9 May 2023 15:26:12 +0200 Subject: [PATCH 15/18] chore: deprecate authenticateWithSessionToken --- packages/clients/src/internals.ts | 3 +++ packages/clients/src/scw/auth.ts | 2 ++ 2 files changed, 5 insertions(+) diff --git a/packages/clients/src/internals.ts b/packages/clients/src/internals.ts index d912b7cf3..66c8c75f5 100644 --- a/packages/clients/src/internals.ts +++ b/packages/clients/src/internals.ts @@ -1,7 +1,10 @@ export { isJSONObject } from './helpers/json' export { waitForResource } from './internal/async/interval-retrier' +export { addAsyncHeaderInterceptor } from './internal/interceptors/helpers' export { API } from './scw/api' +/* eslint-disable deprecation/deprecation */ export { authenticateWithSessionToken } from './scw/auth' +/* eslint-enable deprecation/deprecation */ export type { DefaultValues } from './scw/client-settings' export { marshalScwFile, diff --git a/packages/clients/src/scw/auth.ts b/packages/clients/src/scw/auth.ts index 911a0f8fa..da3824ee8 100644 --- a/packages/clients/src/scw/auth.ts +++ b/packages/clients/src/scw/auth.ts @@ -19,6 +19,8 @@ interface TokenAccessor { * @param getToken - The token accessor * @returns The request interceptor * + * @deprecated Please use addAsyncHeaderInterceptor instead. + * * @internal */ export const authenticateWithSessionToken = ( From 9bccccbac7b3564999428dd16579501570790acc Mon Sep 17 00:00:00 2001 From: Vincent Germain Date: Tue, 9 May 2023 15:57:43 +0200 Subject: [PATCH 16/18] feat: send final request to the response error interceptors --- .../clients/src/scw/fetch/build-fetcher.ts | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/clients/src/scw/fetch/build-fetcher.ts b/packages/clients/src/scw/fetch/build-fetcher.ts index 47349a501..e2db67698 100644 --- a/packages/clients/src/scw/fetch/build-fetcher.ts +++ b/packages/clients/src/scw/fetch/build-fetcher.ts @@ -94,20 +94,25 @@ export const buildFetcher = (settings: Settings, httpClient: typeof fetch) => { unwrapper: ResponseUnmarshaller = asIs, ): Promise => { const requestId = `${(requestNumber += 1)}` - const finalRequest = buildRequest(request, settings) const reqInterceptors = prepareRequest(requestId) - const resInterceptors = prepareResponse(requestId) - const resUnmarshaller = responseParser( - unwrapper, - request.responseType ?? 'json', - ) - const resErrorInterceptors = prepareResponseErrors() + const finalRequest = await reqInterceptors(buildRequest(request, settings)) + + try { + const response = await httpClient(finalRequest) + const resInterceptors = prepareResponse(requestId) + const finalResponse = await resInterceptors(response) + const resUnmarshaller = responseParser( + unwrapper, + request.responseType ?? 'json', + ) + const unmarshaledResponse = await resUnmarshaller(finalResponse) + + return unmarshaledResponse + } catch (err) { + const resErrorInterceptors = prepareResponseErrors() + const handledError = (await resErrorInterceptors(finalRequest, err)) as T - return Promise.resolve(finalRequest) - .then(reqInterceptors) - .then(httpClient) - .then(resInterceptors) - .then(resUnmarshaller) - .catch(obj => resErrorInterceptors(finalRequest, obj) as T) + return handledError + } } } From 38e5300337cd58aca0bbc437c3a7ec40bfc54a2c Mon Sep 17 00:00:00 2001 From: Vincent Germain Date: Tue, 9 May 2023 15:59:01 +0200 Subject: [PATCH 17/18] test: cover more cases --- .../src/scw/fetch/__tests__/build-fetcher.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/packages/clients/src/scw/fetch/__tests__/build-fetcher.ts b/packages/clients/src/scw/fetch/__tests__/build-fetcher.ts index 1152bd4ea..e4b42c3f8 100644 --- a/packages/clients/src/scw/fetch/__tests__/build-fetcher.ts +++ b/packages/clients/src/scw/fetch/__tests__/build-fetcher.ts @@ -1,5 +1,6 @@ import { afterAll, describe, expect, it, jest } from '@jest/globals' import { isBrowser } from '../../../helpers/is-browser' +import { addHeaderInterceptor } from '../../../internal/interceptors/helpers' import type { Settings } from '../../client-settings' import { buildFetcher, buildRequest } from '../build-fetcher' import type { ScwRequest } from '../types' @@ -97,6 +98,31 @@ describe(`buildFetcher (mock)`, () => { ).resolves.toStrictEqual('dummy-output') }) + it('gets modified response', () => { + mockedFetch.mockResolvedValue( + new Response(JSON.stringify({}), { + headers: { 'Content-Type': 'application/json' }, + }), + ) + + return expect( + buildFetcher( + { + ...DEFAULT_SETTINGS, + interceptors: [ + { + response: () => new Response(JSON.stringify(42)), + }, + ], + }, + global.fetch, + )({ + method: 'POST', + path: '/undefined', + }), + ).resolves.toStrictEqual('42') + }) + it(`gets a response without error for a simple request without unmarshaller`, async () => { mockedFetch.mockResolvedValue( new Response(JSON.stringify({ any_parameter: 'any-value' }), { @@ -132,4 +158,27 @@ describe(`buildFetcher (mock)`, () => { }), ).resolves.toBe(42) }) + + it('gets modified request in response error', () => { + mockedFetch.mockRejectedValue(new TypeError('')) + + return expect( + buildFetcher( + { + ...DEFAULT_SETTINGS, + interceptors: [ + { + request: addHeaderInterceptor('random-header', '42'), + responseError: ({ request }) => + request.headers.get('random-header'), + }, + ], + }, + global.fetch, + )({ + method: 'GET', + path: '/will-trigger-an-error', + }), + ).resolves.toBe('42') + }) }) From da0058bf5d1c5e6748705dfe9ccc2245d9094b71 Mon Sep 17 00:00:00 2001 From: Vincent Germain Date: Wed, 10 May 2023 11:42:06 +0200 Subject: [PATCH 18/18] fix: composeResponseErrorInterceptors throws the last processed error --- .../src/internal/interceptors/__tests__/composer.ts | 13 +++++++++++++ .../clients/src/internal/interceptors/composer.ts | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/clients/src/internal/interceptors/__tests__/composer.ts b/packages/clients/src/internal/interceptors/__tests__/composer.ts index 4d341d913..f0674a290 100644 --- a/packages/clients/src/internal/interceptors/__tests__/composer.ts +++ b/packages/clients/src/internal/interceptors/__tests__/composer.ts @@ -64,4 +64,17 @@ describe('composeResponseErrorInterceptors', () => { return expect(interceptors).resolves.toBe(42) }) + + it('throws the last processed error', () => { + const interceptors = composeResponseErrorInterceptors([ + ({ error }): Promise => { + throw error + }, + (): Promise => { + throw new TypeError('second error') + }, + ])(new Request('https://api.scaleway.com'), new TypeError('first error')) + + return expect(interceptors).rejects.toThrow(new TypeError('second error')) + }) }) diff --git a/packages/clients/src/internal/interceptors/composer.ts b/packages/clients/src/internal/interceptors/composer.ts index c62f80359..94373b5d7 100644 --- a/packages/clients/src/internal/interceptors/composer.ts +++ b/packages/clients/src/internal/interceptors/composer.ts @@ -57,5 +57,5 @@ export const composeResponseErrorInterceptors = } } - throw error + throw prevError }