Skip to content

Commit

Permalink
feat(digital-ocean): added handler and adapter to add support for dig…
Browse files Browse the repository at this point in the history
…ital ocean
  • Loading branch information
H4ad committed Aug 27, 2022
1 parent fa1aa84 commit 0a29085
Show file tree
Hide file tree
Showing 7 changed files with 374 additions and 0 deletions.
45 changes: 45 additions & 0 deletions src/@types/digital-ocean/digital-ocean-http-event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//#region Imports

import { SingleValueHeaders } from '../headers';

//#endregion

/**
* The interface to represents the values of args send when someone calls a function using HTTP Endpoint.
*
* @public
* @breadcrumb Types / Digital Ocean / DigitalOceanHttpEvent
*/
export interface DigitalOceanHttpEvent {
/**
* The HTTP Method of the request
*/
__ow_method: string;

/**
* The body of the request.
*
* @remarks From my tests it usually appears along with {@link __ow_isBase64Encoded}=true, so this body is always a base64 string.
*/
__ow_body?: string;

/**
* Indicates if body is base64, from my tests, when it appears, is always true.
*/
__ow_isBase64Encoded?: true;

/**
* The HTTP Headers of the request
*/
__ow_headers: SingleValueHeaders;

/**
* The path in the request
*/
__ow_path: string;

/**
* This could represent Query Params or Body Params, it's very strange behavior, but it is what it is.
*/
[key: string]: unknown;
}
28 changes: 28 additions & 0 deletions src/@types/digital-ocean/digital-ocean-http-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//#region Imports

import { SingleValueHeaders } from '../headers';

//#endregion

/**
* The interface to represents the response of Digital Ocean Function.
*
* @public
* @breadcrumb Types / Digital Ocean / DigitalOceanHttpResponse
*/
export interface DigitalOceanHttpResponse {
/**
* The HTTP Headers of the response
*/
headers?: SingleValueHeaders;

/**
* The body of the response
*/
body: unknown;

/**
* The HTTP Status code of the response
*/
statusCode: number;
}
1 change: 1 addition & 0 deletions src/@types/digital-ocean/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './digital-ocean-http-event';
280 changes: 280 additions & 0 deletions src/adapters/digital-ocean/http-function.adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
//#region Imports

import { DigitalOceanHttpEvent } from '../../@types/digital-ocean';
import { DigitalOceanHttpResponse } from '../../@types/digital-ocean/digital-ocean-http-response';
import {
AdapterContract,
AdapterRequest,
GetResponseAdapterProps,
OnErrorProps,
} from '../../contracts';
import {
getDefaultIfUndefined,
getEventBodyAsBuffer,
getFlattenedHeadersMap,
getPathWithQueryStringParams,
} from '../../core';

//#endregion

/**
* The result of parser strategy
*
* @breadcrumb Adapters / Digital Ocean / HttpFunctionAdapter
* @public
*/
export type ArgsParserResult = { path: string; body?: string };

/**
* The interface that represents strategy to parse `event` and get the `path` and `body` to forward to framework.
*
* @breadcrumb Adapters / Digital Ocean / HttpFunctionAdapter
* @public
*/
export interface ArgsParserStrategy {
/**
* The method to parse `event` and create path and get the body for the request.
*
* @param event - The event sent from DigitalOcean Functions
*/
parse: (event: DigitalOceanHttpEvent) => ArgsParserResult;
}

/**
* The default parser to get path and body.
*
* By default, if `__ow_body` is set, the params inside `event` will be forwarded to `queryParams`.
* If we didn't find any params, we return `__ow_body`.
* If `__ow_body` is not set, all params is forwarded inside `body`.
*
* @example
* ```typescript
* const strategy = new DefaultArgsParserStrategy();
* const __ow_path = '/test';
*
* // url called: /test?page=1, with body being an image
* console.log(strategy.parse({ __ow_path, __ow_body: 'base64', page: '1' }));
* // { path: '/test?page=1', body: 'base64' }
*
* // url called: /test, with body being an image
* console.log(strategy.parse({ __ow_path, __ow_body: 'base64' }));
* // { path: '/test', body: 'base64' }
*
* // url called: /test?page=1
* console.log(strategy.parse({ __ow_path, page: '1' }));
* // { path: '/test', body: '{"page":"1"}' }
*
* // url called: /test, with body being { test: true }
* console.log(strategy.parse({ __ow_path, test: true }));
* // { path: '/test', body: '{"test":true}' }
* ```
*
* @breadcrumb Adapters / Digital Ocean / HttpFunctionAdapter
* @public
*/
export class DefaultArgsParserStrategy implements ArgsParserStrategy {
/**
* {@inheritDoc}
*/
public parse(event: DigitalOceanHttpEvent): ArgsParserResult {
const paramKeys = Object.keys(event).filter(key => !key.startsWith('__ow'));

if (paramKeys.length === 0)
return { path: event.__ow_path, body: event.__ow_body };

const params: Record<string, any> = Object.entries(event)
.filter(([key]) => !key.startsWith('__ow'))
.reduce((acc, [key, value]) => {
acc[key] = value;

return acc;
}, {});

if (event.__ow_body) {
const path = getPathWithQueryStringParams(event.__ow_path, params);

return { path, body: event.__ow_body };
}

return {
path: event.__ow_path,
body: JSON.stringify(params),
};
}
}

