Making progress
This commit is contained in:
parent
81d7281e00
commit
0281a1fd49
@ -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.
|
||||
|
||||
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.
|
||||
|
||||
@ -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.
|
||||
- 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 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.
|
||||
|
1401
package-lock.json
generated
1401
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,8 @@
|
||||
"build": "tsup",
|
||||
"start:server": "node external-service",
|
||||
"invoke": "node app",
|
||||
"test": "vitest"
|
||||
"test": "vitest",
|
||||
"compile-schemas": "json2ts -i ./external-service/schema.json -o src/product.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "10.17.27",
|
||||
@ -16,6 +17,7 @@
|
||||
"eslint-config-prettier": "7.2.0",
|
||||
"eslint-plugin-prettier": "3.3.1",
|
||||
"eslint-plugin-unused-imports": "1.0.1",
|
||||
"json-schema-to-typescript": "^15.0.0",
|
||||
"prettier": "2.2.1",
|
||||
"source-map-support": "0.5.16",
|
||||
"ts-loader": "9.4.1",
|
||||
|
77
src/index.ts
77
src/index.ts
@ -1,82 +1,13 @@
|
||||
import { KinesisStreamEvent } from 'aws-lambda';
|
||||
import { KinesisStreamRecord } from 'aws-lambda/trigger/kinesis-stream';
|
||||
|
||||
/*
|
||||
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;
|
||||
}
|
||||
import { Processor } from './processor';
|
||||
|
||||
export const handler = (event: KinesisStreamEvent) => {
|
||||
console.log('!!');
|
||||
// console.log(event);
|
||||
|
||||
const processor = new Processor();
|
||||
|
||||
for (const elm of event.Records) {
|
||||
console.log(elm);
|
||||
console.log('>-<');
|
||||
processEvent(elm);
|
||||
processor.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');
|
||||
|
||||
|
||||
}
|
@ -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
16
src/product.ts
Normal 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
19
src/structs.ts
Normal 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;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { Processor } from '../src/processor';
|
||||
|
||||
describe('myTSTest', () => {
|
||||
it('should pass', () => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
const processor = new Processor();
|
||||
|
||||
describe('decodePayload', () => {
|
||||
const properDataBase64 =
|
||||
@ -18,26 +18,164 @@ describe('myTSTest', () => {
|
||||
},
|
||||
};
|
||||
|
||||
it('should pass', () => {
|
||||
expect().toEqual(properDecodedData);
|
||||
it('should pass with proper data', () => {
|
||||
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', () => {
|
||||
it('should pass', () => {
|
||||
expect(1).toBe(1);
|
||||
const kinesisData = {
|
||||
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', () => {
|
||||
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', () => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
expect(processor.transformPayload(validData)).toEqual(validTransformed);
|
||||
});
|
||||
|
||||
describe('handler', () => {
|
||||
it('should pass', () => {
|
||||
expect(1).toBe(1);
|
||||
it('should return a null for an item which does not contain a booking_request', () => {
|
||||
expect(processor.transformPayload(invalidData)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user