Skip to content

Commit

Permalink
feat(invoke-local): handle http event
Browse files Browse the repository at this point in the history
  • Loading branch information
CorentinDoue committed Jun 14, 2021
1 parent 5dd3876 commit 8fa2fc6
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 92 deletions.
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

0 comments on commit 8fa2fc6

Please sign in to comment.