Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/clients/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@ 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'
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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { describe, expect, it } from '@jest/globals'
import {
composeRequestInterceptors,
composeResponseErrorInterceptors,
} from '../composer'

describe('composeRequestInterceptors', () => {
it('modifies the request header', async () => {
const interceptor = composeRequestInterceptors([
({ request }): Request => {
const clone = request.clone()
clone.headers.set('new-header', '42')

return clone
},
])

return expect(
interceptor(new Request('https://api.scaleway.com')).then(obj =>
obj.headers.get('new-header'),
),
).resolves.toBe('42')
})
})

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([
({ error }): Promise<unknown> => {
throw error instanceof NumberError
? new NumberError(error.counter + 1)
: error
},
({ error }): Promise<unknown> => {
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([
({ error }): Promise<unknown> => {
throw error
},
(): Promise<unknown> => Promise.resolve(42),
({ error }): Promise<unknown> => {
throw error
},
])(new Request('https://api.scaleway.com'), new TypeError(''))

return expect(interceptors).resolves.toBe(42)
})

it('throws the last processed error', () => {
const interceptors = composeResponseErrorInterceptors([
({ error }): Promise<unknown> => {
throw error
},
(): Promise<unknown> => {
throw new TypeError('second error')
},
])(new Request('https://api.scaleway.com'), new TypeError('first error'))

return expect(interceptors).rejects.toThrow(new TypeError('second error'))
})
})
Original file line number Diff line number Diff line change
@@ -1,32 +1,41 @@
import { describe, expect, it } from '@jest/globals'
import { addAsyncHeaderInterceptor, addHeaderInterceptor } from '../request'
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')
})
})

This file was deleted.

61 changes: 61 additions & 0 deletions packages/clients/src/internal/interceptors/composer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type {
RequestInterceptor,
ResponseErrorInterceptor,
ResponseInterceptor,
} from './types'

/**
* 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<Request> =>
interceptors.reduce(
async (asyncResult, interceptor): Promise<Request> =>
interceptor({ request: await asyncResult }),
Promise.resolve(request),
)

/**
* Composes response interceptors.
*
* @param interceptors - A list of response interceptors
* @returns An async composed interceptor
*
* @internal
*/
export const composeResponseInterceptors =
(interceptors: ResponseInterceptor[]) =>
async (response: Response): Promise<Response> =>
interceptors.reduce(
async (asyncResult, interceptor): Promise<Response> =>
interceptor({ response: await asyncResult }),
Promise.resolve(response),
)

/**
* Compose response error interceptors.
*
* @internal
*/
export const composeResponseErrorInterceptors =
(interceptors: ResponseErrorInterceptor[]) =>
async (request: Request, error: unknown): Promise<unknown> => {
let prevError = error
for (const interceptor of interceptors) {
try {
const res = await interceptor({ request, error: prevError })

return res
} catch (err) {
prevError = err
}
}

throw prevError
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import type { Interceptor } from './interceptor'

/** Request Interceptor. */
export type RequestInterceptor = Interceptor<Request>
import type { RequestInterceptor } from './types'

/**
* Adds an header to a request through an interceptor.
Expand All @@ -14,7 +11,7 @@ export type RequestInterceptor = Interceptor<Request>
*/
export const addHeaderInterceptor =
(key: string, value?: string): RequestInterceptor =>
request => {
({ request }) => {
const clone = request.clone()
if (value !== undefined) {
clone.headers.append(key, value)
Expand Down
21 changes: 0 additions & 21 deletions packages/clients/src/internal/interceptors/interceptor.ts

This file was deleted.

4 changes: 0 additions & 4 deletions packages/clients/src/internal/interceptors/response.ts

This file was deleted.

87 changes: 87 additions & 0 deletions packages/clients/src/internal/interceptors/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* 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> }): Request | Promise<Request>
}

/**
* 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> }): Response | Promise<Response>
}

/**
* 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<unknown>
}

/**
* 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
responseError?: ResponseErrorInterceptor
}
5 changes: 3 additions & 2 deletions packages/clients/src/internals.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
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 { 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,
Expand Down
Loading