Skip to content

Commit 9600fe0

Browse files
committed
add builder and serializer to base resource model
1 parent 11659ea commit 9600fe0

File tree

4 files changed

+151
-47
lines changed

4 files changed

+151
-47
lines changed

src/callback.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import { v4 as uuidv4 } from 'uuid';
22
import CloudFormation from 'aws-sdk/clients/cloudformation';
33

4-
import {
5-
SessionProxy,
6-
} from './proxy';
7-
import { BaseResourceModel, OperationStatus, Response } from './interface';
4+
import { SessionProxy } from './proxy';
5+
import { BaseResourceModel, CfnResponse, OperationStatus } from './interface';
86

97

10-
const LOG = console;
8+
const LOGGER = console;
119

12-
interface ProgressOptions extends Response<BaseResourceModel> {
10+
interface ProgressOptions extends CfnResponse<BaseResourceModel> {
1311
session: SessionProxy,
1412
currentOperationStatus?: OperationStatus,
1513
}
@@ -46,6 +44,6 @@ export async function reportProgress(options: ProgressOptions): Promise<void> {
4644
if (response['ResponseMetadata']) {
4745
requestId = response.ResponseMetadata.RequestId;
4846
}
49-
LOG.info(`Record Handler Progress with Request Id ${requestId} and Request: ${request}`);
47+
LOGGER.debug(`Record Handler Progress with Request Id ${requestId} and Request: ${JSON.stringify(request)}`);
5048
}
5149
}

src/interface.ts

Lines changed: 74 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {
33
LogicalResourceId,
44
NextToken,
55
} from 'aws-sdk/clients/cloudformation';
6+
import { allArgsConstructor, builder } from 'tombok';
7+
68

79
export type Optional<T> = T | undefined | null;
810

@@ -11,46 +13,46 @@ export interface Callable<R extends Array<any>, T> {
1113
}
1214

1315
export enum Action {
14-
Create = "CREATE",
15-
Read = "READ",
16-
Update = "UPDATE",
17-
Delete = "DELETE",
18-
List = "LIST",
16+
Create = 'CREATE',
17+
Read = 'READ',
18+
Update = 'UPDATE',
19+
Delete = 'DELETE',
20+
List = 'LIST',
1921
}
2022

2123
export enum StandardUnit {
22-
Count = "Count",
23-
Milliseconds = "Milliseconds",
24+
Count = 'Count',
25+
Milliseconds = 'Milliseconds',
2426
}
2527

2628
export enum MetricTypes {
27-
HandlerException = "HandlerException",
28-
HandlerInvocationCount = "HandlerInvocationCount",
29-
HandlerInvocationDuration = "HandlerInvocationDuration",
29+
HandlerException = 'HandlerException',
30+
HandlerInvocationCount = 'HandlerInvocationCount',
31+
HandlerInvocationDuration = 'HandlerInvocationDuration',
3032
}
3133

3234
export enum OperationStatus {
33-
Pending = "PENDING",
34-
InProgress = "IN_PROGRESS",
35-
Success = "SUCCESS",
36-
Failed = "FAILED",
35+
Pending = 'PENDING',
36+
InProgress = 'IN_PROGRESS',
37+
Success = 'SUCCESS',
38+
Failed = 'FAILED',
3739
}
3840

3941
export enum HandlerErrorCode {
40-
NotUpdatable = "NotUpdatable",
41-
InvalidRequest = "InvalidRequest",
42-
AccessDenied = "AccessDenied",
43-
InvalidCredentials = "InvalidCredentials",
44-
AlreadyExists = "AlreadyExists",
45-
NotFound = "NotFound",
46-
ResourceConflict = "ResourceConflict",
47-
Throttling = "Throttling",
48-
ServiceLimitExceeded = "ServiceLimitExceeded",
49-
NotStabilized = "NotStabilized",
50-
GeneralServiceException = "GeneralServiceException",
51-
ServiceInternalError = "ServiceInternalError",
52-
NetworkFailure = "NetworkFailure",
53-
InternalFailure = "InternalFailure",
42+
NotUpdatable = 'NotUpdatable',
43+
InvalidRequest = 'InvalidRequest',
44+
AccessDenied = 'AccessDenied',
45+
InvalidCredentials = 'InvalidCredentials',
46+
AlreadyExists = 'AlreadyExists',
47+
NotFound = 'NotFound',
48+
ResourceConflict = 'ResourceConflict',
49+
Throttling = 'Throttling',
50+
ServiceLimitExceeded = 'ServiceLimitExceeded',
51+
NotStabilized = 'NotStabilized',
52+
GeneralServiceException = 'GeneralServiceException',
53+
ServiceInternalError = 'ServiceInternalError',
54+
NetworkFailure = 'NetworkFailure',
55+
InternalFailure = 'InternalFailure',
5456
}
5557

