Skip to content

Commit

Permalink
Merge pull request #26 from H4ad/feature/trpc
Browse files Browse the repository at this point in the history
feat(trpc): added support to framework trpc
  • Loading branch information
H4ad authored Jun 28, 2022
2 parents 1ed5c6f + abbd86f commit 864f15c
Show file tree
Hide file tree
Showing 11 changed files with 852 additions and 42 deletions.
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 9 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
"node.js",
"http",
"huawei",
"functiongraph"
"functiongraph",
"trpc"
],
"bugs": {
"url": "https://github.com/H4ad/serverless-adapter/issues"
Expand Down Expand Up @@ -97,7 +98,8 @@
},
"peerDependencies": {
"@hapi/hapi": ">= 20.0.0",
"@types/aws-lambda": "^8.10.92",
"@trpc/server": ">= 9.0.0",
"@types/aws-lambda": ">= 8.10.92",
"@types/express": ">= 4.15.4",
"@types/hapi": ">= 18.0.7",
"@types/koa": ">= 2.11.2",
Expand All @@ -107,10 +109,13 @@
"koa": ">= 2.5.1"
},
"peerDependenciesMeta": {
"@types/aws-lambda": {
"@hapi/hapi": {
"optional": true
},
"@hapi/hapi": {
"@trpc/server": {
"optional": true
},
"@types/aws-lambda": {
"optional": true
},
"express": {
Expand Down
1 change: 1 addition & 0 deletions src/frameworks/trpc/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './trpc.framework';
296 changes: 296 additions & 0 deletions src/frameworks/trpc/trpc.framework.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
//#region

import { IncomingMessage, ServerResponse } from 'http';
import type { AnyRouter, DataTransformer } from '@trpc/server';
import {
NodeHTTPCreateContextFn,
NodeHTTPCreateContextFnOptions,
NodeHTTPHandlerOptions,
nodeHTTPRequestHandler,
} from '@trpc/server/adapters/node-http';
import { SingleValueHeaders } from '../../@types';
import { FrameworkContract } from '../../contracts';
import { getDefaultIfUndefined, getFlattenedHeadersMap } from '../../core';

//#endregion

/**
* The transformer that is responsible to transform buffer's input to javascript objects
*
* @breadcrumb Frameworks / TrpcFramework
* @public
*/
export class BufferToJSObjectTransformer implements DataTransformer {
/**
* Deserialize unknown values to javascript objects
*
* @param value - The value to be deserialized
*/
public deserialize(value?: unknown): any {
if (value instanceof Buffer) return JSON.parse(value.toString('utf-8'));

return value;
}

/**
* The value to be serialized, do nothing because tRPC can handle.
*
* @param value - The value to be serialized
*/
public serialize(value: any): any {
return value;
}
}

/**
* The context created by this library that allows getting some information from the request and setting the status and header of the response.
*
* @breadcrumb Frameworks / TrpcFramework
* @public
*/
export interface TrpcAdapterBaseContext {
/**
* The request object that will be forward to your app
*/
request: IncomingMessage;

/**
* The response object that will be forward to your app to output the response
*/
response: ServerResponse;

/**
* The method to set response status.
*
* @param statusCode - The response status that you want
*/
setStatus(statusCode: number): void;

/**
* The method to set some header in the response
*
* @param name - The name of the header
* @param value - The value to be set in the header
*/
setHeader(name: string, value: number | string): void;

/**
* The method to remove some header from the response
*
* @param name - The name of the header
*/
removeHeader(name: string): void;

/**
* The method to return the value of some header from the request
*
* @param name - The name of the header
*/
getHeader(name: string): string | undefined;

/**
* The method to return the request headers
*/
getHeaders(): SingleValueHeaders;

/**
* The method to return user's address
*/
getIp(): string | undefined;

/**
* The method to return the URL called
*/
getUrl(): string | undefined;

/**
* The method to return the HTTP Method in the request
*/
getMethod(): string | undefined;
}

/**
* This is the context merged between {@link TrpcAdapterBaseContext} and the {@link TContext} that you provided.
*
* This context will be merged with the context you created with `createContext` inside {@link TrpcFrameworkOptions}.
* So to make the type work, just send the properties you've added inside {@link TContext}.
*
* @example
* ```typescript
* type MyCustomContext = { user: { name: string } };
* type TrpcContext = TrpcAdapterContext<MyCustomContext>; // your final context type to put inside trpc.router
* ```
*
* @breadcrumb Frameworks / TrpcFramework
* @public
*/
export type TrpcAdapterContext<TContext> = TContext & TrpcAdapterBaseContext;

/**
* The options to customize the {@link TrpcFramework}
*
* @breadcrumb Frameworks / TrpcFramework
* @public
*/
export type TrpcFrameworkOptions<TContext> = Omit<
NodeHTTPHandlerOptions<AnyRouter<TContext>, IncomingMessage, ServerResponse>,
'router' | 'createContext'
> & {
createContext?: (
opts: NodeHTTPCreateContextFnOptions<IncomingMessage, ServerResponse>,
) =>
| Omit<TContext, keyof TrpcAdapterBaseContext>
| Promise<Omit<TContext, keyof TrpcAdapterBaseContext>>;
};

/**
* The framework that forwards requests to TRPC handler
*
* @breadcrumb Frameworks / TrpcFramework
* @public
*/
export class TrpcFramework<TContext extends TrpcAdapterBaseContext>
implements FrameworkContract<AnyRouter<TContext>>
{
//#region Constructor

/**
* Default constructor
*/
constructor(protected readonly options?: TrpcFrameworkOptions<TContext>) {}

//#endregion

//#region Public Methods

/**
* {@inheritDoc}
*/
public sendRequest(
app: AnyRouter<TContext>,
request: IncomingMessage,
response: ServerResponse,
): void {
const endpoint = this.getSafeUrlForTrpc(request);

nodeHTTPRequestHandler({
req: request,
res: response,
path: endpoint,
router: app,
...this.options,
createContext: createContextOptions =>
this.mergeDefaultContextWithOptionsContext(createContextOptions),
});
}

//#endregion

//#region Protected Methods

/**
* Get safe url that can be used inside Trpc.
*
* @example
* ```typescript
* const url = getSafeUrlForTrpc('/users?input=hello');
* console.log(url); // users
* ```
*
* @param request - The request object that will be forward to your app
*/
protected getSafeUrlForTrpc(request: IncomingMessage): string {
let url = request.url!;

if (url.startsWith('/')) url = url.slice(1);

if (url.includes('?')) url = url.split('?')[0];

return url;
}

/**
* Merge the default context ({@link TrpcAdapterContext}) with the context created by the user.
*
* @param createContextOptions - The options sent by trpc to create the context
*/
protected mergeDefaultContextWithOptionsContext(
createContextOptions: NodeHTTPCreateContextFnOptions<
IncomingMessage,
ServerResponse
>,
): TContext | Promise<TContext> {
const createContextFromOptions: NodeHTTPCreateContextFn<
AnyRouter<Omit<TContext, keyof TrpcAdapterBaseContext>>,
IncomingMessage,
ServerResponse
> = getDefaultIfUndefined(
this.options?.createContext,
() =>
undefined as unknown as Omit<TContext, keyof TrpcAdapterBaseContext>,
);

const resolvedContext = createContextFromOptions(createContextOptions);

if (resolvedContext && resolvedContext.then) {
return resolvedContext.then(context =>
this.wrapResolvedContextWithDefaultContext(
context,
createContextOptions,
),
);
}

return this.wrapResolvedContextWithDefaultContext(
resolvedContext,
createContextOptions,
);
}

/**
* Wraps the resolved context from the {@link TrpcFrameworkOptions} created with `createContext` and merge
* with the {@link TrpcAdapterContext} generated by the library.
*
* @param resolvedContext - The context created with `createContext` inside {@link TrpcFrameworkOptions}
* @param createContextOptions - The options sent by trpc to create the context
*/
protected wrapResolvedContextWithDefaultContext(
resolvedContext: TContext,
createContextOptions: NodeHTTPCreateContextFnOptions<
IncomingMessage,
ServerResponse
>,
): TContext {
const request = createContextOptions.req;
const response = createContextOptions.res;

return {
...resolvedContext,
request,
response,
getUrl: () => request.url,
getMethod: () => request.method,
getHeaders: () => getFlattenedHeadersMap(request.headers, ',', true),
setHeader: (header, value) => {
response.setHeader(header, value);
},
removeHeader: header => {
response.removeHeader(header);
},
getHeader: (header: string) => {
return getFlattenedHeadersMap(request.headers, ',', true)[
header.toLowerCase()
];
},
setStatus: (statusCode: number) => {
response.statusCode = statusCode;
// force undefined to get default message for the status code
// ref: https://nodejs.org/dist/latest-v16.x/docs/api/http.html#responsestatusmessage
response.statusMessage = undefined as any;
},
getIp: () => request.connection.remoteAddress,
};
}

//#endregion
}
1 change: 1 addition & 0 deletions src/index.doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './frameworks/fastify';
export * from './frameworks/koa';
export * from './frameworks/hapi';
export * from './frameworks/lazy';
export * from './frameworks/trpc';
export * from './handlers/default';
export * from './handlers/huawei';
export * from './network';
Expand Down
3 changes: 2 additions & 1 deletion test/frameworks/koa.framework.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import Application, { Context } from 'koa';
import { NO_OP } from '../../src';
import { KoaFramework } from '../../src/frameworks/koa';
import { TestRouteBuilderHandler, createTestSuiteFor } from './utils';

function createHandler(): TestRouteBuilderHandler<Application> {
return (app, path, handler) => {
app.use((ctx: Context) => {
const [statusCode, resultBody, headers] = handler(ctx.headers, ctx.body);
const [statusCode, resultBody, headers] = handler(ctx.headers, NO_OP);

for (const header of Object.keys(headers))
ctx.set(header, headers[header]);
Expand Down
Loading

0 comments on commit 864f15c

Please sign in to comment.