Skip to content

Commit a94ba25

Browse files
author
Will Pote
committed
Initial commit: 4
1 parent 6c91854 commit a94ba25

17 files changed

+1218
-284
lines changed

bin/app.ts

+26-8
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,20 @@ export class RoutingAPIStage extends Stage {
3030
constructor(
3131
scope: Construct,
3232
id: string,
33-
props: StageProps & { nodeRPC: string }
33+
props: StageProps & {
34+
nodeRPC: string;
35+
nodeRPCUsername: string;
36+
nodeRPCPassword: string;
37+
}
3438
) {
3539
super(scope, id, props);
36-
const { nodeRPC } = props;
40+
const { nodeRPC, nodeRPCUsername, nodeRPCPassword } = props;
3741

38-
const { url } = new RoutingAPIStack(this, 'RoutingAPI', { nodeRPC });
42+
const { url } = new RoutingAPIStack(this, 'RoutingAPI', {
43+
nodeRPC,
44+
nodeRPCUsername,
45+
nodeRPCPassword,
46+
});
3947
this.url = url;
4048
}
4149
}
@@ -81,17 +89,21 @@ export class RoutingAPIPipeline extends Stack {
8189

8290
// Secrets are stored in secrets manager in the pipeline account. Accounts we deploy to
8391
// have been granted permissions to access secrets via resource policies.
84-
const rpcNodeUrl = sm.Secret.fromSecretAttributes(this, 'RPCNodeUrl', {
92+
const rpcNodeDetails = sm.Secret.fromSecretAttributes(this, 'RPCNodeUrl', {
8593
secretCompleteArn:
86-
'arn:aws:secretsmanager:us-east-2:644039819003:secret:routing-api-infura-rpc-url-fSmY28',
87-
});
94+
'arn:aws:secretsmanager:us-east-2:644039819003:secret:routing-api-bison-trails-c1bCOW',
95+
}).secretValue.toJSON();
8896

8997
// Beta us-east-2
9098
const betaUsEast2Stage = new RoutingAPIStage(this, 'beta-us-east-2', {
9199
env: { account: '145079444317', region: 'us-east-2' },
92-
nodeRPC: rpcNodeUrl.secretValue.toString(),
100+
nodeRPC: rpcNodeDetails.url,
101+
nodeRPCUsername: rpcNodeDetails.username ?? '',
102+
nodeRPCPassword: rpcNodeDetails.password ?? '',
93103
});
104+
94105
const betaUsEast2AppStage = pipeline.addApplicationStage(betaUsEast2Stage);
106+
95107
this.addIntegTests(
96108
pipeline,
97109
sourceArtifact,
@@ -102,9 +114,13 @@ export class RoutingAPIPipeline extends Stack {
102114
// Prod us-east-2
103115
const prodUsEast2Stage = new RoutingAPIStage(this, 'prod-us-east-2', {
104116
env: { account: '606857263320', region: 'us-east-2' },
105-
nodeRPC: rpcNodeUrl.secretValue.toString(),
117+
nodeRPC: rpcNodeDetails.url,
118+
nodeRPCUsername: rpcNodeDetails.username ?? '',
119+
nodeRPCPassword: rpcNodeDetails.password ?? '',
106120
});
121+
107122
const prodUsEast2AppStage = pipeline.addApplicationStage(prodUsEast2Stage);
123+
108124
this.addIntegTests(
109125
pipeline,
110126
sourceArtifact,
@@ -149,6 +165,8 @@ const app = new cdk.App();
149165
// Local dev stack
150166
new RoutingAPIStack(app, 'RoutingAPIStack', {
151167
nodeRPC: process.env.JSON_RPC_URL!,
168+
nodeRPCUsername: process.env.JSON_RPC_USERNAME!,
169+
nodeRPCPassword: process.env.JSON_RPC_PASSWORD!,
152170
});
153171

154172
new RoutingAPIPipeline(app, 'RoutingAPIPipelineStack', {

bin/stacks/routing-api-stack.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ export class RoutingAPIStack extends cdk.Stack {
1313
constructor(
1414
parent: cdk.Construct,
1515
name: string,
16-
props: cdk.StackProps & { nodeRPC: string }
16+
props: cdk.StackProps & {
17+
nodeRPC: string;
18+
nodeRPCUsername: string;
19+
nodeRPCPassword: string;
20+
}
1721
) {
1822
super(parent, name, props);
1923

@@ -22,12 +26,18 @@ export class RoutingAPIStack extends cdk.Stack {
2226
'RoutingCachingStack'
2327
);
2428

25-
const { nodeRPC } = props;
29+
const { nodeRPC, nodeRPCUsername, nodeRPCPassword } = props;
2630

2731
const { routingLambda } = new RoutingLambdaStack(
2832
this,
2933
'RoutingLambdaStack',
30-
{ poolCacheBucket, poolCacheKey, nodeRPC }
34+
{
35+
poolCacheBucket,
36+
poolCacheKey,
37+
nodeRPC,
38+
nodeRPCUsername,
39+
nodeRPCPassword,
40+
}
3141
);
3242

3343
const accessLogGroup = new aws_logs.LogGroup(this, 'RoutingAPIGAccessLogs');

bin/stacks/routing-lambda-stack.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export interface RoutingLambdaStackProps extends cdk.NestedStackProps {
99
poolCacheBucket: aws_s3.Bucket;
1010
poolCacheKey: string;
1111
nodeRPC: string;
12+
nodeRPCUsername: string;
13+
nodeRPCPassword: string;
1214
}
1315
export class RoutingLambdaStack extends cdk.NestedStack {
1416
public readonly routingLambda: aws_lambda_nodejs.NodejsFunction;
@@ -20,7 +22,13 @@ export class RoutingLambdaStack extends cdk.NestedStack {
2022
props: RoutingLambdaStackProps
2123
) {
2224
super(scope, name, props);
23-
const { poolCacheBucket, poolCacheKey, nodeRPC } = props;
25+
const {
26+
poolCacheBucket,
27+
poolCacheKey,
28+
nodeRPC,
29+
nodeRPCUsername,
30+
nodeRPCPassword,
31+
} = props;
2432

2533
const lambdaRole = new aws_iam.Role(this, 'RoutingLambdaRole', {
2634
assumedBy: new aws_iam.ServicePrincipal('lambda.amazonaws.com'),
@@ -59,6 +67,8 @@ export class RoutingLambdaStack extends cdk.NestedStack {
5967
POOL_CACHE_BUCKET: poolCacheBucket.bucketName,
6068
POOL_CACHE_KEY: poolCacheKey,
6169
JSON_RPC_URL: nodeRPC,
70+
JSON_RPC_USERNAME: nodeRPCUsername,
71+
JSON_RPC_PASSWORD: nodeRPCPassword,
6272
},
6373
layers: [
6474
aws_lambda.LayerVersion.fromLayerVersionArn(

cdk.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"app": "npx ts-node bin/app.ts",
2+
"app": "npx ts-node --project=tsconfig.cdk.json bin/app.ts",
33
"context": {
44
"@aws-cdk/core:newStyleStackSynthesis": true
55
}

lib/handlers/handler.ts

+218
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import Joi from '@hapi/joi';
2+
import { metricScope, MetricsLogger } from 'aws-embedded-metrics';
3+
import {
4+
APIGatewayProxyEvent,
5+
APIGatewayProxyHandler,
6+
APIGatewayProxyResult,
7+
Context,
8+
} from 'aws-lambda';
9+
import { default as bunyan, default as Logger } from 'bunyan';
10+
11+
export type BaseRInj = {
12+
log: Logger;
13+
};
14+
15+
export type HandleRequestParams<CInj, RInj, Req> = {
16+
context: Context;
17+
event: APIGatewayProxyEvent;
18+
request: Req;
19+
containerInjected: CInj;
20+
requestInjected: RInj;
21+
};
22+
23+
export type Response<Res> = {
24+
statusCode: number;
25+
body: Res;
26+
};
27+
28+
export abstract class Injector<CInj, RInj extends BaseRInj, Req> {
29+
private containerInjected: CInj;
30+
public constructor() {}
31+
32+
public async build() {
33+
this.containerInjected = await this.buildContainerInjected();
34+
return this;
35+
}
36+
37+
public abstract getRequestInjected(
38+
containerInjected: CInj,
39+
request: Req,
40+
event: APIGatewayProxyEvent,
41+
context: Context,
42+
log: Logger,
43+
metrics: MetricsLogger
44+
): Promise<RInj>;
45+
46+
public abstract buildContainerInjected(): Promise<CInj>;
47+
48+
public async getContainerInjected(): Promise<CInj> {
49+
if (this.containerInjected === undefined) {
50+
throw new Error(
51+
'Container injected undefined. Must call build() before using.'
52+
);
53+
}
54+
return this.containerInjected;
55+
}
56+
}
57+
58+
export abstract class APIGLambdaHandler<CInj, RInj extends BaseRInj, Req, Res> {
59+
constructor(
60+
private handlerName: string,
61+
private injector: Injector<CInj, RInj, Req>
62+
) {}
63+
64+
get handler(): APIGatewayProxyHandler {
65+
return metricScope(
66+
(metric: MetricsLogger) =>
67+
async (
68+
event: APIGatewayProxyEvent,
69+
context: Context
70+
): Promise<APIGatewayProxyResult> => {
71+
let log: Logger = bunyan.createLogger({
72+
name: this.handlerName,
73+
serializers: bunyan.stdSerializers,
74+
level: bunyan.INFO,
75+
requestId: context.awsRequestId,
76+
});
77+
78+
log.info({ event, context }, 'Request started.');
79+
80+
let request: Req;
81+
try {
82+
const requestValidation = await this.parseAndValidateRequest(event);
83+
84+
if (requestValidation.state == 'invalid') {
85+
return requestValidation.errorResponse;
86+
}
87+
88+
request = requestValidation.request;
89+
} catch (err) {
90+
log.error({ err }, 'Unexpected error validating request');
91+
return { statusCode: 500, body: 'Internal error' };
92+
}
93+
94+
const containerInjected = await this.injector.getContainerInjected();
95+
96+
let requestInjected: RInj;
97+
try {
98+
requestInjected = await this.injector.getRequestInjected(
99+
containerInjected,
100+
request,
101+
event,
102+
context,
103+
log,
104+
metric
105+
);
106+
} catch (err) {
107+
log.error({ err, event }, 'Error building request injected.');
108+
return { statusCode: 500, body: 'Internal error' };
109+
}
110+
111+
({ log } = requestInjected);
112+
113+
let statusCode: number;
114+
let body: Res;
115+
116+
log.info({ request }, 'Invoking handle request');
117+
try {
118+
({ statusCode, body } = await this.handleRequest({
119+
context,
120+
event,
121+
request,
122+
containerInjected,
123+
requestInjected,
124+
}));
125+
} catch (err) {
126+
log.error({ err }, 'Unexpected error in handler');
127+
return { statusCode: 500, body: 'Internal error' };
128+
}
129+
130+
// let response: Res;
131+
try {
132+
const responseValidation = await this.parseAndValidateResponse(
133+
body
134+
);
135+
136+
if (responseValidation.state == 'invalid') {
137+
return responseValidation.errorResponse;
138+
}
139+
140+
// response = responseValidation.response;
141+
} catch (err) {
142+
log.error({ err }, 'Unexpected error in handler');
143+
return { statusCode: 500, body: 'Internal error' };
144+
}
145+
146+
return { statusCode, body: JSON.stringify(body) };
147+
}
148+
);
149+
}
150+
151+
public abstract handleRequest(
152+
params: HandleRequestParams<CInj, RInj, Req>
153+
): Promise<Response<Res>>;
154+
155+
protected abstract requestBodySchema(): Joi.ObjectSchema | null;
156+
protected abstract responseBodySchema(): Joi.ObjectSchema | null;
157+
158+
private async parseAndValidateRequest(
159+
event: APIGatewayProxyEvent
160+
): Promise<
161+
| { state: 'valid'; request: Req }
162+
| { state: 'invalid'; errorResponse: APIGatewayProxyResult }
163+
> {
164+
let body: any;
165+
try {
166+
body = JSON.parse(event.body ?? '');
167+
} catch (err) {
168+
return { state: 'invalid', errorResponse: { statusCode: 422, body: '' } };
169+
}
170+
171+
const bodySchema = this.requestBodySchema();
172+
173+
if (!bodySchema) {
174+
return { state: 'valid', request: body as Req };
175+
}
176+
177+
const res = bodySchema.validate(body, {
178+
allowUnknown: true, // Makes API schema changes and rollbacks easier.
179+
stripUnknown: true,
180+
});
181+
182+
if (res.error) {
183+
return {
184+
state: 'invalid',
185+
errorResponse: { statusCode: 400, body: res.error.message },
186+
};
187+
}
188+
189+
return { state: 'valid', request: res.value as Req };
190+
}
191+
192+
private async parseAndValidateResponse(
193+
body: Res
194+
): Promise<
195+
| { state: 'valid'; response: Res }
196+
| { state: 'invalid'; errorResponse: APIGatewayProxyResult }
197+
> {
198+
const responseSchema = this.responseBodySchema();
199+
200+
if (!responseSchema) {
201+
return { state: 'valid', response: body as Res };
202+
}
203+
204+
const res = responseSchema.validate(body, {
205+
allowUnknown: true,
206+
stripUnknown: true, // Ensure no unexpected fields returned to users.
207+
});
208+
209+
if (res.error) {
210+
return {
211+
state: 'invalid',
212+
errorResponse: { statusCode: 500, body: 'Internal error' },
213+
};
214+
}
215+
216+
return { state: 'valid', response: res.value as Res };
217+
}
218+
}

lib/handlers/index.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1-
import { quoteHandler } from './quote/quote';
1+
import { QuoteHandler } from './quote/quote';
2+
import { QuoteHandlerInjector } from "./quote/injector";
23

3-
module.exports = { quoteHandler };
4+
const quoteHandlerInjector = new QuoteHandlerInjector();
5+
quoteHandlerInjector.build();
6+
const quoteHandler = new QuoteHandler('quote', quoteHandlerInjector);
7+
8+
module.exports = { quoteHandler: quoteHandler.handler };

0 commit comments

Comments
 (0)