Skip to content

Commit 11659ea

Browse files
committed
ensure resource serialization on both entrypoints
1 parent ad4ab12 commit 11659ea

File tree

2 files changed

+78
-52
lines changed

2 files changed

+78
-52
lines changed

src/resource.ts

Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import {
99
BaseResourceModel,
1010
BaseResourceHandlerRequest,
1111
Callable,
12+
CfnResponse,
1213
Credentials,
1314
HandlerErrorCode,
1415
OperationStatus,
1516
Optional,
16-
Response,
17+
RequestContext,
1718
} from './interface';
1819
import { ProviderLogHandler } from './log-delivery';
1920
import { MetricsPublisherProxy } from './metrics';
@@ -37,31 +38,36 @@ class HandlerEvents extends Map<Action, string | symbol> {};
3738
/**
3839
* Decorates a method to ensure that the JSON input and output are serialized properly.
3940
*
40-
* @returns {PropertyDescriptor}
41+
* @returns {MethodDecorator}
4142
*/
42-
function ensureSerialize(target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor {
43-
44-
// Save a reference to the original method this way we keep the values currently in the
45-
// descriptor and don't overwrite what another decorator might have done to the descriptor.
46-
if(descriptor === undefined) {
47-
descriptor = Object.getOwnPropertyDescriptor(target, propertyKey);
48-
}
49-
const originalMethod = descriptor.value;
50-
// Wrapping the original method with new signature.
51-
descriptor.value = async function(event: Object | Map<string, any>, context: any): Promise<any> {
52-
let mappedEvent: Map<string, any>;
53-
if (event instanceof Map) {
54-
mappedEvent = new Map<string, any>(event);
55-
} else {
56-
mappedEvent = new Map<string, any>(Object.entries(event));
43+
function ensureSerialize(toResponse: boolean = false): MethodDecorator {
44+
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor {
45+
type Resource = typeof target;
46+
// Save a reference to the original method this way we keep the values currently in the
47+
// descriptor and don't overwrite what another decorator might have done to the descriptor.
48+
if(descriptor === undefined) {
49+
descriptor = Object.getOwnPropertyDescriptor(target, propertyKey);
50+
}
51+
const originalMethod = descriptor.value;
52+
// Wrapping the original method with new signature.
53+
descriptor.value = async function(event: Object | Map<string, any>, context: any): Promise<ProgressEvent | CfnResponse<Resource>> {
54+
let mappedEvent: Map<string, any>;
55+
if (event instanceof Map) {
56+
mappedEvent = new Map<string, any>(event);
57+
} else {
58+
mappedEvent = new Map<string, any>(Object.entries(event));
59+
}
60+
const progress: ProgressEvent = await originalMethod.apply(this, [mappedEvent, context]);
61+
if (toResponse) {
62+
// Use the raw event data as a last-ditch attempt to call back if the
63+
// request is invalid.
64+
const serialized = progress.serialize(true, mappedEvent.get('bearerToken'));
65+
return serialized.toObject() as CfnResponse<Resource>;
66+
}
67+
return progress;
5768
}
58-
const progress = await originalMethod.apply(this, [mappedEvent, context]);
59-
// Use the raw event data as a last-ditch attempt to call back if the
60-
// request is invalid.
61-
const serialized = progress.serialize(true, mappedEvent.get('bearerToken'));
62-
return serialized.toObject();
69+
return descriptor;
6370
}
64-
return descriptor;
6571
}
6672

6773
/**
@@ -112,16 +118,16 @@ export abstract class BaseResource<T extends BaseResourceModel = BaseResourceMod
112118
if (handlerResponse.status !== OperationStatus.InProgress) {
113119
return false;
114120
}
115-
// Modify requestContext dict in-place, so that invoke count is bumped on local
116-
// reinvoke too
117-
const reinvokeContext = handlerRequest.requestContext;
121+
// Modify requestContext in-place, so that invoke count is bumped on local
122+
// reinvoke too.
123+
const reinvokeContext: RequestContext<Map<string, any>> = handlerRequest.requestContext;
118124
reinvokeContext.invocation = (reinvokeContext.invocation || 0) + 1;
119125
const callbackDelaySeconds = handlerResponse.callbackDelaySeconds;
120126
const remainingMs = context.getRemainingTimeInMillis();
121127

122128
// When a handler requests a sub-minute callback delay, and if the lambda
123129
// invocation has enough runtime (with 20% buffer), we can re-run the handler
124-
// locally otherwise we re-invoke through CloudWatchEvents
130+
// locally otherwise we re-invoke through CloudWatchEvents.
125131
const neededMsRemaining = callbackDelaySeconds * 1200 + INVOCATION_TIMEOUT_MS;
126132
if (callbackDelaySeconds < 60 && remainingMs > neededMsRemaining) {
127133
const delay = async (ms: number) => {
@@ -196,15 +202,15 @@ export abstract class BaseResource<T extends BaseResourceModel = BaseResourceMod
196202
throw new InternalFailure(`${err} (${err.name})`);
197203
}
198204

199-
return [session, request, action, event.callbackContext || new Map()];
205+
return [session, request, action, event.callbackContext || new Map<string, any>()];
200206
}
201207

202208
// @ts-ignore
203209
public async testEntrypoint (
204210
eventData: Object | Map<string, any>, context: any
205-
): Promise<Response<BaseResource>>;
211+
): Promise<ProgressEvent>;
206212
@boundMethod
207-
@ensureSerialize
213+
@ensureSerialize()
208214
public async testEntrypoint(
209215
eventData: Map<string, any>, context: any
210216
): Promise<ProgressEvent> {
@@ -217,10 +223,11 @@ export abstract class BaseResource<T extends BaseResourceModel = BaseResourceMod
217223
if (err instanceof BaseHandlerException) {
218224
LOGGER.error('Handler error')
219225
progress = err.toProgressEvent();
226+
} else {
227+
LOGGER.error('Exception caught');
228+
msg = err.message || msg;
229+
progress = ProgressEvent.failed(HandlerErrorCode.InternalFailure, msg);
220230
}
221-
LOGGER.error('Exception caught');
222-
msg = err.message || msg;
223-
progress = ProgressEvent.failed(HandlerErrorCode.InternalFailure, msg);
224231
}
225232
return Promise.resolve(progress);
226233
}
@@ -253,7 +260,7 @@ export abstract class BaseResource<T extends BaseResourceModel = BaseResourceMod
253260
throw new Error('No platform credentials');
254261
}
255262
action = event.action;
256-
callbackContext = event.requestContext.callbackContext || {} as Map<string, any>;
263+
callbackContext = event.requestContext?.callbackContext || new Map<string, any>();
257264
} catch(err) {
258265
LOGGER.error('Invalid request');
259266
throw new InvalidRequest(`${err} (${err.name})`);
@@ -286,9 +293,9 @@ export abstract class BaseResource<T extends BaseResourceModel = BaseResourceMod
286293
// @ts-ignore
287294
public async entrypoint (
288295
eventData: Object | Map<string, any>, context: LambdaContext
289-
): Promise<Response<BaseResource>>;
296+
): Promise<CfnResponse<BaseResource>>;
290297
@boundMethod
291-
@ensureSerialize
298+
@ensureSerialize(true)
292299
public async entrypoint (
293300
eventData: Map<string, any>, context: LambdaContext
294301
): Promise<ProgressEvent> {
@@ -357,6 +364,9 @@ export abstract class BaseResource<T extends BaseResourceModel = BaseResourceMod
357364
}
358365
if (progress.callbackContext) {
359366
const callback = progress.callbackContext;
367+
if (!event.requestContext) {
368+
event.requestContext = {} as RequestContext<Map<string, any>>;
369+
}
360370
event.requestContext.callbackContext = callback;
361371
}
362372
if (MUTATING_ACTIONS.includes(event.action)) {

tests/lib/resource.test.ts

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import { reportProgress } from '../../src/callback';
77
import {
88
Action,
99
BaseResourceHandlerRequest,
10+
BaseResourceModel,
11+
CfnResponse,
1012
HandlerErrorCode,
1113
OperationStatus,
1214
RequestContext,
13-
Response,
14-
BaseResourceModel,
1515
} from '../../src/interface';
1616
import { ProviderLogHandler } from '../../src/log-delivery';
1717
import { MetricsPublisherProxy } from '../../src/metrics';
@@ -127,7 +127,7 @@ describe('when getting resource', () => {
127127

128128
test('entrypoint handler error', async () => {
129129
const resource = getResource();
130-
const event: Response<Resource> = await resource.entrypoint({}, null);
130+
const event: CfnResponse<Resource> = await resource.entrypoint({}, null);
131131
expect(event.operationStatus).toBe(OperationStatus.Failed);
132132
expect(event.errorCode).toBe(HandlerErrorCode.InvalidRequest);
133133
});
@@ -138,7 +138,7 @@ describe('when getting resource', () => {
138138
const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success());
139139
const resource = new Resource(TYPE_NAME, MockModel);
140140
resource.addHandler(Action.Create, mockHandler);
141-
const event: Response<Resource> = await resource.entrypoint(entrypointPayload, null);
141+
const event: CfnResponse<Resource> = await resource.entrypoint(entrypointPayload, null);
142142
expect(mockLogDelivery).toBeCalledTimes(1);
143143
expect(mockReportProgress).toBeCalledTimes(2);
144144
expect(event).toMatchObject({
@@ -163,7 +163,7 @@ describe('when getting resource', () => {
163163
mockInvokeHandler.mockImplementation(() => {
164164
throw new exceptions.InvalidRequest('handler failed');
165165
});
166-
const event: Response<Resource> = await resource.entrypoint(entrypointPayload, null);
166+
const event: CfnResponse<Resource> = await resource.entrypoint(entrypointPayload, null);
167167
expect(mockPublishException).toBeCalledTimes(1);
168168
expect(mockInvokeHandler).toBeCalledTimes(1);
169169
expect(event).toMatchObject({
@@ -197,6 +197,25 @@ describe('when getting resource', () => {
197197
expect(mockHandler).toBeCalledTimes(1);
198198
});
199199

200+
test('entrypoint without context', async () => {
201+
entrypointPayload['requestContext'] = null;
202+
const mockLogDelivery: jest.Mock = (ProviderLogHandler.setup as unknown) as jest.Mock;
203+
const mockReportProgress: jest.Mock = (reportProgress as unknown) as jest.Mock;
204+
const event: ProgressEvent = ProgressEvent.success(null, { 'c': 'd' });
205+
const mockHandler: jest.Mock = jest.fn(() => event);
206+
const resource = new Resource(TYPE_NAME, MockModel);
207+
resource.addHandler(Action.Create, mockHandler);
208+
const response: CfnResponse<Resource> = await resource.entrypoint(entrypointPayload, null);
209+
expect(mockLogDelivery).toBeCalledTimes(1);
210+
expect(mockReportProgress).toBeCalledTimes(2);
211+
expect(response).toMatchObject({
212+
message: '',
213+
bearerToken: '123456',
214+
operationStatus: OperationStatus.Success,
215+
});
216+
expect(mockHandler).toBeCalledTimes(1);
217+
});
218+
200219
test('entrypoint success without caller provider creds', async () => {
201220
const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success());
202221
const resource = new Resource(TYPE_NAME, MockModel);
@@ -209,7 +228,7 @@ describe('when getting resource', () => {
209228
// Credentials are defined in payload, but null.
210229
entrypointPayload['requestData']['providerCredentials'] = null;
211230
entrypointPayload['requestData']['callerCredentials'] = null;
212-
let response: Response<Resource> = await resource.entrypoint(entrypointPayload, null);
231+
let response: CfnResponse<Resource> = await resource.entrypoint(entrypointPayload, null);
213232
expect(response).toMatchObject(expected);
214233

215234
// Credentials are undefined in payload.
@@ -302,7 +321,7 @@ describe('when getting resource', () => {
302321
throw new Error('exception');
303322
});
304323
const resource = getResource();
305-
const event: Response<Resource> = await resource.entrypoint({}, null);
324+
const event: CfnResponse<Resource> = await resource.entrypoint({}, null);
306325
expect(mockParseRequest).toBeCalledTimes(1);
307326
expect(event.operationStatus).toBe(OperationStatus.Failed);
308327
expect(event.errorCode).toBe(HandlerErrorCode.InternalFailure);
@@ -425,8 +444,8 @@ describe('when getting resource', () => {
425444

426445
test('test entrypoint handler error', async () => {
427446
const resource = getResource();
428-
const event: Response<Resource> = await resource.testEntrypoint({}, null);
429-
expect(event.operationStatus).toBe(OperationStatus.Failed);
447+
const event: ProgressEvent = await resource.testEntrypoint({}, null);
448+
expect(event.status).toBe(OperationStatus.Failed);
430449
expect(event.errorCode).toBe(HandlerErrorCode.InternalFailure);
431450
});
432451

@@ -436,8 +455,8 @@ describe('when getting resource', () => {
436455
mockParseRequest.mockImplementationOnce(() => {
437456
throw new Error('exception');
438457
});
439-
const event: Response<Resource> = await resource.testEntrypoint({}, null);
440-
expect(event.operationStatus).toBe(OperationStatus.Failed);
458+
const event: ProgressEvent = await resource.testEntrypoint({}, null);
459+
expect(event.status).toBe(OperationStatus.Failed);
441460
expect(event.errorCode).toBe(HandlerErrorCode.InternalFailure);
442461
expect(event.message).toBe('exception');
443462
});
@@ -465,11 +484,8 @@ describe('when getting resource', () => {
465484
logicalResourceIdentifier: null,
466485
},
467486
};
468-
const event: Response<Resource> = await resource.testEntrypoint(payload, null);
469-
expect(event).toMatchObject({
470-
message: '',
471-
operationStatus: OperationStatus.InProgress,
472-
});
487+
const event: ProgressEvent = await resource.testEntrypoint(payload, null);
488+
expect(event).toBe(progressEvent);
473489

474490
expect(spyDeserialize).nthCalledWith(1, {state: 'state1'});
475491
expect(spyDeserialize).nthCalledWith(2, {state: 'state2'});

0 commit comments

Comments
 (0)