Skip to content

Commit 797b912

Browse files
committed
✨ feat: enable HTTP trigger async function
Signed-off-by: Haili Zhang <haili.zhang@outlook.com>
1 parent ce63398 commit 797b912

File tree

8 files changed

+143
-46
lines changed

8 files changed

+143
-46
lines changed

src/function_wrappers.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414

1515
// eslint-disable-next-line node/no-deprecated-api
1616
import * as domain from 'domain';
17+
18+
import * as Debug from 'debug';
19+
import {get, isEmpty} from 'lodash';
20+
1721
import {Request, Response, RequestHandler} from 'express';
1822
import {sendCrashResponse} from './logger';
1923
import {sendResponse} from './invoker';
@@ -27,9 +31,14 @@ import {
2731
CloudEventFunctionWithCallback,
2832
HandlerFunction,
2933
} from './functions';
30-
import {CloudEvent} from './functions';
34+
import {CloudEvent, OpenFunction} from './functions';
3135
import {SignatureType} from './types';
3236

37+
import {OpenFunctionContext} from './openfunction/function_context';
38+
import {OpenFunctionRuntime} from './openfunction/function_runtime';
39+
40+
const debug = Debug('common:wrapper');
41+
3342
/**
3443
* The handler function used to signal completion of event functions.
3544
*/
@@ -122,6 +131,37 @@ const wrapHttpFunction = (execute: HttpFunction): RequestHandler => {
122131
};
123132
};
124133

