Making progress

This commit is contained in:
Martin Donnelly 2024-08-07 00:34:05 +01:00
parent 81d7281e00
commit 0281a1fd49
8 changed files with 1644 additions and 109 deletions

View File

@ -16,7 +16,7 @@ Your assignment is to implement a function that is subscribed to these events an
You will implement the function that is subscribed to the event stream. The event stream contains events of different types, your function should publish `booking_completed` events to the external system. You will implement the function that is subscribed to the event stream. The event stream contains events of different types, your function should publish `booking_completed` events to the external system.
The external system accepts these events in a format defined in the enclosed [JSON Schema](./external-service/schema.json). Your function will need to transform events from the stream into this format before publishing. The external system accepts these events in a format defined in the enclosed [JSON Product](./external-service/schema.json). Your function will need to transform events from the stream into this format before publishing.
Infrastructure and build tools have been provided, so you can concentrate on the code for the function. Infrastructure and build tools have been provided, so you can concentrate on the code for the function.
@ -26,7 +26,7 @@ You are welcome to install any additional packages from NPM to help you complete
- Implement the assignment using JavaScript or TypeScript. Under [src](./src) there are two entry points, `index.js` and `index.ts` - you must delete the one you don't plan to use for your code to build correctly. - Implement the assignment using JavaScript or TypeScript. Under [src](./src) there are two entry points, `index.js` and `index.ts` - you must delete the one you don't plan to use for your code to build correctly.
- Your function should pick out `booking_completed` events and ignore other event types - Your function should pick out `booking_completed` events and ignore other event types
- Your function should transform events into the format defined in the [JSON Schema](./external-service/schema.json) - Your function should transform events into the format defined in the [JSON Product](./external-service/schema.json)
- Your function should publish events to the [Mock Server](#mock-server) - Your function should publish events to the [Mock Server](#mock-server)
- Your function should have 100% test coverage, by adding tests under [test](./test). - Your function should have 100% test coverage, by adding tests under [test](./test).
- File names for tests should end with `.test.ts` or `.test.js` to be picked up by the test runner. - File names for tests should end with `.test.ts` or `.test.js` to be picked up by the test runner.

1401
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,8 @@
"build": "tsup", "build": "tsup",
"start:server": "node external-service", "start:server": "node external-service",
"invoke": "node app", "invoke": "node app",
"test": "vitest" "test": "vitest",
"compile-schemas": "json2ts -i ./external-service/schema.json -o src/product.ts"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "10.17.27", "@types/node": "10.17.27",
@ -16,6 +17,7 @@
"eslint-config-prettier": "7.2.0", "eslint-config-prettier": "7.2.0",
"eslint-plugin-prettier": "3.3.1", "eslint-plugin-prettier": "3.3.1",
"eslint-plugin-unused-imports": "1.0.1", "eslint-plugin-unused-imports": "1.0.1",
"json-schema-to-typescript": "^15.0.0",
"prettier": "2.2.1", "prettier": "2.2.1",
"source-map-support": "0.5.16", "source-map-support": "0.5.16",
"ts-loader": "9.4.1", "ts-loader": "9.4.1",

View File

@ -1,82 +1,13 @@
import { KinesisStreamEvent } from 'aws-lambda'; import { KinesisStreamEvent } from 'aws-lambda';
import { KinesisStreamRecord } from 'aws-lambda/trigger/kinesis-stream'; import { Processor } from './processor';
/*
Making an assumption here that the eventblock layout is fairly static and that
object fields such as booking_requested will only exist in the booking event,
and other fields may be substituted in for other event types.
*/
export interface EventBlock {
id: string;
partitionKey: string;
timestamp: number;
type: string;
booking_requested?: BookingRequested;
}
export interface BookingRequested {
timestamp: number;
orderId: number;
product_provider: string;
}
export const handler = (event: KinesisStreamEvent) => { export const handler = (event: KinesisStreamEvent) => {
console.log('!!'); console.log('!!');
// console.log(event); // console.log(event);
const processor = new Processor();
for (const elm of event.Records) { for (const elm of event.Records) {
console.log(elm); processor.processEvent(elm);
console.log('>-<');
processEvent(elm);
} }
}; };
function processEvent(event: KinesisStreamRecord) {
console.log(JSON.stringify(event));
let data: EventBlock | null = decodePayload(event.kinesis.data);
console.log('DecodePayload:: ', JSON.stringify(data));
if (data === null) {
return null;
}
if (data.hasOwnProperty('type')) {
switch (data.type) {
case 'booking_requested':
console.log('booking_requested');
transformPayload(data);
break;
default:
console.log('Default');
}
}
}
function decodePayload(data: any): EventBlock | null {
if (data === null) {
return null;
}
console.log('-----');
console.log(data);
console.log(atob(data));
console.log('-----');
try {
const eventBlock: EventBlock = JSON.parse(atob(data));
return eventBlock;
} catch (err) {
console.error(err);
return null;
}
}
function transformPayload(data: EventBlock) {
console.log('Transform and roll out');
}

View File

@ -0,0 +1,68 @@
import { KinesisStreamRecord } from 'aws-lambda/trigger/kinesis-stream';
import { EventBlock } from './structs';
import { Product } from './product';
export class Processor {
public processEvent(event: KinesisStreamRecord) {
// console.log(JSON.stringify(event));
let data: EventBlock | null = this.decodePayload(event.kinesis.data);
// console.log('DecodePayload:: ', JSON.stringify(data));
if (!data) {
return null;
}
// console.log('!!data.type:', data.type);
switch (data.type) {
case 'booking_requested':
// console.log('booking_requested');
const product: Product | null = this.transformPayload(data);
console.log('Product:', JSON.stringify(product));
this.sendPayload(product);
break;
default:
}
}
public decodePayload(data: any): EventBlock | null {
if (data === null || data === '') {
return null;
}
try {
return JSON.parse(atob(data));
} catch (err) {
console.error(err);
return null;
}
}
public transformPayload(data: EventBlock): Product | null {
console.log('Transform and roll out');
console.log('Data:', JSON.stringify(data));
const { booking_requested } = data;
if (booking_requested) {
const timestamp = new Date(booking_requested.timestamp);
const newProduct: Product = {
product_order_id_buyer: booking_requested['orderId'],
timestamp: timestamp.toISOString(),
product_provider_buyer: booking_requested['product_provider'],
};
return newProduct;
}
return null;
}
public sendPayload(data: Product | null): boolean {
console.log('Sendpayload', data);
return false;
}
}

16
src/product.ts Normal file
View File

@ -0,0 +1,16 @@
export interface Product {
/**
* A unique reference ID for the buyer's order
*/
product_order_id_buyer: number;
/**
* A timestamp describing when the event occurred, in ISO 8601 format
*/
timestamp: string;
/**
* The seller providing the product
*/
product_provider_buyer: string;
}

19
src/structs.ts Normal file
View File

@ -0,0 +1,19 @@
/*
Making an assumption here that the eventblock layout is fairly static and that
object fields such as booking_requested will only exist in the booking event,
and other fields may be substituted in for other event types.
*/
export interface EventBlock {
id: string;
partitionKey: string;
timestamp: number;
type: string;
booking_requested?: BookingRequested;
}
export interface BookingRequested {
timestamp: number;
orderId: number;
product_provider: string;
}

View File

@ -1,7 +1,7 @@
import { Processor } from '../src/processor';
describe('myTSTest', () => { describe('myTSTest', () => {
it('should pass', () => { const processor = new Processor();
expect(1).toBe(1);
});
describe('decodePayload', () => { describe('decodePayload', () => {
const properDataBase64 = const properDataBase64 =
@ -18,26 +18,164 @@ describe('myTSTest', () => {
}, },
}; };
it('should pass', () => { it('should pass with proper data', () => {
expect().toEqual(properDecodedData); expect(processor.decodePayload(properDataBase64)).toEqual(
properDecodedData
);
});
it('should return null using an empty string', () => {
expect(processor.decodePayload('')).toBeNull();
});
it('should return null when using non base64', () => {
expect(processor.decodePayload('This is not in base64')).toBeNull();
}); });
}); });
describe('processEvent', () => { describe('processEvent', () => {
it('should pass', () => { const kinesisData = {
expect(1).toBe(1); kinesis: {
data:
'eyJpZCI6ImE0MzgwYzAyLTE0OTItMTFlYy1hMGIyLWM3OGZmYmQ2OTM0NyIsInBhcnRpdGlvbktleSI6ImRhZDNiODMwLTAyOWQtNGZlMy1iMDJkLTc1MDFjOTc4NzYwOCIsInRpbWVzdGFtcCI6MTYzMTUzODA1OTQ1NiwidHlwZSI6ImJvb2tpbmdfY29tcGxldGVkIiwiYm9va2luZ19jb21wbGV0ZWQiOnsidGltZXN0YW1wIjoxNjMxNTM4MDU5NDU2LCJwcm9kdWN0X3Byb3ZpZGVyIjoiQnJpdHRhbnkgRmVycmllcyIsIm9yZGVySWQiOjEyMzQ3OH19',
partitionKey: 'dad3b830-029d-4fe3-b02d-7501c9787608',
approximateArrivalTimestamp: 1631538059456,
kinesisSchemaVersion: '1.0',
sequenceNumber: 'dad3b830-029d-4fe3-b02d-7501c9787608',
},
eventSource: 'aws:kinesis',
eventID:
'shardId-000000000000:49545115243490985018280067714973144582180062593244200961',
invokeIdentityArn: 'arn:aws:iam::EXAMPLE',
eventVersion: '1.0',
eventName: 'aws:kinesis:record',
eventSourceARN: 'arn:aws:kinesis:EXAMPLE',
awsRegion: 'us-east-1',
};
const brokenData = {
kinesis: {
data: 'Data that will not decode',
partitionKey: 'dad3b830-029d-4fe3-b02d-7501c9787608',
approximateArrivalTimestamp: 1631538059456,
kinesisSchemaVersion: '1.0',
sequenceNumber: 'dad3b830-029d-4fe3-b02d-7501c9787608',
},
};
const emptyData = {
kinesis: {
data: '',
partitionKey: 'dad3b830-029d-4fe3-b02d-7501c9787608',
approximateArrivalTimestamp: 1631538059456,
kinesisSchemaVersion: '1.0',
sequenceNumber: 'dad3b830-029d-4fe3-b02d-7501c9787608',
},
};
// This data block has a type:"booking_notcompleted"
const dataWrongTypeData = {
kinesis: {
data:
'eyJpZCI6ImE0MzgwYzAyLTE0OTItMTFlYy1hMGIyLWM3OGZmYmQ2OTM0NyIsInBhcnRpdGlvbktleSI6ImRhZDNiODMwLTAyOWQtNGZlMy1iMDJkLTc1MDFjOTc4NzYwOCIsInRpbWVzdGFtcCI6MTYzMTUzODA1OTQ1NiwidHlwZSI6ImJvb2tpbmdfbm90Y29tcGxldGVkIiwiYm9va2luZ19jb21wbGV0ZWQiOnsidGltZXN0YW1wIjoxNjMxNTM4MDU5NDU2LCJwcm9kdWN0X3Byb3ZpZGVyIjoiQnJpdHRhbnkgRmVycmllcyIsIm9yZGVySWQiOjEyMzQ3OH19',
partitionKey: 'dad3b830-029d-4fe3-b02d-7501c9787608',
approximateArrivalTimestamp: 1631538059456,
kinesisSchemaVersion: '1.0',
sequenceNumber: 'dad3b830-029d-4fe3-b02d-7501c9787608',
},
};
const decodedData = {
id: 'a4380c02-1492-11ec-a0b2-c78ffbd69347',
partitionKey: 'dad3b830-029d-4fe3-b02d-7501c9787608',
timestamp: 1631538059456,
type: 'booking_completed',
booking_completed: {
timestamp: 1631538059456,
product_provider: 'Brittany Ferries',
orderId: 123478,
},
};
const improperKinesisData = Object.assign({}, kinesisData, brokenData);
const emptyKinesisData = Object.assign({}, kinesisData, emptyData);
const wrongTypeKinesisData = Object.assign(
{},
kinesisData,
dataWrongTypeData
);
it('should pass with proper data', () => {
expect(processor.processEvent(kinesisData)).toBeUndefined();
});
it('should fail on data that did not decode correctly', () => {
expect(processor.processEvent(improperKinesisData)).toBeNull();
});
it('should fail on on empty data', () => {
expect(processor.processEvent(emptyKinesisData)).toBeNull();
});
it('should safely handle proper data but with an unwanted type', () => {
expect(processor.processEvent(wrongTypeKinesisData)).toBeUndefined();
});
// Need this to get 100% coverage on the case statement
it('cover all case steps', () => {
const allTypes = ['booking_requested', 'not_a_case_option'];
for (const testCase of allTypes) {
const workData = Object.assign({}, decodedData, { type: testCase });
const workBlob = btoa(JSON.stringify(workData));
const tempKinesisData = Object.assign({}, kinesisData, {
kinesis: {
data: workBlob,
},
});
expect(processor.processEvent(tempKinesisData)).toBeUndefined();
}
}); });
}); });
describe('transformPayload', () => { describe('transformPayload', () => {
const validData = {
id: 'a4388132-1492-11ec-a0b2-c78ffbd69347',
partitionKey: '2fce53f1-1daf-4247-9245-0ad78d0b0d2e',
timestamp: 1631538059459,
type: 'booking_requested',
booking_requested: {
timestamp: 1631538059459,
orderId: 10017,
product_provider: 'Stena Line',
},
};
const invalidData = {
id: 'a4388132-1492-11ec-a0b2-c78ffbd69347',
partitionKey: '2fce53f1-1daf-4247-9245-0ad78d0b0d2e',
timestamp: 1631538059459,
type: 'booking_NOT_requested',
booking_NOT_requested: {
timestamp: 1631538059459,
orderId: 10017,
product_provider: 'Stena Square',
},
};
const validTransformed = {
product_order_id_buyer: 10017,
timestamp: '2021-09-13T13:00:59.459Z',
product_provider_buyer: 'Stena Line',
};
it('should pass', () => { it('should pass', () => {
expect(1).toBe(1); expect(processor.transformPayload(validData)).toEqual(validTransformed);
});
}); });
describe('handler', () => { it('should return a null for an item which does not contain a booking_request', () => {
it('should pass', () => { expect(processor.transformPayload(invalidData)).toBeNull();
expect(1).toBe(1);
}); });
}); });
}); });