Skip to content

Commit 8f2f039

Browse files
authored
Merge pull request #16 from Virtual-Finland-Development/feat/url-redirector-for-testbed-gw
feat: add uri redirector for special request paths
2 parents 88be474 + e08ae5e commit 8f2f039

11 files changed

Lines changed: 163 additions & 56 deletions

File tree

infra/package-lock.json

Lines changed: 14 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

infra/resources/CloudFront.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function createCloudFrontDistribution(
3030
],
3131
customErrorResponses: [],
3232
defaultCacheBehavior: {
33-
allowedMethods: ['GET', 'HEAD', 'OPTIONS'],
33+
allowedMethods: ['HEAD', 'DELETE', 'POST', 'GET', 'OPTIONS', 'PUT', 'PATCH'],
3434
cachedMethods: ['GET', 'HEAD'],
3535
targetOriginId: bucket.arn,
3636
viewerProtocolPolicy: 'redirect-to-https',

infra/resources/S3Bucket.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default function createS3Bucket(setup: ISetup) {
1818
corsRules: [
1919
{
2020
allowedHeaders: ['*'],
21-
allowedMethods: ['GET'],
21+
allowedMethods: ['GET', 'POST'],
2222
allowedOrigins: ['*'],
2323
},
2424
],

src/app.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,15 @@ test('Basic router test', async () => {
1717
],
1818
} as CloudFrontRequestEvent;
1919
*/
20+
21+
// Mock event
2022
const event = {
2123
rawPath: '/resources/bazz',
24+
requestContext: {
25+
http: {
26+
method: 'GET',
27+
},
28+
},
2229
} as unknown as APIGatewayProxyEventV2;
2330

2431
const result = (await offlineHandler(event)) as unknown as APIGatewayProxyStructuredResultV2;

src/app.ts

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,19 @@ import {
77
} from 'aws-lambda';
88
import mime from 'mime';
99
import { InternalResources } from './resources/index';
10+
import { resolveError, resolveUri } from './utils/api';
1011
import { getResource, listResources } from './utils/data/repositories/ResourceRepository';
11-
import { cutTooLongString, generateSimpleHash } from './utils/helpers';
12+
import { cutTooLongString, decodeBase64, generateSimpleHash, parseRequestInputParams } from './utils/helpers';
1213
import { storeToS3 } from './utils/lib/S3Bucket';
1314
import { Environment, getInternalResourceInfo } from './utils/runtime';
1415

1516
async function engageResourcesRouter(
1617
resourceURI: string,
17-
queryParams: string
18+
params: Record<string, string>
1819
): Promise<{
1920
response: CloudFrontResultResponse | undefined;
2021
cacheable?: { filepath: string; data: string; mime: string };
2122
}> {
22-
const urlParams = Object.fromEntries(new URLSearchParams(queryParams || ''));
2323
const resourceName = resourceURI.replace('/resources', '').replace('/', ''); // First occurence of "/" is removed
2424
if (!resourceName) {
2525
// On a requets path /resources, return a list of resources
@@ -35,15 +35,15 @@ async function engageResourcesRouter(
3535
const resource = getResource(resourceName);
3636
if (resource) {
3737
console.log('Resource: ', resource.name);
38-
const resourceResponse = await resource.retrieve(urlParams);
38+
const resourceResponse = await resource.retrieve(params);
3939

4040
// If resource size is larger than 1MB, store it in S3 and redirect to it instead
4141
// This is a workaround to avoid CloudFront cache limit
4242
// @see: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/edge-functions-restrictions.html#lambda-at-edge-function-restrictions
4343
console.log('Size: ', resourceResponse.size, 'bytes');
4444
if (resourceResponse.size > 1024 * 1024) {
4545
const extension = mime.getExtension(resourceResponse.mime);
46-
const cachedName = `/cached/${resource.name}-${generateSimpleHash(urlParams)}.${extension}`;
46+
const cachedName = `/cached/${resource.name}-${generateSimpleHash(params)}.${extension}`;
4747

4848
return {
4949
response: undefined,
@@ -96,15 +96,26 @@ async function engageResourcesRouter(
9696
export async function handler(event: CloudFrontRequestEvent): Promise<CloudFrontRequestResult> {
9797
try {
9898
const request = event.Records[0].cf.request;
99-
let uri = request.uri;
100-
const queryParams = request.querystring;
99+
let uri = resolveUri(request.uri);
100+
const params = Object.fromEntries(new URLSearchParams(request.querystring || ''));
101+
if (request.method === 'POST' && typeof request.body?.data === 'string') {
102+
try {
103+
const body = JSON.parse(decodeBase64(request.body.data));
104+
if (typeof body === 'object') {
105+
Object.assign(params, parseRequestInputParams(body));
106+
}
107+
} catch (error) {
108+
//
109+
}
110+
}
111+
101112
console.log('Request: ', {
102113
uri,
103-
queryParams,
114+
params,
104115
});
105116

106117
if (uri.startsWith('/resources')) {
107-
const routerResponse = await engageResourcesRouter(uri, queryParams);
118+
const routerResponse = await engageResourcesRouter(uri, params);
108119
if (routerResponse.response) {
109120
console.log('Response: ', {
110121
...routerResponse.response,
@@ -130,14 +141,30 @@ export async function handler(event: CloudFrontRequestEvent): Promise<CloudFront
130141
}
131142
}
132143

144+
if (request.method === 'POST') {
145+
// If the request is POST, we need to use a redirect for the cache to work
146+
return {
147+
status: '302',
148+
statusDescription: 'Found',
149+
headers: {
150+
location: [
151+
{
152+
key: 'Location',
153+
value: uri,
154+
},
155+
],
156+
},
157+
};
158+
}
159+
133160
request.uri = uri; // Pass through to origin
134161
return request;
135162
} catch (error: any) {
136-
console.log(error?.message, error?.stack);
163+
const errorPackage = resolveError(error);
137164
return {
138-
status: '500',
139-
statusDescription: 'Internal Server Error',
140-
body: JSON.stringify({ message: error?.message }),
165+
status: errorPackage.statusCode.toString(),
166+
statusDescription: errorPackage.description,
167+
body: errorPackage.body,
141168
};
142169
}
143170
}
@@ -153,15 +180,26 @@ export async function offlineHandler(event: APIGatewayProxyEventV2): Promise<API
153180
Environment.isLocal = true;
154181

155182
const handle = async (event: APIGatewayProxyEventV2) => {
156-
let uri = event.rawPath;
157-
const queryParams = event.rawQueryString;
183+
let uri = resolveUri(event.rawPath);
184+
const params = Object.fromEntries(new URLSearchParams(event.rawQueryString || ''));
185+
if (event.requestContext.http.method === 'POST') {
186+
try {
187+
const body = JSON.parse(event.body || '{}');
188+
if (typeof body === 'object') {
189+
Object.assign(params, parseRequestInputParams(body));
190+
}
191+
} catch (error) {
192+
//
193+
}
194+
}
195+
158196
console.log('Request: ', {
159197
uri,
160-
queryParams,
198+
params,
161199
});
162200

163201
if (uri.startsWith('/resources')) {
164-
const routerResponse: any = await engageResourcesRouter(uri, queryParams);
202+
const routerResponse: any = await engageResourcesRouter(uri, params);
165203
if (routerResponse.response) {
166204
return {
167205
statusCode: parseInt(routerResponse.response.status),
@@ -209,10 +247,6 @@ export async function offlineHandler(event: APIGatewayProxyEventV2): Promise<API
209247

210248
return await Promise.race([handle(event), internalTimeoutEnforcer() as any]); // Return the first to resolve
211249
} catch (error: any) {
212-
console.log(error?.message, error?.stack);
213-
return {
214-
statusCode: 500,
215-
body: JSON.stringify({ message: error?.message }),
216-
};
250+
return resolveError(error);
217251
}
218252
}

src/resources/internal/BusinessFinlandEscoOccupations.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import InternalResource from '../../utils/data/models/InternalResource';
22
import { getOutput } from '../../utils/data/parsers';
33
import { getPaginationParams } from '../../utils/filters';
44

5+
import BusinessFinlandDataSet from './business-finland-esco-v1_1_1-occupations.json';
6+
57
interface Occupation {
68
escoCode: string;
79
escoJobTitle: string;
@@ -35,4 +37,10 @@ export default new InternalResource({
3537
return getOutput()<OccupationsResponse>(data);
3638
},
3739
},
40+
dataGetter() {
41+
return Promise.resolve({
42+
data: JSON.stringify(BusinessFinlandDataSet),
43+
mime: 'application/json',
44+
});
45+
},
3846
});

src/utils/api.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const UriRedirects: Record<string, string> = {
2+
'/productizer/draft/Employment/EscoOccupations': '/resources/BusinessFinlandEscoOccupations',
3+
};
4+
5+
export function resolveUri(uri: string): string {
6+
return UriRedirects[uri] || uri;
7+
}
8+
9+
export function resolveError(error: Error): { statusCode: number; body: string; description: string } {
10+
const errorPackage = resolveErrorPackage(error);
11+
console.error('Error: ', errorPackage);
12+
return errorPackage;
13+
}
14+
15+
function resolveErrorPackage(error: Error): { statusCode: number; body: string; description: string } {
16+
return {
17+
statusCode: 500,
18+
description: 'Internal Server Error',
19+
body: error.message || 'Internal Server Error',
20+
};
21+
}

src/utils/data/models/InternalResource.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,44 @@ import { retrieveFromS3 } from '../../lib/S3Bucket';
33
import { Environment, getInternalResourceInfo } from '../../runtime';
44
import BaseResource from './internal/BaseResource';
55

6+
const inMemoryCache: Record<
7+
string,
8+
{
9+
data: string;
10+
mime: string;
11+
}
12+
> = {};
13+
614
export default class InternalResource extends BaseResource {
715
public type = 'internal';
816

917
protected async _retrieveDataPackage(): Promise<{
1018
data: string;
1119
mime: string;
1220
}> {
21+
if (typeof inMemoryCache[this.uri] !== 'undefined') {
22+
console.log('Using in-memory cache for resource');
23+
return inMemoryCache[this.uri];
24+
}
25+
1326
const fileName = this.uri;
1427

1528
if (Environment.isLocal) {
1629
const resource = await InternalResources.getResourcePassThrough(fileName);
1730
if (resource) {
18-
return {
31+
inMemoryCache[this.uri] = {
1932
data: resource.body,
2033
mime: resource.mime || 'application/json',
2134
};
35+
36+
return inMemoryCache[this.uri];
2237
}
2338
throw new Error(`Resource ${fileName} not found`);
2439
}
2540

2641
const bucketName = getInternalResourceInfo().name;
2742
const bucketKey = `resources/${fileName}`;
28-
return retrieveFromS3(bucketName, bucketKey);
43+
inMemoryCache[this.uri] = await retrieveFromS3(bucketName, bucketKey);
44+
return inMemoryCache[this.uri];
2945
}
3046
}

src/utils/data/models/internal/BaseResource.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ export default class BaseResource implements IResource {
2020
input?: (data: string) => unknown;
2121
transform?: (data: unknown, params: Record<string, string>) => Promise<unknown>;
2222
output?: (data: unknown) => string;
23-
} = {};
23+
};
2424

2525
protected _dataGetter: ((params: Record<string, string>) => Promise<{ data: string; mime: string }>) | undefined;
26+
protected _settings: Record<string, string | number | boolean>;
2627

2728
constructor({
2829
name,
@@ -31,6 +32,7 @@ export default class BaseResource implements IResource {
3132
type,
3233
dataGetter,
3334
parsers,
35+
settings,
3436
}: {
3537
name: string;
3638
type?: 'external' | 'library' | 'internal';
@@ -42,34 +44,31 @@ export default class BaseResource implements IResource {
4244
transform?: (data: unknown, params: Record<string, string>) => Promise<unknown>; // Data intake -> transformed data
4345
output?: (data: unknown) => string; // Transformed data intake -> raw output data
4446
};
47+
settings?: Record<string, string | number | boolean>;
4548
}) {
4649
this.name = name;
4750
this.uri = uri || '';
4851
this.type = type || this.type;
4952
this._mime = mime;
5053
this._dataGetter = dataGetter;
5154
this._parsers = parsers || {};
55+
this._settings = settings || {};
5256
}
5357

5458
public async retrieve(params: Record<string, string>): Promise<{
5559
data: string;
5660
mime: string;
5761
size: number;
5862
}> {
59-
try {
60-
this.validateSelf();
61-
const dataPackage = await this._retrieveDataPackage(params);
62-
const mime = this._mime || dataPackage.mime;
63-
const finalData = await this._parseRawData(dataPackage.data, mime, params);
64-
return {
65-
data: finalData,
66-
mime: mime,
67-
size: Buffer.byteLength(finalData, 'utf8'),
68-
};
69-
} catch (error) {
70-
console.log(error);
71-
throw error;
72-
}
63+
this.validateSelf();
64+
const dataPackage = await this._retrieveDataPackage(params);
65+
const mime = this._mime || dataPackage.mime;
66+
const finalData = await this._parseRawData(dataPackage.data, mime, params);
67+
return {
68+
data: finalData,
69+
mime: mime,
70+
size: Buffer.byteLength(finalData, 'utf8'),
71+
};
7372
}
7473

7574
private validateSelf(): void {

0 commit comments

Comments
 (0)