134+
const wrapHttpAsyncFunction = (
135+
userFunction: OpenFunction,
136+
context: OpenFunctionContext
137+
): RequestHandler => {
138+
const ctx = OpenFunctionRuntime.ProxyContext(context);
139+
const httpHandler = (req: Request, res: Response) => {
140+
const callback = getOnDoneCallback(res);
141+
142+
Promise.resolve()
143+
.then(() => userFunction(ctx, req.body))
144+
.then(result => {
145+
debug('ℹ️ User function returned: %j', result);
146+
147+
const data = get(result, 'body');
148+
const code = get(result, 'code', 200);
149+
const headers = get(result, 'headers');
150+
151+
!isEmpty(headers) && res.set(headers);
152+
153+
if (data !== undefined) {
154+
res.status(code).send(data);
155+
} else {
156+
res.status(code).end();
157+
}
158+
})
159+
.catch(err => callback(err, undefined));
160+
};
161+
162+
return wrapHttpFunction(httpHandler);
163+
};
164+
125165
/**
126166
* Wraps an async CloudEvent function in an express RequestHandler.
127167
* @param userFunction User's function.
@@ -202,10 +242,16 @@ const wrapEventFunctionWithCallback = (
202242
*/
203243
export const wrapUserFunction = <T = unknown>(
204244
userFunction: HandlerFunction<T>,
205-
signatureType: SignatureType
245+
signatureType: SignatureType,
246+
context?: object
206247
): RequestHandler => {
207248
switch (signatureType) {
208249
case 'http':
250+
if (!isEmpty((context as OpenFunctionContext)?.outputs))
251+
return wrapHttpAsyncFunction(
252+
userFunction as OpenFunction,
253+
context as OpenFunctionContext
254+
);
209255
return wrapHttpFunction(userFunction as HttpFunction);
210256
case 'event':
211257
// Callback style if user function has more than 2 arguments.

src/functions.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,38 @@ export interface CloudEventFunctionWithCallback<T = unknown> {
7878
}
7979

8080
/**
81-
* A OpenFunction async function handler.
81+
* An OpenFunction async function handler.
8282
* @public
8383
*/
8484
export interface OpenFunction {
8585
(ctx: OpenFunctionRuntime, data: {}): any;
8686
}
8787

88+
/**
89+
* OpenFunction runtime as the context handler.
90+
* @public
91+
*/
92+
export {OpenFunctionRuntime};
93+
94+
/**
95+
* HTTP response ouput from OpenFunction async function
96+
* @public
97+
*/
98+
export interface HttpFunctionResponse {
99+
/**
100+
* Status code of the response.
101+
*/
102+
code?: number;
103+
/**
104+
* Headers of the response.
105+
*/
106+
headers?: Record<string, string>;
107+
/**
108+
* Body of the response.
109+
*/
110+
body?: any;
111+
}
112+
88113
/**
89114
* A function handler.
90115
* @public

src/openfunction/async_server.ts

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {forEach, has, get} from 'lodash';
1+
import {forEach} from 'lodash';
22
import {DaprServer} from 'dapr-client';
33

44
import {OpenFunction} from '../functions';
@@ -20,7 +20,7 @@ export default function (
2020
context: OpenFunctionContext
2121
): AsyncFunctionServer {
2222
const app = new DaprServer('localhost', context.port);
23-
const ctx = getContextProxy(context);
23+
const ctx = OpenFunctionRuntime.ProxyContext(context);
2424

2525
const wrapper = async (data: object) => {
2626
await userFunction(ctx, data);
@@ -44,23 +44,3 @@ export default function (
4444

4545
return app;
4646
}
47-
48-
/**
49-
* It creates a proxy for the runtime object, which delegates all property access to the runtime object
50-
* @param {OpenFunctionContext} context - The context object to be proxied.
51-
* @returns The proxy object.
52-
*/
53-
function getContextProxy(context: OpenFunctionContext): OpenFunctionRuntime {
54-
// Get a proper runtime for the context
55-
const runtime = OpenFunctionRuntime.Parse(context);
56-
57-
// Create a proxy for the context
58-
return new Proxy(runtime, {
59-
get: (target, prop) => {
60-
// Provide delegated property access of the context object
61-
if (has(target.context, prop)) return get(target.context, prop);
62-
// Otherwise, return the property of the runtime object
63-
else return Reflect.get(target, prop);
64-
},
65-
});
66-
}

src/openfunction/function_runtime.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import {env} from 'process';
22

3-
import {chain} from 'lodash';
3+
import {chain, get, has} from 'lodash';
44
import {DaprClient, CommunicationProtocolEnum} from 'dapr-client';
55

6+
import {HttpFunctionResponse} from '../functions';
7+
68
import {
79
OpenFunctionComponent,
810
OpenFunctionContext,
@@ -11,6 +13,7 @@ import {
1113

1214
/**
1315
* The OpenFunction's serving runtime abstract class.
16+
* @public
1417
*/
1518
export abstract class OpenFunctionRuntime {
1619
/**
@@ -32,6 +35,26 @@ export abstract class OpenFunctionRuntime {
3235
return new DaprRuntime(context);
3336
}
3437

38+
/**
39+
* It creates a proxy for the runtime object, which delegates all property access to the runtime object
40+
* @param context - The context object to be proxied.
41+
* @returns The proxy object.
42+
*/
43+
static ProxyContext(context: OpenFunctionContext): OpenFunctionRuntime {
44+
// Get a proper runtime for the context
45+
const runtime = OpenFunctionRuntime.Parse(context);
46+
47+
// Create a proxy for the context
48+
return new Proxy(runtime, {
49+
get: (target, prop) => {
50+
// Provide delegated property access of the context object
51+
if (has(target.context, prop)) return get(target.context, prop);
52+
// Otherwise, return the property of the runtime object
53+
else return Reflect.get(target, prop);
54+
},
55+
});
56+
}
57+
3558
/**
3659
* Getter for the port of Dapr sidecar
3760
*/
@@ -43,7 +66,26 @@ export abstract class OpenFunctionRuntime {
4366
}
4467

4568
/**
46-
* The promise that send data to certain ouput binding or pubsub topic.
69+
* It returns an HTTP style response object with a `code`, `headers`, and `body` property
70+
* @param body - The data you want to send back to the client.
71+
* @param code - The HTTP status code to return.
72+
* @param headers - An object containing the headers to be sent with the response.
73+
* @returns A function that takes in data, code, and headers and returns an response object.
74+
*/
75+
response(
76+
body: unknown,
77+
code = 200,
78+
headers?: Record<string, string>
79+
): HttpFunctionResponse {
80+
return {
81+
code,
82+
headers,
83+
body,
84+
};
85+
}
86+
87+
/**
88+
* The promise that send data to certain ouput.
4789
*/
4890
abstract send(data: object, output?: string): Promise<object>;
4991
}

src/server.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,12 @@ import {cloudEventToBackgroundEventMiddleware} from './middleware/cloud_event_to
2424
import {backgroundEventToCloudEventMiddleware} from './middleware/background_event_to_cloud_event';
2525
import {wrapUserFunction} from './function_wrappers';
2626

27-
import daprOutputMiddleware from './openfunction/dapr_output_middleware';
28-
2927
/**
3028
* Creates and configures an Express application and returns an HTTP server
3129
* which will run it.
3230
* @param userFunction User's function.
3331
* @param functionSignatureType Type of user's function signature.
32+
* @param context Optional context object.
3433
* @return HTTP server.
3534
*/
3635
export function getServer(
@@ -53,9 +52,6 @@ export function getServer(
5352
next();
5453
});
5554

56-
// Use OpenFunction middlewares
57-
app.use(daprOutputMiddleware);
58-
5955
/**
6056
* Retains a reference to the raw body buffer to allow access to the raw body
6157
* for things like request signature validation. This is used as the "verify"
@@ -139,7 +135,11 @@ export function getServer(
139135
}
140136

141137
// Set up the routes for the user's function
142-
const requestHandler = wrapUserFunction(userFunction, functionSignatureType);
138+
const requestHandler = wrapUserFunction(
139+
userFunction,
140+
functionSignatureType,
141+
context
142+
);
143143
if (functionSignatureType === 'http') {
144144
app.all('/*', requestHandler);
145145
} else {

test/conformance/function.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,10 @@ function writeJson(content) {
4444
fs.writeFileSync(fileName, json);
4545
}
4646

47-
function tryKnative(req, res) {
48-
debug('✅ Function should receive request: %o', req.body);
49-
res.send(req.body);
47+
async function tryKnativeAsync(ctx, data) {
48+
debug('✅ Function should receive request: %o', data);
49+
await ctx.send(data);
50+
return ctx.response(data);
5051
}
5152

5253
function tryAsync(ctx, data) {
@@ -57,6 +58,6 @@ module.exports = {
5758
writeHttp,
5859
writeCloudEvent,
5960
writeLegacyEvent,
60-
tryKnative,
61+
tryKnativeAsync,
6162
tryAsync,
6263
};

test/conformance/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
},
99
"scripts": {
1010
"start": "functions-framework",
11-
"knative": "concurrently npm:knative:run:* npm:knative:test",
12-
"knative:run:func": "cross-env DEBUG=test:*,common:*,ofn:* env-cmd -e knative functions-framework --target=tryKnative",
13-
"knative:run:dapr": "dapr run -H 3500 -d ../data/components/http --log-level warn",
14-
"knative:test": "wait-on tcp:3500 tcp:8080 && curl -s -d '{\"data\": \"hello\"}' -H 'Content-Type: application/json' localhost:8080",
11+
"knative:async": "concurrently npm:knative:async:run:* npm:knative:async:test",
12+
"knative:async:run:func": "cross-env DEBUG=test:*,common:*,ofn:* env-cmd -e knative functions-framework --target=tryKnativeAsync",
13+
"knative:async:run:dapr": "dapr run -H 3500 -d ../data/components/http --log-level warn",
14+
"knative:async:test": "wait-on tcp:3500 tcp:8080 && curl -s -d '{\"data\": \"hello\"}' -H 'Content-Type: application/json' localhost:8080",
1515
"async": "concurrently npm:async:run:*",
1616
"async:run:func": "cross-env DEBUG=test:*,common:*,ofn:* env-cmd -e async functions-framework --target=tryAsync",
1717
"async:run:dapr": "dapr run -H 3500 -p 8080 -d ../data/components/cron --log-level info"

test/integration/http_binding.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import {deepStrictEqual} from 'assert';
33
import * as sinon from 'sinon';
44
import * as supertest from 'supertest';
55
import * as shell from 'shelljs';
6-
import {Request, Response} from 'express';
76
import {cloneDeep, forEach, set} from 'lodash';
87

9-
import {getServer} from '../../src/server';
108
import {OpenFunctionContext} from '../../src/openfunction/function_context';
119

10+
import {OpenFunctionRuntime} from '../../src/functions';
11+
import {getServer} from '../../src/server';
12+
1213
const TEST_CONTEXT: OpenFunctionContext = {
1314
name: 'test-context',
1415
version: '1.0.0',
@@ -81,15 +82,17 @@ describe('OpenFunction - HTTP Binding', () => {
8182
);
8283

8384
const server = getServer(
84-
(req: Request, res: Response) => {
85-
res.status(200).json(TEST_PAYLOAD);
85+
async (ctx: OpenFunctionRuntime, data: {}) => {
86+
await ctx.send(data);
87+
return ctx.response(data);
8688
},
8789
'http',
8890
context
8991
);
9092

9193
await supertest(server)
92-
.get('/')
94+
.post('/')
95+
.send(TEST_PAYLOAD)
9396
.expect(200)
9497
.expect(res => {
9598
deepStrictEqual(res.body, TEST_PAYLOAD);

0 commit comments

Comments
 (0)