/**
* The options to customize the {@link HttpFunctionAdapter}
*
* @breadcrumb Adapters / Digital Ocean / HttpFunctionAdapter
* @public
*/
export interface HttpFunctionAdapterOptions {
/**
* Strip base path for custom domains
*
* @defaultValue ''
*/
stripBasePath?: string;

/**
* Because DigitalOcean don't tell us if param inside args is query param or body param, we need to guess.
* By default, we use {@link DefaultArgsParserStrategy} which prefers send params inside body instead query param.
*
* @defaultValue {@link DefaultArgsParserStrategy}
*/
argsParserStrategy?: ArgsParserStrategy;
}

/**
* The adapter to handle requests from Digital Ocean Functions when called from HTTP Endpoint.
*
* @example
* ```typescript
* const stripBasePath = '/any/custom/base/path'; // default ''
* const adapter = new HttpFunctionAdapter({ stripBasePath });
* ```
*
* @breadcrumb Adapters / Digital Ocean / HttpFunctionAdapter
* @public
*/
export class HttpFunctionAdapter
implements
AdapterContract<DigitalOceanHttpEvent, void, DigitalOceanHttpResponse>
{
//#region Constructor

/**
* Default constructor
*
* @param options - The options to customize the {@link HttpFunctionAdapter}
*/
constructor(protected readonly options?: HttpFunctionAdapterOptions) {}

//#endregion

//#region Public Methods

/**
* {@inheritDoc}
*/
public getAdapterName(): string {
return HttpFunctionAdapter.name;
}

/**
* {@inheritDoc}
*/
public canHandle(event: unknown): event is DigitalOceanHttpEvent {
const digitalOceanHttpEvent = event as DigitalOceanHttpEvent;

return (
digitalOceanHttpEvent.__ow_path !== undefined &&
digitalOceanHttpEvent.__ow_method !== undefined &&
digitalOceanHttpEvent.__ow_headers !== undefined
);
}

/**
* {@inheritDoc}
*/
public getRequest(event: DigitalOceanHttpEvent): AdapterRequest {
if (!event.__ow_path) event.__ow_path = '/';

event.__ow_path = this.getPathStrippedPath(event.__ow_path);

const headers = event.__ow_headers;
const method = event.__ow_method;
const argsParserStrategy = getDefaultIfUndefined(
this.options?.argsParserStrategy,
new DefaultArgsParserStrategy(),
);
const { path, body: rawBody } = argsParserStrategy.parse(event);

let body: Buffer | undefined;

if (rawBody) {
const [bufferBody, contentLength] = getEventBodyAsBuffer(
rawBody,
!!event.__ow_isBase64Encoded,
);

body = bufferBody;
headers['content-length'] = String(contentLength);
}

const remoteAddress = headers['x-forwarded-for'];

return {
method,
headers,
body,
remoteAddress,
path,
};
}

/**
* {@inheritDoc}
*/
public getResponse({
headers: responseHeaders,
body,
statusCode,
}: GetResponseAdapterProps<DigitalOceanHttpEvent>): DigitalOceanHttpResponse {
const headers = getFlattenedHeadersMap(responseHeaders);

return {
statusCode,
body,
headers,
};
}

/**
* {@inheritDoc}
*/
public onErrorWhileForwarding({
error,
delegatedResolver,
respondWithErrors,
event,
log,
}: OnErrorProps<DigitalOceanHttpEvent, DigitalOceanHttpResponse>): void {
const body = respondWithErrors ? error.stack : '';
const errorResponse = this.getResponse({
event,
statusCode: 500,
body: body || '',
headers: {},
isBase64Encoded: false,
log,
});

delegatedResolver.succeed(errorResponse);
}

//#endregion

//#region Protected Methods

/**
* Get path from event
*
* @param path - The original path
*/
protected getPathStrippedPath(path: string): string {
const stripBasePath = getDefaultIfUndefined(
this.options?.stripBasePath,
'',
);

if (!stripBasePath) return path;

const replaceRegex = new RegExp(`^${stripBasePath}`);

return path.replace(replaceRegex, '');
}

//#endregion
}
1 change: 1 addition & 0 deletions src/adapters/digital-ocean/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './http-function.adapter';
19 changes: 19 additions & 0 deletions src/handlers/digital-ocean/digital-ocean.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* eslint-disable @typescript-eslint/unbound-method */
//#region Imports

import { DefaultHandler } from '../default';

//#endregion

/**
* The class that implements a serverless handler for Digital Ocean Functions.
*
* @breadcrumb Handlers / DigitalOceanHandler
* @public
*/
export class DigitalOceanHandler<
TApp,
TEvent,
TResponse,
TReturn,
> extends DefaultHandler<TApp, TEvent, void, void, TResponse, TReturn> {}
Empty file.

0 comments on commit 0a29085

Please sign in to comment.