Skip to content
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

feat: Add support for http events in invoke local #264

Merged
merged 2 commits into from
Jun 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion invokeLocal/googleInvokeLocal.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class GoogleInvokeLocal {

async invokeLocal() {
const functionObj = this.serverless.service.getFunction(this.options.function);
this.validateEventsProperty(functionObj, this.options.function, ['event']); // Only event is currently supported
this.validateEventsProperty(functionObj, this.options.function);

const runtime = this.provider.getRuntime(functionObj);
if (!runtime.startsWith('nodejs')) {
Expand Down
4 changes: 1 addition & 3 deletions invokeLocal/googleInvokeLocal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,7 @@ describe('GoogleInvokeLocal', () => {

it('should validate the function configuration', async () => {
await googleInvokeLocal.invokeLocal();
expect(
validateEventsPropertyStub.calledOnceWith(functionObj, functionName, ['event'])
).toEqual(true);
expect(validateEventsPropertyStub.calledOnceWith(functionObj, functionName)).toEqual(true);
});

it('should get the runtime', async () => {
Expand Down
30 changes: 30 additions & 0 deletions invokeLocal/lib/httpReqRes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict';

const express = require('express');
const http = require('http');
const net = require('net');

// The getReqRes method create an express request and an express response
// as they are created in an express server before being passed to the middlewares
// Google use express 4.17.1 to run http cloud function
// https://cloud.google.com/functions/docs/writing/http#http_frameworks
const app = express();

module.exports = {
getReqRes() {
const req = new http.IncomingMessage(new net.Socket());
const expressRequest = Object.assign(req, { app });
Object.setPrototypeOf(expressRequest, express.request);

const res = new http.ServerResponse(req);
const expressResponse = Object.assign(res, { app, req: expressRequest });
Object.setPrototypeOf(expressResponse, express.response);

expressRequest.res = expressResponse;

return {
expressRequest,
expressResponse,
};
},
};
107 changes: 82 additions & 25 deletions invokeLocal/lib/nodeJs.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const chalk = require('chalk');
const path = require('path');
const _ = require('lodash');
const { getReqRes } = require('./httpReqRes');

const tryToRequirePaths = (paths) => {
let loaded;
Expand All @@ -19,10 +20,10 @@ const tryToRequirePaths = (paths) => {
return loaded;
};

const jsonContentType = 'application/json';

module.exports = {
async invokeLocalNodeJs(functionObj, event, customContext) {
let hasResponded = false;

// index.js and function.js are the two files supported by default by a cloud-function
// TODO add the file pointed by the main key of the package.json
const paths = ['index.js', 'function.js'].map((fileName) =>
Expand All @@ -41,27 +42,41 @@ module.exports = {

this.addEnvironmentVariablesToProcessEnv(functionObj);

function handleError(err) {
let errorResult;
if (err instanceof Error) {
errorResult = {
errorMessage: err.message,
errorType: err.constructor.name,
stackTrace: err.stack && err.stack.split('\n'),
};
} else {
errorResult = {
errorMessage: err,
};
}
const eventType = Object.keys(functionObj.events[0])[0];

this.serverless.cli.consoleLog(chalk.red(JSON.stringify(errorResult, null, 4)));
process.exitCode = 1;
switch (eventType) {
case 'event':
return this.handleEvent(cloudFunction, event, customContext);
case 'http':
return this.handleHttp(cloudFunction, event, customContext);
default:
throw new Error(`${eventType} is not supported`);
}
},
handleError(err, resolve) {
let errorResult;
if (err instanceof Error) {
errorResult = {
errorMessage: err.message,
errorType: err.constructor.name,
stackTrace: err.stack && err.stack.split('\n'),
};
} else {
errorResult = {
errorMessage: err,
};
}

this.serverless.cli.consoleLog(chalk.red(JSON.stringify(errorResult, null, 4)));
resolve();
process.exitCode = 1;
},
handleEvent(cloudFunction, event, customContext) {
let hasResponded = false;

function handleResult(result) {
if (result instanceof Error) {
handleError.call(this, result);
this.handleError.call(this, result);
return;
}
this.serverless.cli.consoleLog(JSON.stringify(result, null, 4));
Expand All @@ -72,26 +87,68 @@ module.exports = {
if (!hasResponded) {
hasResponded = true;
if (err) {
handleError.call(this, err);
this.handleError(err, resolve);
} else if (result) {
handleResult.call(this, result);
}
resolve();
}
resolve();
};

let context = {};

if (customContext) {
context = customContext;
}

const maybeThennable = cloudFunction(event, context, callback);
if (maybeThennable) {
return Promise.resolve(maybeThennable).then(callback.bind(this, null), callback.bind(this));
try {
const maybeThennable = cloudFunction(event, context, callback);
if (maybeThennable) {
Promise.resolve(maybeThennable).then(callback.bind(this, null), callback.bind(this));
}
} catch (error) {
this.handleError(error, resolve);
}
});
},
handleHttp(cloudFunction, event) {
const { expressRequest, expressResponse: response } = getReqRes();
const request = Object.assign(expressRequest, event);

return new Promise((resolve) => {
const endCallback = (data) => {
if (data && Buffer.isBuffer(data)) {
data = data.toString();
}
const headers = response.getHeaders();
const bodyIsJson =
headers['content-type'] && headers['content-type'].includes(jsonContentType);
if (data && bodyIsJson) {
data = JSON.parse(data);
}
this.serverless.cli.consoleLog(
JSON.stringify(
{
status: response.statusCode,
headers,
body: data,
},
null,
4
)
);
resolve();
};

return maybeThennable;
Object.assign(response, { end: endCallback }); // Override of the end method which is always called to send the response of the http request

try {
const maybeThennable = cloudFunction(request, response);
if (maybeThennable) {
Promise.resolve(maybeThennable).catch((error) => this.handleError(error, resolve));
}
} catch (error) {
this.handleError(error, resolve);
}
});
},

Expand Down
Loading