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.
|
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
1401
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
77
src/index.ts
77
src/index.ts
@ -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');
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -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', () => {
|
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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user