5658
export interface Credentials {
@@ -59,27 +61,59 @@ export interface Credentials {
5961
sessionToken: string;
6062
}
6163

62-
export interface RequestContext<CallbackT> {
64+
export interface RequestContext<T> {
6365
invocation: number;
64-
callbackContext: CallbackT;
66+
callbackContext: T;
6567
cloudWatchEventsRuleName: string;
6668
cloudWatchEventsTargetId: string;
6769
}
6870

69-
export interface BaseResourceModel {
70-
serialize(): Map<string, any>;
71-
deserialize(): BaseResourceModel;
71+
@builder
72+
@allArgsConstructor
73+
export class BaseResourceModel {
74+
['constructor']: typeof BaseResourceModel;
75+
protected static readonly TYPE_NAME?: string;
76+
77+
constructor(...args: any[]) {}
78+
public static builder() {}
79+
80+
public getTypeName(): string {
81+
return Object.getPrototypeOf(this).constructor.TYPE_NAME;
82+
}
83+
84+
public serialize(): Map<string, any> {
85+
const data: Map<string, any> = new Map<string, any>(Object.entries(this));
86+
data.forEach((value: any, key: string) => {
87+
if (value == null) {
88+
data.delete(key);
89+
}
90+
});
91+
return data;
92+
}
93+
94+
public static deserialize(jsonData: Object): ThisType<BaseResourceModel> {
95+
return new this(new Map<string, any>(Object.entries(jsonData)));
96+
}
97+
98+
public toObject(): Object {
99+
// @ts-ignore
100+
const obj = Object.fromEntries(this.serialize().entries());
101+
return obj;
102+
}
72103
}
73104

74-
export interface BaseResourceHandlerRequest<T extends BaseResourceModel> {
75-
clientRequestToken: ClientRequestToken;
76-
desiredResourceState?: T;
77-
previousResourceState?: T;
78-
logicalResourceIdentifier?: LogicalResourceId;
79-
nextToken?: NextToken;
105+
@allArgsConstructor
106+
export class BaseResourceHandlerRequest<T extends BaseResourceModel> {
107+
public clientRequestToken: ClientRequestToken;
108+
public desiredResourceState?: T;
109+
public previousResourceState?: T;
110+
public logicalResourceIdentifier?: LogicalResourceId;
111+
public nextToken?: NextToken;
112+
113+
constructor(...args: any[]) {}
80114
}
81115

82-
export interface Response<T> {
116+
export interface CfnResponse<T> {
83117
bearerToken: string;
84118
errorCode?: HandlerErrorCode;
85119
operationStatus: OperationStatus;

src/utils.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,30 @@ export interface LambdaContext {
137137
invokedFunctionArn: string;
138138
getRemainingTimeInMillis(): number;
139139
}
140+
141+
/**
142+
* Returns an ordinary object using the Map's keys as the object's keys and its values as the object's values.
143+
*
144+
* @throws {Error} Since object keys are evaluated as strings (in particular, `{ [myObj]: value }` will have a key named
145+
* `[Object object]`), it's possible that two keys within the Map may evaluate to the same object key.
146+
* In this case, if the associated values are not the same, throws an Error.
147+
*/
148+
Map.prototype.toObject = function(): Object {
149+
let o: any = {};
150+
for (let [key, value] of this.entries()) {
151+
if (o.hasOwnProperty(key) && o[key] !== value) {
152+
throw new Error(`Duplicate key ${key} found in Map. First value: ${o[key]}, next value: ${value}`);
153+
}
154+
155+
o[key] = value;
156+
}
157+
158+
return o;
159+
};
160+
161+
/**
162+
* Defines the default JSON representation of a Map to be an array of key-value pairs.
163+
*/
164+
Map.prototype.toJSON = function<K, V>(this: Map<K, V>): Array<[K, V]> {
165+
return Array.from(this.entries());
166+
};

tests/lib/interface.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {
2+
BaseResourceModel,
3+
Optional,
4+
} from '../../src/interface';
5+
6+
7+
describe('when getting interface', () => {
8+
9+
class ResourceModel extends BaseResourceModel {
10+
['constructor']: typeof ResourceModel;
11+
public static readonly TYPE_NAME: string = 'Test::Resource::Model';
12+
13+
public somekey: Optional<string>;
14+
public someotherkey: Optional<string>;
15+
}
16+
17+
test('base resource model get type name', () => {
18+
const model = new ResourceModel();
19+
expect(model.getTypeName()).toBe(model.constructor.TYPE_NAME);
20+
});
21+
22+
test('base resource model deserialize', () => {
23+
expect(() => ResourceModel.deserialize(null)).toThrow('Cannot convert undefined or null to object');
24+
});
25+
26+
test('base resource model serialize', () => {
27+
const model = new ResourceModel(new Map(Object.entries({
28+
somekey: 'a', someotherkey: null
29+
})));
30+
const serialized = model.serialize();
31+
expect(serialized.size).toBe(1);
32+
expect(serialized.get('someotherkey')).not.toBeDefined();
33+
});
34+
35+
test('base resource model to object', () => {
36+
const model = new ResourceModel(new Map(Object.entries({
37+
somekey: 'a', someotherkey: 'b'
38+
})));
39+
const obj = model.toObject();
40+
expect(obj).toMatchObject({
41+
somekey: 'a',
42+
someotherkey: 'b',
43+
});
44+
});
45+
});

0 commit comments

Comments
 (0)