Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supports [email protected] #165

Merged
merged 10 commits into from
Nov 15, 2023
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
5 changes: 3 additions & 2 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import inspector from 'inspector';
const testTimeout = inspector.url() ? 1e8 : 10e3;

const config: Config.InitialOptions = {
preset: '@lifeomic/jest-config',
testEnvironment: 'node',
transform: {
'^.+\\.tsx?$': [
'^.+\\.(j|t)s$': [
'@swc/jest',
{
jsc: { target: 'es2019' },
Expand All @@ -35,6 +34,8 @@ const config: Config.InitialOptions = {
verbose: true,
maxWorkers: '50%',
testTimeout,
// need to transform imported js file using ESM
transformIgnorePatterns: ['/node_modules/(?!(axios)/)'],
};

export default config;
8 changes: 3 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@
"clean": "yarn tsc --build --clean"
},
"devDependencies": {
"@lifeomic/eslint-config-standards": "^3.0.0",
"@lifeomic/jest-config": "^1.1.2",
"@lifeomic/typescript-config": "^1.0.3",
"@lifeomic/eslint-config-standards": "3.0.0",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I locked it to 3.0.0 to be able to run yarn lint on node.js v14.

"@lifeomic/typescript-config": "^3.1.0",
"@swc/core": "^1.2.207",
"@swc/jest": "^0.2.21",
"@types/jest": "^28.1.3",
Expand All @@ -42,7 +41,6 @@
"@types/uuid": "^8.3.4",
"aws-sdk-client-mock": "^1.0.0",
"conventional-changelog-conventionalcommits": "^4.0.0",
"coveralls": "^3.1.0",
"eslint": "^8.18.0",
"jest": "^28.1.2",
"jest-mock-extended": "^2.0.6",
Expand All @@ -60,7 +58,7 @@
"@aws-sdk/signature-v4": "^3.110.0",
"@aws-sdk/url-parser": "^3.357.0",
"@types/aws-lambda": "^8.10.101",
"axios": "^0.27.2",
"axios": "^1.6.0",
"lodash": "^4.17.21",
"nearley": "2",
"url-parse": "^1.5.10",
Expand Down
4 changes: 2 additions & 2 deletions src/adapters/helpers/apiGateway.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AxiosRequestHeaders } from 'axios';
import { RawAxiosRequestHeaders } from 'axios';
import { APIGatewayProxyEvent } from 'aws-lambda';

/**
Expand All @@ -7,7 +7,7 @@ import { APIGatewayProxyEvent } from 'aws-lambda';
* https://github.com/apollographql/apollo-server/issues/5504
*/
export type ToProxyHeaders = Pick<APIGatewayProxyEvent, 'multiValueHeaders' | 'headers'>;
export const toProxyHeaders = (headers: AxiosRequestHeaders = {}): ToProxyHeaders => {
export const toProxyHeaders = (headers: RawAxiosRequestHeaders = {}): ToProxyHeaders => {
const response: ToProxyHeaders = {
multiValueHeaders: {},
headers: {},
Expand Down
10 changes: 5 additions & 5 deletions src/adapters/helpers/chainAdapters.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import axios, { AxiosAdapter } from 'axios';
import type { AlphaOptions, AlphaAdapter } from '../../types';
import axios, { getAdapter } from 'axios';
import type { InternalAlphaRequestConfig, AlphaAdapter } from '../../types';

export type Predicate = (config: AlphaOptions) => any;
export type Predicate = (config: InternalAlphaRequestConfig) => any;

export const chainAdapters = (
config: AlphaOptions,
config: InternalAlphaRequestConfig,
predicate: Predicate,
adapter: AlphaAdapter,
) => {
const nextAdapter = config.adapter || axios.defaults.adapter as AxiosAdapter;
const nextAdapter = getAdapter(config.adapter || axios.defaults.adapter);

config.adapter = async (config) => {
if (predicate(config)) {
Expand Down
10 changes: 5 additions & 5 deletions src/adapters/helpers/lambdaResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import http from 'http';
import { RequestError } from './requestError';
import { TextEncoder } from 'util';

import type { AlphaOptions } from '../../types';
import type { AlphaOptions, InternalAlphaRequestConfig } from '../../types';
import type { InvocationRequest } from '@aws-sdk/client-lambda';
import type { AxiosResponse } from 'axios';
import { HandlerRequest } from '../../types';
import { type AlphaResponse, HandlerRequest } from '../../types';

export interface Payload {
body: string;
Expand All @@ -24,13 +24,13 @@ const payloadToData = (config: AlphaOptions, payload: Payload) => {
};

export const lambdaResponse = (
config: AlphaOptions,
config: InternalAlphaRequestConfig,
request: InvocationRequest | HandlerRequest,
payload: Payload,
): AxiosResponse => {
): AlphaResponse => {
const data = payloadToData(config, payload);

const response: AxiosResponse = {
const response: AlphaResponse = {
config,
data,
headers: payload.headers,
Expand Down
10 changes: 5 additions & 5 deletions src/adapters/helpers/requestError.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { InvocationRequest, InvocationResponse } from '@aws-sdk/client-lambda';
import { HandlerRequest, AlphaOptions } from '../../types';
import { AxiosError, AxiosResponse } from 'axios';
import { HandlerRequest } from '../../types';
import { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios';

export const isAxiosError = (err: any | AxiosError): err is AxiosError =>
export const isAxiosError = (err: any): err is AxiosError =>
(typeof err === 'object') && !!err.isAxiosError;

export const isAlphaRequestError = (err: any | RequestError): err is RequestError =>
export const isAlphaRequestError = (err: any): err is RequestError =>
(typeof err === 'object') && !!err.isAlphaRequestError;

export class RequestError extends Error implements Omit<AxiosError, 'response' | 'toJSON' | 'isAxiosError'> {
Expand All @@ -15,7 +15,7 @@ export class RequestError extends Error implements Omit<AxiosError, 'response' |

constructor (
message: string,
public config: AlphaOptions,
public config: InternalAxiosRequestConfig,
public request: InvocationRequest | HandlerRequest,
public response?: InvocationResponse | AxiosResponse,
) {
Expand Down
6 changes: 3 additions & 3 deletions src/adapters/lambda-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { lambdaEvent } from './helpers/lambdaEvent';
import { lambdaResponse, Payload } from './helpers/lambdaResponse';
import { promisify } from './helpers/promisify';
import { RequestError } from './helpers/requestError';
import { AlphaOptions, AlphaAdapter, HandlerRequest } from '../types';
import { InternalAlphaRequestConfig, AlphaAdapter, HandlerRequest } from '../types';
import { v4 as uuid } from 'uuid';
import { Context, Handler } from 'aws-lambda';
import { Alpha } from '../alpha';
Expand Down Expand Up @@ -39,12 +39,12 @@ const lambdaHandlerAdapter: AlphaAdapter = async (config) => {
try {
const result = await handler(request.event, request.context as Context) as Payload;
return lambdaResponse(config, request, result);
} catch (error: any | Error) {
} catch (error: any) {
throw new RequestError(error.message as string, config, request, error.response as AxiosResponse);
}
};

const lambdaHandlerRequestInterceptor = (config: AlphaOptions) => chainAdapters(
const lambdaHandlerRequestInterceptor = (config: InternalAlphaRequestConfig) => chainAdapters(
config,
(config) => !isAbsoluteURL(config.url as string) && config.lambda,
lambdaHandlerAdapter,
Expand Down
4 changes: 2 additions & 2 deletions src/adapters/lambda-invocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { lambdaEvent } from './helpers/lambdaEvent';
import { lambdaResponse, Payload } from './helpers/lambdaResponse';
import { parseLambdaUrl, isAbsoluteURL } from '../utils/url';
import { RequestError } from './helpers/requestError';
import { AlphaOptions, AlphaAdapter } from '../types';
import { InternalAlphaRequestConfig, AlphaAdapter } from '../types';
import { Alpha } from '../alpha';
import { AbortController } from '@aws-sdk/abort-controller';

Expand Down Expand Up @@ -101,7 +101,7 @@ const lambdaInvocationAdapter: AlphaAdapter = async (config) => {
return lambdaResponse(config, request, payload);
};

const lambdaInvocationRequestInterceptor = (config: AlphaOptions) => {
const lambdaInvocationRequestInterceptor = (config: InternalAlphaRequestConfig) => {
return chainAdapters(
config,
(config) => (config.url as string).startsWith('lambda:') || (config.baseURL?.startsWith('lambda:')),
Expand Down
20 changes: 15 additions & 5 deletions src/adapters/response-retry.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import isBoolean from 'lodash/isBoolean';
import defaults from 'lodash/defaults';
import _get from 'lodash/get';
import _inRange from 'lodash/inRange';
import { RequestError } from './helpers/requestError';
import type { AlphaOptions } from '../types';
import type { Alpha } from '../alpha';
import { AxiosError } from 'axios';

export interface RetryOptions {
attempts: number;
Expand All @@ -17,11 +18,20 @@ export interface RetryAlphaOptions extends Omit<AlphaOptions, 'retry'> {
retry: RetryOptions;
}

const isRetryableError = (error: any | RequestError) => {
const isServerSideError = (error: RequestError) => {
return (
error.response
&& (
_inRange(_get(error.response, 'StatusCode', 0) as number, 500, 600)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before the addition of StatusCode checks, was this function failing to detect some failures?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function is refactored from isRetryableError(). it checked the statusCode of http request via Axios API (AxiosResponse) only.

These lines are for checking the status code of lambda function invocation via Lambda API (InvocationResponse). I could remove this one if aws internal error during lambda invocation is not retryable.

|| _inRange(_get(error.response, 'status', 0) as number, 500, 600)
)
);
};

const isRetryableError = (error: RequestError) => {
if (error.isLambdaInvokeTimeout) return true;

return error.code !== 'ECONNABORTED' &&
(!error.response || (error.response.status >= 500 && error.response.status <= 599));
return error.code !== 'ECONNABORTED' && isServerSideError(error);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be ! isServerSideError ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I don't think so. My mistake

};

const DEFAULTS = {
Expand Down Expand Up @@ -57,7 +67,7 @@ const exponentialBackoff = (config: RetryAlphaOptions) => {
export const setup = (client: Alpha) => {
client.interceptors.response.use(
undefined,
async (err: any | AxiosError) => {
async (err: any) => {
if (!('config' in err && err.config.retry)) {
return Promise.reject(err);
}
Expand Down
10 changes: 5 additions & 5 deletions src/alpha.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pick from 'lodash/pick';
import merge from 'lodash/merge';

import axios, { Axios, AxiosAdapter, AxiosResponse } from 'axios';
import axios, { Axios, AxiosAdapter, AxiosHeaders, AxiosResponse } from 'axios';
import cloneDeep from 'lodash/cloneDeep';
import { AlphaOptions, AlphaResponse, HandlerRequest } from './types';
import { Handler } from 'aws-lambda';
Expand Down Expand Up @@ -35,7 +35,7 @@ export class Alpha extends Axios {
constructor(target: string | Handler)
constructor(target: string | Handler, config: AlphaOptions)
constructor (...[target, config]: ConstructorArgs) {
const tmpOptions: AlphaOptions = {};
const tmpOptions: AlphaOptions = { headers: new AxiosHeaders() };
if (typeof target === 'object') {
Object.assign(tmpOptions, target);
} else if (typeof target === 'string') {
Expand Down Expand Up @@ -75,20 +75,20 @@ export class Alpha extends Axios {
if (castResp.status === 301 || castResp.status === 302) {
if (maxRedirects === 0) {
const request = castResp.request as InvocationRequest | HandlerRequest;
throw new RequestError('Exceeded maximum number of redirects.', castResp.config, request, response);
throw new RequestError('Exceeded maximum number of redirects.', castResp.config, request, castResp);
}

const redirect = cloneDeep(config);
redirect.maxRedirects = maxRedirects - 1;
redirect.url = resolve(castResp.headers.location, castResp.config.url as string);
redirect.url = resolve(castResp.headers.location as string, castResp.config.url);
return this.request(redirect);
}

return response as R;
}

get<T = any, R = AlphaResponse<T>, D = any>(url: string, config?: AlphaOptions<D>): Promise<R> {
return super.get(url, config);
return super.get<T, R, D>(url, config);
}
delete<T = any, R = AlphaResponse<T>, D = any>(url: string, config?: AlphaOptions<D>): Promise<R> {
return super.delete(url, config);
Expand Down
11 changes: 5 additions & 6 deletions src/interceptors/aws-v4-signature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import {
HttpRequest,
HeaderBag,
} from '@aws-sdk/types';

import buildURL from 'axios/lib/helpers/buildURL';
import transformData from 'axios/lib/core/transformData';
import buildFullPath from 'axios/lib/core/buildFullPath';
import buildURL from 'axios/unsafe/helpers/buildURL.js';
import transformData from 'axios/unsafe/core/transformData.js';
import buildFullPath from 'axios/unsafe/core/buildFullPath.js';

import type { Alpha } from '../alpha';
import type { AlphaInterceptor, AlphaOptions } from '../types';
Expand Down Expand Up @@ -37,7 +36,7 @@ const unsignableHeaders = new Set([
]);

const combineParams = (url: string, { params, paramsSerializer }: AlphaOptions): HttpRequest['query'] => {
const fullUrl = buildURL(url, params, paramsSerializer);
const fullUrl: string = buildURL(url, params, paramsSerializer);
const { query } = parseUrl(fullUrl);
return query;
};
Expand All @@ -55,7 +54,7 @@ const awsV4Signature: AlphaInterceptor = async (config) => {
return config;
}

let fullPath = buildFullPath(config.baseURL, config.url);
let fullPath: string = buildFullPath(config.baseURL, config.url);
if (isLambdaUrl(fullPath)) {
const lambdaUrl = parseLambdaUrl(fullPath) as LambdaUrl;
fullPath = `lambda://${lambdaUrl.name}${lambdaUrl.path}`;
Expand Down
6 changes: 5 additions & 1 deletion src/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@
import { isAbsoluteURL, LambdaUrl, parseLambdaUrl } from './utils/url';
import { URL } from 'url';

export const resolve = (url: string, base: string) => {
export const resolve = (url: string, base?: string) => {
shawnzhu marked this conversation as resolved.
Show resolved Hide resolved
if (isAbsoluteURL(url)) {
return url;
}

if (!base) {
return url;
}

let lambdaParts = parseLambdaUrl(base);

if (!lambdaParts) {
Expand Down
16 changes: 9 additions & 7 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AxiosPromise, AxiosRequestConfig, AxiosResponse } from 'axios';
import type { AxiosPromise, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import type { Lambda } from '@aws-sdk/client-lambda';
import type { Context, Handler } from 'aws-lambda';
import { SignatureV4CryptoInit, SignatureV4Init } from '@aws-sdk/signature-v4';
Expand All @@ -13,10 +13,6 @@ export interface RetryOptions {
type SignatureV4Constructor = SignatureV4Init & SignatureV4CryptoInit;
type SignatureV4Optionals = 'credentials' | 'region' | 'sha256' | 'service';

export interface AlphaResponse<ResponseData = any, ConfigData = any> extends AxiosResponse<ResponseData> {
config: AlphaOptions<ConfigData>;
}

export type SignAwsV4Config =
& Omit<SignatureV4Constructor, SignatureV4Optionals>
& Partial<Pick<SignatureV4Constructor, SignatureV4Optionals>>;
Expand All @@ -33,8 +29,14 @@ export interface AlphaOptions<D = any> extends AxiosRequestConfig<D> {
Lambda?: typeof Lambda;
}

export type AlphaAdapter = (config: AlphaOptions) => AxiosPromise;
export type AlphaInterceptor = (config: AlphaOptions) => (Promise<AlphaOptions> | AlphaOptions);
export type InternalAlphaRequestConfig<D = any> = AlphaOptions<D> & InternalAxiosRequestConfig;

export type AlphaAdapter = (config: InternalAlphaRequestConfig) => AxiosPromise;
export type AlphaInterceptor = (config: InternalAlphaRequestConfig) => (Promise<InternalAlphaRequestConfig> | InternalAlphaRequestConfig);

export interface AlphaResponse<ResponseData = any, ConfigData = any> extends AxiosResponse<ResponseData> {
config: InternalAlphaRequestConfig<ConfigData>;
}

export interface HandlerRequest<T = Record<string, any>> {
event: T;
Expand Down
5 changes: 2 additions & 3 deletions test/adapters/helpers/apiGateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@ import { ToProxyHeaders, toProxyHeaders } from '../../../src/adapters/helpers/ap
import { v4 as uuid } from 'uuid';
import { AxiosRequestHeaders } from 'axios';

// TODO support the use case that request header value could be undefined/null
test('will convert header values', () => {
expect(toProxyHeaders()).toEqual({ multiValueHeaders: {}, headers: {} });
const multiStringHeader = [uuid(), ` ${uuid()}`, `${uuid()} `, uuid()];
// @ts-expect-error undefined is not allowed as header value, but it works
const input: AxiosRequestHeaders = {
stringHeader: uuid(),
multiStringHeader: multiStringHeader.join(','),
// @ts-ignore
numberHeader: 123456,
// @ts-ignore
booleanHeader: true,
// @ts-ignore
shawnzhu marked this conversation as resolved.
Show resolved Hide resolved
undefinedHeader: undefined,
};

Expand Down
16 changes: 9 additions & 7 deletions test/adapters/helpers/chainAdapters.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import axios from 'axios';
import axios, { AxiosHeaders, getAdapter } from 'axios';
import { chainAdapters } from '../../../src/adapters/helpers/chainAdapters';
import { AlphaAdapter } from '../../../src';

test('will set default adapter', () => {
test('will set default adapter', async () => {
const adapter: AlphaAdapter = jest.fn();
const defaultAdapter = axios.defaults.adapter = jest.fn();
const config = chainAdapters(
{},
{ headers: new AxiosHeaders() },
() => false,
adapter,
);
expect(defaultAdapter).not.toBeCalled();
expect(() => config.adapter!(config)).not.toThrow();
expect(defaultAdapter).toBeCalled();
expect(adapter).not.toBeCalled();
expect(defaultAdapter).not.toHaveBeenCalled();

expect(config.adapter).toHaveLength(1); // an array of adapters
await expect(getAdapter(config.adapter)(config)).resolves.toBeUndefined();
expect(defaultAdapter).toHaveBeenCalled();
expect(adapter).not.toHaveBeenCalled();
});
Loading
Loading