Skip to content

Commit 446d161

Browse files
authored
feat: Add support for http events in invoke local (#264)
1 parent 2fb939d commit 446d161

7 files changed

+304
-92
lines changed

invokeLocal/googleInvokeLocal.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class GoogleInvokeLocal {
2929

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

3434
const runtime = this.provider.getRuntime(functionObj);
3535
if (!runtime.startsWith('nodejs')) {

invokeLocal/googleInvokeLocal.test.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,7 @@ describe('GoogleInvokeLocal', () => {
165165

166166
it('should validate the function configuration', async () => {
167167
await googleInvokeLocal.invokeLocal();
168-
expect(
169-
validateEventsPropertyStub.calledOnceWith(functionObj, functionName, ['event'])
170-
).toEqual(true);
168+
expect(validateEventsPropertyStub.calledOnceWith(functionObj, functionName)).toEqual(true);
171169
});
172170

173171
it('should get the runtime', async () => {

invokeLocal/lib/httpReqRes.js

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use strict';
2+
3+
const express = require('express');
4+
const http = require('http');
5+
const net = require('net');
6+
7+
// The getReqRes method create an express request and an express response
8+
// as they are created in an express server before being passed to the middlewares
9+
// Google use express 4.17.1 to run http cloud function
10+
// https://cloud.google.com/functions/docs/writing/http#http_frameworks
11+
const app = express();
12+
13+
module.exports = {
14+
getReqRes() {
15+
const req = new http.IncomingMessage(new net.Socket());
16+
const expressRequest = Object.assign(req, { app });
17+
Object.setPrototypeOf(expressRequest, express.request);
18+
19+
const res = new http.ServerResponse(req);
20+
const expressResponse = Object.assign(res, { app, req: expressRequest });
21+
Object.setPrototypeOf(expressResponse, express.response);
22+
23+
expressRequest.res = expressResponse;
24+
25+
return {
26+
expressRequest,
27+
expressResponse,
28+
};
29+
},
30+
};

invokeLocal/lib/nodeJs.js

+82-25
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const chalk = require('chalk');
44
const path = require('path');
55
const _ = require('lodash');
6+
const { getReqRes } = require('./httpReqRes');
67

78
const tryToRequirePaths = (paths) => {
89
let loaded;
@@ -19,10 +20,10 @@ const tryToRequirePaths = (paths) => {
1920
return loaded;
2021
};
2122

23+
const jsonContentType = 'application/json';
24+
2225
module.exports = {
2326
async invokeLocalNodeJs(functionObj, event, customContext) {
24-
let hasResponded = false;
25-
2627
// index.js and function.js are the two files supported by default by a cloud-function
2728
// TODO add the file pointed by the main key of the package.json
2829
const paths = ['index.js', 'function.js'].map((fileName) =>
@@ -41,27 +42,41 @@ module.exports = {
4142

4243
this.addEnvironmentVariablesToProcessEnv(functionObj);
4344

44-
function handleError(err) {
45-
let errorResult;
46-
if (err instanceof Error) {
47-
errorResult = {
48-
errorMessage: err.message,
49-
errorType: err.constructor.name,
50-
stackTrace: err.stack && err.stack.split('\n'),
51-
};
52-
} else {
53-
errorResult = {
54-
errorMessage: err,
55-
};
56-
}
45+
const eventType = Object.keys(functionObj.events[0])[0];
5746

58-
this.serverless.cli.consoleLog(chalk.red(JSON.stringify(errorResult, null, 4)));
59-
process.exitCode = 1;
47+
switch (eventType) {
48+
case 'event':
49+
return this.handleEvent(cloudFunction, event, customContext);
50+
case 'http':
51+
return this.handleHttp(cloudFunction, event, customContext);
52+
default:
53+
throw new Error(`${eventType} is not supported`);
6054
}
55+
},
56+
handleError(err, resolve) {
57+
let errorResult;
58+
if (err instanceof Error) {
59+
errorResult = {
60+
errorMessage: err.message,
61+
errorType: err.constructor.name,
62+
stackTrace: err.stack && err.stack.split('\n'),
63+
};
64+
} else {
65+
errorResult = {
66+
errorMessage: err,
67+
};
68+
}
69+
70+
this.serverless.cli.consoleLog(chalk.red(JSON.stringify(errorResult, null, 4)));
71+
resolve();
72+
process.exitCode = 1;
73+
},
74+
handleEvent(cloudFunction, event, customContext) {
75+
let hasResponded = false;
6176

6277
function handleResult(result) {
6378
if (result instanceof Error) {
64-
handleError.call(this, result);
79+
this.handleError.call(this, result);
6580
return;
6681
}
6782
this.serverless.cli.consoleLog(JSON.stringify(result, null, 4));
@@ -72,26 +87,68 @@ module.exports = {
7287
if (!hasResponded) {
7388
hasResponded = true;
7489
if (err) {
75-
handleError.call(this, err);
90+
this.handleError(err, resolve);
7691
} else if (result) {
7792
handleResult.call(this, result);
7893
}
94+
resolve();
7995
}
80-
resolve();
8196
};
8297

8398
let context = {};
8499

85100
if (customContext) {
86101
context = customContext;
87102
}
88-
89-
const maybeThennable = cloudFunction(event, context, callback);
90-
if (maybeThennable) {
91-
return Promise.resolve(maybeThennable).then(callback.bind(this, null), callback.bind(this));
103+
try {
104+
const maybeThennable = cloudFunction(event, context, callback);
105+
if (maybeThennable) {
106+
Promise.resolve(maybeThennable).then(callback.bind(this, null), callback.bind(this));
107+
}
108+
} catch (error) {
109+
this.handleError(error, resolve);
92110
}
111+
});
112+
},
113+
handleHttp(cloudFunction, event) {
114+
const { expressRequest, expressResponse: response } = getReqRes();
115+
const request = Object.assign(expressRequest, event);
116+
117+
return new Promise((resolve) => {
118+
const endCallback = (data) => {
119+
if (data && Buffer.isBuffer(data)) {
120+
data = data.toString();
121+
}
122+
const headers = response.getHeaders();
123+
const bodyIsJson =
124+
headers['content-type'] && headers['content-type'].includes(jsonContentType);
125+
if (data && bodyIsJson) {
126+
data = JSON.parse(data);
127+
}
128+
this.serverless.cli.consoleLog(
129+
JSON.stringify(
130+
{
131+
status: response.statusCode,
132+
headers,
133+
body: data,
134+
},
135+
null,
136+
4
137+
)
138+
);
139+
resolve();
140+
};
93141

94-
return maybeThennable;
142+
Object.assign(response, { end: endCallback }); // Override of the end method which is always called to send the response of the http request
143+
144+
try {
145+
const maybeThennable = cloudFunction(request, response);
146+
if (maybeThennable) {
147+
Promise.resolve(maybeThennable).catch((error) => this.handleError(error, resolve));
148+
}
149+
} catch (error) {
150+
this.handleError(error, resolve);
151+
}
95152
});
96153
},
97154

0 commit comments

Comments
 (0)