-
-
Notifications
You must be signed in to change notification settings - Fork 794
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: Support handlers exported from nested modules #1348
fix: Support handlers exported from nested modules #1348
Conversation
0afefd9
to
50b3181
Compare
callbackHandlerDeferred(event, context, callback) { | ||
setTimeout(() => callback(null, 'foo'), 100) | ||
}, | ||
|
||
promiseHandler() { | ||
return Promise.resolve('foo') | ||
}, | ||
|
||
promiseHandlerDeferred() { | ||
return new Promise((resolve) => { | ||
setTimeout(() => resolve('foo'), 100) | ||
}) | ||
}, | ||
|
||
async asyncFunctionHandler() { | ||
return 'foo' | ||
}, | ||
|
||
async asyncFunctionHandlerObject() { | ||
return { | ||
foo: 'bar', | ||
} | ||
}, | ||
|
||
// we deliberately test the case where a 'callback' is defined | ||
// in the handler, but a promise is being returned to protect from a | ||
// potential naive implementation, e.g. | ||
// | ||
// const { promisify } = 'utils' | ||
// const promisifiedHandler = handler.length === 3 ? promisify(handler) : handler | ||
// | ||
// if someone would return a promise, but also defines callback, without using it | ||
// the handler would not be returning anything | ||
|
||
promiseWithDefinedCallbackHandler( | ||
event, // eslint-disable-line no-unused-vars | ||
context, // eslint-disable-line no-unused-vars | ||
callback, // eslint-disable-line no-unused-vars | ||
) { | ||
return Promise.resolve('Hello Promise!') | ||
}, | ||
|
||
contextSucceedWithContextDoneHandler(event, context) { | ||
context.succeed('Hello Context.succeed!') | ||
|
||
context.done(null, 'Hello Context.done!') | ||
}, | ||
|
||
callbackWithContextDoneHandler(event, context, callback) { | ||
callback(null, 'Hello Callback!') | ||
|
||
context.done(null, 'Hello Context.done!') | ||
}, | ||
|
||
callbackWithPromiseHandler(event, context, callback) { | ||
callback(null, 'Hello Callback!') | ||
|
||
return Promise.resolve('Hello Promise!') | ||
}, | ||
|
||
callbackInsidePromiseHandler(event, context, callback) { | ||
return new Promise((resolve) => { | ||
callback(null, 'Hello Callback!') | ||
|
||
resolve('Hello Promise!') | ||
}) | ||
}, | ||
|
||
async requestIdHandler(event, context) { | ||
return context.awsRequestId | ||
}, | ||
|
||
async remainingExecutionTimeHandler(event, context) { | ||
const first = context.getRemainingTimeInMillis() | ||
|
||
await new Promise((resolve) => { | ||
setTimeout(resolve, 100) | ||
}) | ||
|
||
const second = context.getRemainingTimeInMillis() | ||
|
||
await new Promise((resolve) => { | ||
setTimeout(resolve, 200) | ||
}) | ||
|
||
const third = context.getRemainingTimeInMillis() | ||
|
||
return [first, second, third] | ||
}, | ||
|
||
async defaultTimeoutHandler(event, context) { | ||
return context.getRemainingTimeInMillis() | ||
}, | ||
|
||
executionTimeInMillisHandler() { | ||
return new Promise((resolve) => { | ||
setTimeout(resolve, 100) | ||
}) | ||
}, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this file replaces src/lambda/__tests__/fixtures/lambdaFunction.fixture.js
. AWS lamda will throw errors when a file path includes dot .
.
callbackHandlerDeferred(event, context, callback) { | ||
setTimeout(() => callback(null, 'foo'), 100) | ||
}, | ||
|
||
promiseHandler() { | ||
return Promise.resolve('foo') | ||
}, | ||
|
||
promiseHandlerDeferred() { | ||
return new Promise((resolve) => { | ||
setTimeout(() => resolve('foo'), 100) | ||
}) | ||
}, | ||
|
||
async asyncFunctionHandler() { | ||
return 'foo' | ||
}, | ||
|
||
async asyncFunctionHandlerObject() { | ||
return { | ||
foo: 'bar', | ||
} | ||
}, | ||
|
||
// we deliberately test the case where a 'callback' is defined | ||
// in the handler, but a promise is being returned to protect from a | ||
// potential naive implementation, e.g. | ||
// | ||
// const { promisify } = 'utils' | ||
// const promisifiedHandler = handler.length === 3 ? promisify(handler) : handler | ||
// | ||
// if someone would return a promise, but also defines callback, without using it | ||
// the handler would not be returning anything | ||
|
||
promiseWithDefinedCallbackHandler( | ||
event, // eslint-disable-line no-unused-vars | ||
context, // eslint-disable-line no-unused-vars | ||
callback, // eslint-disable-line no-unused-vars | ||
) { | ||
return Promise.resolve('Hello Promise!') | ||
}, | ||
|
||
contextSucceedWithContextDoneHandler(event, context) { | ||
context.succeed('Hello Context.succeed!') | ||
|
||
context.done(null, 'Hello Context.done!') | ||
}, | ||
|
||
callbackWithContextDoneHandler(event, context, callback) { | ||
callback(null, 'Hello Callback!') | ||
|
||
context.done(null, 'Hello Context.done!') | ||
}, | ||
|
||
callbackWithPromiseHandler(event, context, callback) { | ||
callback(null, 'Hello Callback!') | ||
|
||
return Promise.resolve('Hello Promise!') | ||
}, | ||
|
||
callbackInsidePromiseHandler(event, context, callback) { | ||
return new Promise((resolve) => { | ||
callback(null, 'Hello Callback!') | ||
|
||
resolve('Hello Promise!') | ||
}) | ||
}, | ||
|
||
async requestIdHandler(event, context) { | ||
return context.awsRequestId | ||
}, | ||
|
||
async remainingExecutionTimeHandler(event, context) { | ||
const first = context.getRemainingTimeInMillis() | ||
|
||
await new Promise((resolve) => { | ||
setTimeout(resolve, 100) | ||
}) | ||
|
||
const second = context.getRemainingTimeInMillis() | ||
|
||
await new Promise((resolve) => { | ||
setTimeout(resolve, 200) | ||
}) | ||
|
||
const third = context.getRemainingTimeInMillis() | ||
|
||
return [first, second, third] | ||
}, | ||
|
||
async defaultTimeoutHandler(event, context) { | ||
return context.getRemainingTimeInMillis() | ||
}, | ||
|
||
executionTimeInMillisHandler() { | ||
return new Promise((resolve) => { | ||
setTimeout(resolve, 100) | ||
}) | ||
}, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this file replaces src/lambda/__tests__/fixtures/lambdaFunction.fixture.js
. AWS lamda will throw errors when a file path includes dot .
.
50b3181
to
b9e3c7a
Compare
AWS currently allows handler functions to be buried deep inside of a module or object but serverless-offline doesn't. The change updates serverless offline to align with how AWS Lambda attempts to find a handler function. It also changes the fixture file `src/lambda/__tests__/fixtures/lambdaFunction.fixture.js` to align with how AWS expects the module path of handler to be.
b9e3c7a
to
3e6dced
Compare
@pgrzesik can you help review this ? or tell me what I have to do to get it reviewed ? |
Hey @akinboboye - I'll try to review it sometime this week |
thank you for replying on this! Much appreciated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @akinboboye - it looks good in general - I only have a few minor comments and we should be good to go
@@ -1,6 +1,10 @@ | |||
// some-folder/src.index => some-folder/src | |||
export default function splitHandlerPathAndName(handler) { | |||
// Split handler into method name and path i.e. handler.run | |||
const prepathDelimiter = handler.lastIndexOf('/') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't it be easier to do a split on /
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I played around with this. Doing a split would break the the ruby logic and would also some not too easy to understand flows for determining the handler path
, name
and moduleNestings
.
Let me know what you think. thanks
src/utils/splitHandlerPathAndName.js
Outdated
const delimiter = handler.lastIndexOf('.') | ||
const path = handler.substr(0, delimiter) | ||
const name = handler.substr(delimiter + 1) | ||
const postpathSplit = postpath.split('.') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would something like
const [filename, ...moduleNesting] = postpath.split('.')
work here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i applied this fix. Quite simpler and easy to understand. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @akinboboye 🙇
Description
Added support for finding handlers that are further down the export tree or in nested objects. e.g imagine the code and handler path below:
in index.js
For this case the handler would be:
index.object.handler1
.The problem is that offline would read the
handlerPath
andhandler
asindex.object1
and thishandler1
respectively. However, that handlerPath doesn't exist and would subsequently lead to aMODULE_NOT_FOUND
error.On the flip side of things, AWS Lambda has not issues with finding the handler using the path above. it works well and it is supported!
Another use case is when a handler that is exported from a particular module is then re-exported in another module as shown below:
user-handlers.js file
and in an index file, you could import and re-export the handler as below:
index.js file
The handlers definition in
serverless.yml
for getUser and addUser would then beindex.userHandlers.getUser
andindex.userHandlers.addUser
respectively. The problem here again is thatserverless-offline
would not interpret the above handler correctly while AWS would.Motivation and Context
In the examples given above, you could argue that things could have been arranged differently in order to work with how
serverless-offline
understands it. But shouldserverless-offline
or AWS and Node js be our standard ? If AWS gives me flexibility to annex the full power of NodeJs modules for my use cases, while face limitations fromserverless-offline
?Also the test fixture file
src/lambda/__tests__/fixtures/lambdaFunction.fixture.js
was rearranged tosrc/lambda/__tests__/fixtures/lambdaFunction.js
where fixture is now exported. The problem with the former is that, AWS doesn't understand thelambdaFunction.fixture.js
to be a module. It thinkslambdaFunction
is the module to look for and fixture is just one of the exports from the file in which the handler lives in.How Has This Been Tested?
Screenshots (if appropriate):