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