From 3849597298098b246e5d24fdeca09ec01915bf4e Mon Sep 17 00:00:00 2001 From: Haili Zhang Date: Sun, 21 Aug 2022 13:24:10 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20plugin=20syste?= =?UTF-8?q?m=20revolution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Haili Zhang --- docs/generated/api.json | 383 +++++++++++++++--- docs/generated/api.md | 22 +- package-lock.json | 133 +++--- package.json | 6 +- src/function_wrappers.ts | 18 +- src/functions.ts | 4 +- src/index.ts | 7 +- src/loader.ts | 240 ++++------- src/main.ts | 14 +- src/openfunction/async_server.ts | 32 +- .../{function_context.ts => context.ts} | 51 +-- src/openfunction/dapr_output_middleware.ts | 38 -- src/openfunction/decs.d.ts | 1 - src/openfunction/plugin.ts | 196 +++++++++ .../{function_runtime.ts => runtime.ts} | 63 ++- src/options.ts | 3 +- test/data/mock/context.ts | 69 ++++ test/data/mock/index.ts | 2 + test/data/mock/payload.ts | 19 + test/data/plugins/constants.mjs | 11 + test/data/plugins/counters.mjs | 65 +++ test/data/plugins/errorMissAll.js | 6 - test/data/plugins/errorMissName.js | 22 - test/data/plugins/errorMissVersion.js | 22 - test/data/plugins/noname.mjs | 7 + test/data/plugins/plugindemo.js | 32 -- test/data/test_data/async_plugin.ts | 111 ----- test/function_wrappers.ts | 2 +- test/integration/async_server.ts | 135 ++++-- test/integration/async_server_plugin.ts | 99 ----- test/integration/cloud_event.ts | 15 +- test/integration/http_binding.ts | 56 ++- test/loader.ts | 228 +---------- test/plugin.ts | 57 +++ 34 files changed, 1129 insertions(+), 1040 deletions(-) rename src/openfunction/{function_context.ts => context.ts} (69%) delete mode 100644 src/openfunction/dapr_output_middleware.ts delete mode 100644 src/openfunction/decs.d.ts create mode 100644 src/openfunction/plugin.ts rename src/openfunction/{function_runtime.ts => runtime.ts} (67%) create mode 100644 test/data/mock/context.ts create mode 100644 test/data/mock/index.ts create mode 100644 test/data/mock/payload.ts create mode 100644 test/data/plugins/constants.mjs create mode 100644 test/data/plugins/counters.mjs delete mode 100644 test/data/plugins/errorMissAll.js delete mode 100644 test/data/plugins/errorMissName.js delete mode 100644 test/data/plugins/errorMissVersion.js create mode 100644 test/data/plugins/noname.mjs delete mode 100644 test/data/plugins/plugindemo.js delete mode 100644 test/data/test_data/async_plugin.ts delete mode 100644 test/integration/async_server_plugin.ts create mode 100644 test/plugin.ts diff --git a/docs/generated/api.json b/docs/generated/api.json index 3180b5a5..d1902656 100644 --- a/docs/generated/api.json +++ b/docs/generated/api.json @@ -1,7 +1,7 @@ { "metadata": { "toolPackage": "@microsoft/api-extractor", - "toolVersion": "7.28.4", + "toolVersion": "7.29.3", "schemaVersion": 1009, "oldestForwardsCompatibleVersion": 1001, "tsdocConfig": { @@ -1487,7 +1487,7 @@ }, { "kind": "Content", - "text": "{}" + "text": "object" }, { "kind": "Content", @@ -1916,29 +1916,15 @@ { "kind": "PropertySignature", "canonicalReference": "@openfunction/functions-framework!OpenFunctionContext#postPlugins:member", - "docComment": "/**\n * Optional post function exec plugins.\n */\n", + "docComment": "/**\n * Optional plugins to be executed after user function.\n */\n", "excerptTokens": [ { "kind": "Content", "text": "postPlugins?: " }, - { - "kind": "Reference", - "text": "Array", - "canonicalReference": "!Array:interface" - }, { "kind": "Content", - "text": "" + "text": "string[]" }, { "kind": "Content", @@ -1951,35 +1937,21 @@ "name": "postPlugins", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 5 + "endIndex": 2 } }, { "kind": "PropertySignature", "canonicalReference": "@openfunction/functions-framework!OpenFunctionContext#prePlugins:member", - "docComment": "/**\n * Optional pre function exec plugins.\n */\n", + "docComment": "/**\n * Optional plugins to be executed before user function.\n */\n", "excerptTokens": [ { "kind": "Content", "text": "prePlugins?: " }, - { - "kind": "Reference", - "text": "Array", - "canonicalReference": "!Array:interface" - }, - { - "kind": "Content", - "text": "" + "text": "string[]" }, { "kind": "Content", @@ -1992,7 +1964,7 @@ "name": "prePlugins", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 5 + "endIndex": 2 } }, { @@ -2174,6 +2146,88 @@ "isStatic": false, "isProtected": true }, + { + "kind": "Method", + "canonicalReference": "@openfunction/functions-framework!OpenFunctionRuntime#getPlugin:member(1)", + "docComment": "/**\n * Get a plugin from the plugin store, or if it doesn't exist, get it from the built-in plugin store.\n *\n * @param name - The name of the plugin to get.\n *\n * @returns A plugin object\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "getPlugin(name: " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "Plugin", + "canonicalReference": "@openfunction/functions-framework!Plugin:class" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "name", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "name": "getPlugin" + }, + { + "kind": "Property", + "canonicalReference": "@openfunction/functions-framework!OpenFunctionRuntime#locals:member", + "docComment": "/**\n * An object to hold local data. TODO: Clarify the usage of this property\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "readonly locals: " + }, + { + "kind": "Reference", + "text": "Record", + "canonicalReference": "!Record:type" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": true, + "isOptional": false, + "releaseTag": "Public", + "name": "locals", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + }, + "isStatic": false, + "isProtected": false + }, { "kind": "Method", "canonicalReference": "@openfunction/functions-framework!OpenFunctionRuntime.Parse:member(1)", @@ -2567,6 +2621,89 @@ }, "isStatic": false, "isProtected": true + }, + { + "kind": "Method", + "canonicalReference": "@openfunction/functions-framework!OpenFunctionRuntime.WrapUserFunction:member(1)", + "docComment": "/**\n * It takes a user function and a context object, and returns a function that executes the user function with the context object, and executes all the pre and post hooks before and after the user function.\n *\n * @param userFunction - The function that you want to wrap.\n *\n * @param context - This is the context object that is passed to the user function.\n *\n * @returns A function that takes in data and returns a promise.\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "static WrapUserFunction(userFunction: " + }, + { + "kind": "Reference", + "text": "OpenFunction", + "canonicalReference": "@openfunction/functions-framework!OpenFunction:interface" + }, + { + "kind": "Content", + "text": ", context: " + }, + { + "kind": "Reference", + "text": "OpenFunctionContext", + "canonicalReference": "@openfunction/functions-framework!OpenFunctionContext:interface" + }, + { + "kind": "Content", + "text": " | " + }, + { + "kind": "Reference", + "text": "OpenFunctionRuntime", + "canonicalReference": "@openfunction/functions-framework!OpenFunctionRuntime:class" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "(data: any) => " + }, + { + "kind": "Reference", + "text": "Promise", + "canonicalReference": "!Promise:interface" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": true, + "returnTypeTokenRange": { + "startIndex": 7, + "endIndex": 10 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "userFunction", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "context", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 6 + }, + "isOptional": false + } + ], + "isOptional": false, + "name": "WrapUserFunction" } ], "implementsTokenRanges": [] @@ -2574,7 +2711,7 @@ { "kind": "Class", "canonicalReference": "@openfunction/functions-framework!Plugin_2:class", - "docComment": "/**\n * The OpenFunction's plugin template.\n *\n * @public\n */\n", + "docComment": "/**\n * Defining an abstract class to represent Plugin.\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -2585,20 +2722,94 @@ "name": "Plugin_2", "preserveMemberOrder": false, "members": [ + { + "kind": "Constructor", + "canonicalReference": "@openfunction/functions-framework!Plugin_2:constructor(1)", + "docComment": "/**\n * Constructor of the OpenFunction plugin.\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "constructor(name: " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ", version?: " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ");" + } + ], + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "name", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "version", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": true + } + ] + }, { "kind": "Method", "canonicalReference": "@openfunction/functions-framework!Plugin_2#execPostHook:member(1)", - "docComment": "/**\n * post main function exec.\n *\n * @param ctx - The openfunction runtime .\n */\n", + "docComment": "/**\n * This function is called after the user function is executed.\n *\n * @param ctx - Object that contains information about the function that is being executed.\n *\n * @param plugins - The collection of loaded pre and post hook plugins.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "execPostHook(ctx?: " + "text": "execPostHook(ctx: " }, { "kind": "Reference", "text": "OpenFunctionRuntime", "canonicalReference": "@openfunction/functions-framework!OpenFunctionRuntime:class" }, + { + "kind": "Content", + "text": " | null" + }, + { + "kind": "Content", + "text": ", plugins: " + }, + { + "kind": "Reference", + "text": "Record", + "canonicalReference": "!Record:type" + }, + { + "kind": "Content", + "text": "" + }, { "kind": "Content", "text": "): " @@ -2619,8 +2830,8 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 5 + "startIndex": 9, + "endIndex": 11 }, "releaseTag": "Public", "isProtected": false, @@ -2630,9 +2841,17 @@ "parameterName": "ctx", "parameterTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 3 }, - "isOptional": true + "isOptional": false + }, + { + "parameterName": "plugins", + "parameterTypeTokenRange": { + "startIndex": 4, + "endIndex": 8 + }, + "isOptional": false } ], "isOptional": false, @@ -2641,17 +2860,43 @@ { "kind": "Method", "canonicalReference": "@openfunction/functions-framework!Plugin_2#execPreHook:member(1)", - "docComment": "/**\n * pre main function exec.\n *\n * @param ctx - The openfunction runtime .\n */\n", + "docComment": "/**\n * This function is called before the user function is executed.\n *\n * @param ctx - Object that contains information about the function that is being executed.\n *\n * @param plugins - The collection of loaded pre and post hook plugins.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "execPreHook(ctx?: " + "text": "execPreHook(ctx: " }, { "kind": "Reference", "text": "OpenFunctionRuntime", "canonicalReference": "@openfunction/functions-framework!OpenFunctionRuntime:class" }, + { + "kind": "Content", + "text": " | null" + }, + { + "kind": "Content", + "text": ", plugins: " + }, + { + "kind": "Reference", + "text": "Record", + "canonicalReference": "!Record:type" + }, + { + "kind": "Content", + "text": "" + }, { "kind": "Content", "text": "): " @@ -2672,8 +2917,8 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 5 + "startIndex": 9, + "endIndex": 11 }, "releaseTag": "Public", "isProtected": false, @@ -2683,9 +2928,17 @@ "parameterName": "ctx", "parameterTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 3 }, - "isOptional": true + "isOptional": false + }, + { + "parameterName": "plugins", + "parameterTypeTokenRange": { + "startIndex": 4, + "endIndex": 8 + }, + "isOptional": false } ], "isOptional": false, @@ -2694,11 +2947,11 @@ { "kind": "Method", "canonicalReference": "@openfunction/functions-framework!Plugin_2#get:member(1)", - "docComment": "/**\n * get instance filed value.\n *\n * @param filedName - the instace filedName\n *\n * @returns filed value.\n */\n", + "docComment": "/**\n * Get the value of a property on the plugin.\n *\n * @param prop - The property to get.\n *\n * @returns The value of the property.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "get(filedName: " + "text": "get(prop: " }, { "kind": "Content", @@ -2710,7 +2963,7 @@ }, { "kind": "Content", - "text": "string" + "text": "any" }, { "kind": "Content", @@ -2727,7 +2980,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "filedName", + "parameterName": "prop", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -2740,12 +2993,12 @@ }, { "kind": "Property", - "canonicalReference": "@openfunction/functions-framework!Plugin_2.OFN_PLUGIN_NAME:member", - "docComment": "", + "canonicalReference": "@openfunction/functions-framework!Plugin_2#name:member", + "docComment": "/**\n * Name of the plugin.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "static OFN_PLUGIN_NAME: " + "text": "readonly name: " }, { "kind": "Content", @@ -2756,25 +3009,25 @@ "text": ";" } ], - "isReadonly": false, + "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "OFN_PLUGIN_NAME", + "name": "name", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": true, + "isStatic": false, "isProtected": false }, { "kind": "Property", - "canonicalReference": "@openfunction/functions-framework!Plugin_2.OFN_PLUGIN_VERSION:member", - "docComment": "", + "canonicalReference": "@openfunction/functions-framework!Plugin_2#version:member", + "docComment": "/**\n * Version of the plugin.\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "static OFN_PLUGIN_VERSION: " + "text": "readonly version: " }, { "kind": "Content", @@ -2785,15 +3038,15 @@ "text": ";" } ], - "isReadonly": false, + "isReadonly": true, "isOptional": false, "releaseTag": "Public", - "name": "OFN_PLUGIN_VERSION", + "name": "version", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 2 }, - "isStatic": true, + "isStatic": false, "isProtected": false } ], diff --git a/docs/generated/api.md b/docs/generated/api.md index e084c92b..864c4d34 100644 --- a/docs/generated/api.md +++ b/docs/generated/api.md @@ -110,7 +110,7 @@ export interface LegacyEvent { // @public export interface OpenFunction { // (undocumented) - (ctx: OpenFunctionRuntime, data: {}): any; + (ctx: OpenFunctionRuntime, data: object): any; } // @public @@ -136,8 +136,8 @@ export interface OpenFunctionContext { name: string; outputs?: OpenFunctionBinding; port?: string; - postPlugins?: Array; - prePlugins?: Array; + postPlugins?: string[]; + prePlugins?: string[]; runtime: `${RuntimeType}` | `${Capitalize}` | `${Uppercase}`; version: string; } @@ -146,6 +146,8 @@ export interface OpenFunctionContext { export abstract class OpenFunctionRuntime { constructor(context: OpenFunctionContext); protected readonly context: OpenFunctionContext; + getPlugin(name: string): Plugin_2; + readonly locals: Record; static Parse(context: OpenFunctionContext): OpenFunctionRuntime; static ProxyContext(context: OpenFunctionContext): OpenFunctionRuntime; get req(): Request_3> | undefined; @@ -158,17 +160,17 @@ export abstract class OpenFunctionRuntime { }; // Warning: (ae-forgotten-export) The symbol "OpenFunctionTrigger" needs to be exported by the entry point index.d.ts protected trigger?: OpenFunctionTrigger; + static WrapUserFunction(userFunction: OpenFunction, context: OpenFunctionContext | OpenFunctionRuntime): (data: any) => Promise; } // @public class Plugin_2 { - execPostHook(ctx?: OpenFunctionRuntime): Promise; - execPreHook(ctx?: OpenFunctionRuntime): Promise; - get(filedName: string): string; - // (undocumented) - static OFN_PLUGIN_NAME: string; - // (undocumented) - static OFN_PLUGIN_VERSION: string; + constructor(name: string, version?: string); + execPostHook(ctx: OpenFunctionRuntime | null, plugins: Record): Promise; + execPreHook(ctx: OpenFunctionRuntime | null, plugins: Record): Promise; + get(prop: string): any; + readonly name: string; + readonly version: string; } export { Plugin_2 as Plugin } diff --git a/package-lock.json b/package-lock.json index f3d638ce..81916e75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,18 +27,18 @@ "functions-framework-nodejs": "build/src/main.js" }, "devDependencies": { - "@microsoft/api-extractor": "^7.28.4", + "@microsoft/api-extractor": "^7.29.3", "@types/body-parser": "1.19.2", "@types/debug": "^4.1.7", "@types/express": "4.17.13", "@types/google-protobuf": "^3.15.6", - "@types/lodash": "^4.14.182", + "@types/lodash": "^4.14.184", "@types/minimist": "1.2.2", "@types/mocha": "9.1.1", "@types/node": "14.18.11", "@types/node-fetch": "^2.6.2", "@types/on-finished": "2.3.1", - "@types/semver": "^7.3.10", + "@types/semver": "^7.3.12", "@types/shelljs": "^0.8.11", "@types/sinon": "10.0.11", "@types/supertest": "2.0.12", @@ -219,37 +219,37 @@ "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, "node_modules/@microsoft/api-extractor": { - "version": "7.28.4", - "resolved": "https://registry.npmmirror.com/@microsoft/api-extractor/-/api-extractor-7.28.4.tgz", - "integrity": "sha512-7JeROBGYTUt4/4HPnpMscsQgLzX0OfGTQR2qOQzzh3kdkMyxmiv2mzpuhoMnwbubb1GvPcyFm+NguoqOqkCVaw==", + "version": "7.29.3", + "resolved": "https://registry.npmmirror.com/@microsoft/api-extractor/-/api-extractor-7.29.3.tgz", + "integrity": "sha512-PHq+Oo8yiXhwi11VQ1Nz36s+aZwgFqjtkd41udWHtSpyMv2slJ74m1cHdpWbs2ovGUCfldayzdpGwnexZLd2bA==", "dev": true, "dependencies": { - "@microsoft/api-extractor-model": "7.21.0", + "@microsoft/api-extractor-model": "7.23.1", "@microsoft/tsdoc": "0.14.1", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.49.0", - "@rushstack/rig-package": "0.3.13", - "@rushstack/ts-command-line": "4.12.1", + "@rushstack/node-core-library": "3.50.2", + "@rushstack/rig-package": "0.3.14", + "@rushstack/ts-command-line": "4.12.2", "colors": "~1.2.1", "lodash": "~4.17.15", "resolve": "~1.17.0", "semver": "~7.3.0", "source-map": "~0.6.1", - "typescript": "~4.6.3" + "typescript": "~4.7.4" }, "bin": { "api-extractor": "bin/api-extractor" } }, "node_modules/@microsoft/api-extractor-model": { - "version": "7.21.0", - "resolved": "https://registry.npmmirror.com/@microsoft/api-extractor-model/-/api-extractor-model-7.21.0.tgz", - "integrity": "sha512-NN4mXzoQWTuzznIcnLWeV6tGyn6Os9frDK6M/mmTXZ73vUYOvSWoKQ5SYzyzP7HF3YtvTmr1Rs+DsBb0HRx7WQ==", + "version": "7.23.1", + "resolved": "https://registry.npmmirror.com/@microsoft/api-extractor-model/-/api-extractor-model-7.23.1.tgz", + "integrity": "sha512-axlZ33H2LfYX7goAaWpzABWZl3JtX/EUkfVBsI4SuMn3AZYBJsP5MVpMCq7jt0PCefWGwwO+Rv+lCmmJIjFhlQ==", "dev": true, "dependencies": { "@microsoft/tsdoc": "0.14.1", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.49.0" + "@rushstack/node-core-library": "3.50.2" } }, "node_modules/@microsoft/api-extractor/node_modules/resolve": { @@ -261,19 +261,6 @@ "path-parse": "^1.0.6" } }, - "node_modules/@microsoft/api-extractor/node_modules/typescript": { - "version": "4.6.4", - "resolved": "https://registry.npmmirror.com/typescript/-/typescript-4.6.4.tgz", - "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/@microsoft/tsdoc": { "version": "0.14.1", "resolved": "https://registry.npmmirror.com/@microsoft/tsdoc/-/tsdoc-0.14.1.tgz", @@ -392,9 +379,9 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, "node_modules/@rushstack/node-core-library": { - "version": "3.49.0", - "resolved": "https://registry.npmmirror.com/@rushstack/node-core-library/-/node-core-library-3.49.0.tgz", - "integrity": "sha512-yBJRzGgUNFwulVrwwBARhbGaHsxVMjsZ9JwU1uSBbqPYCdac+t2HYdzi4f4q/Zpgb0eNbwYj2yxgHYpJORNEaw==", + "version": "3.50.2", + "resolved": "https://registry.npmmirror.com/@rushstack/node-core-library/-/node-core-library-3.50.2.tgz", + "integrity": "sha512-+zpZBcaX5s+wA0avF0Lk3sd5jbGRo5SmsEJpElJbqQd3KGFvc/hcyeNSMqV5+esJ1JuTfnE1QyRt8nvxFNTaQg==", "dev": true, "dependencies": { "@types/node": "12.20.24", @@ -424,9 +411,9 @@ } }, "node_modules/@rushstack/rig-package": { - "version": "0.3.13", - "resolved": "https://registry.npmmirror.com/@rushstack/rig-package/-/rig-package-0.3.13.tgz", - "integrity": "sha512-4/2+yyA/uDl7LQvtYtFs1AkhSWuaIGEKhP9/KK2nNARqOVc5eCXmu1vyOqr5mPvNq7sHoIR+sG84vFbaKYGaDA==", + "version": "0.3.14", + "resolved": "https://registry.npmmirror.com/@rushstack/rig-package/-/rig-package-0.3.14.tgz", + "integrity": "sha512-Ic9EN3kWJCK6iOxEDtwED9nrM146zCDrQaUxbeGOF+q/VLZ/HNHPw+aLqrqmTl0ZT66Sf75Qk6OG+rySjTorvQ==", "dev": true, "dependencies": { "resolve": "~1.17.0", @@ -443,9 +430,9 @@ } }, "node_modules/@rushstack/ts-command-line": { - "version": "4.12.1", - "resolved": "https://registry.npmmirror.com/@rushstack/ts-command-line/-/ts-command-line-4.12.1.tgz", - "integrity": "sha512-S1Nev6h/kNnamhHeGdp30WgxZTA+B76SJ/P721ctP7DrnC+rrjAc6h/R80I4V0cA2QuEEcMdVOQCtK2BTjsOiQ==", + "version": "4.12.2", + "resolved": "https://registry.npmmirror.com/@rushstack/ts-command-line/-/ts-command-line-4.12.2.tgz", + "integrity": "sha512-poBtnumLuWmwmhCEkVAgynWgtnF9Kygekxyp4qtQUSbBrkuyPQTL85c8Cva1YfoUpOdOXxezMAkUt0n5SNKGqw==", "dev": true, "dependencies": { "@types/argparse": "1.0.38", @@ -617,9 +604,9 @@ "dev": true }, "node_modules/@types/lodash": { - "version": "4.14.182", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", - "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==", + "version": "4.14.184", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.184.tgz", + "integrity": "sha512-RoZphVtHbxPZizt4IcILciSWiC6dcn+eZ8oX9IWEYfDMcocdd42f7NPI6fQj+6zI8y4E0L7gu2pcZKLGTRaV9Q==", "dev": true }, "node_modules/@types/long": { @@ -713,9 +700,9 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.3.10", - "resolved": "https://registry.npmmirror.com/@types/semver/-/semver-7.3.10.tgz", - "integrity": "sha512-zsv3fsC7S84NN6nPK06u79oWgrPVd0NvOyqgghV1haPaFcVxIrP4DLomRwGAXk0ui4HZA7mOcSFL98sMVW9viw==", + "version": "7.3.12", + "resolved": "https://registry.npmmirror.com/@types/semver/-/semver-7.3.12.tgz", + "integrity": "sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==", "dev": true }, "node_modules/@types/serve-static": { @@ -7800,23 +7787,23 @@ } }, "@microsoft/api-extractor": { - "version": "7.28.4", - "resolved": "https://registry.npmmirror.com/@microsoft/api-extractor/-/api-extractor-7.28.4.tgz", - "integrity": "sha512-7JeROBGYTUt4/4HPnpMscsQgLzX0OfGTQR2qOQzzh3kdkMyxmiv2mzpuhoMnwbubb1GvPcyFm+NguoqOqkCVaw==", + "version": "7.29.3", + "resolved": "https://registry.npmmirror.com/@microsoft/api-extractor/-/api-extractor-7.29.3.tgz", + "integrity": "sha512-PHq+Oo8yiXhwi11VQ1Nz36s+aZwgFqjtkd41udWHtSpyMv2slJ74m1cHdpWbs2ovGUCfldayzdpGwnexZLd2bA==", "dev": true, "requires": { - "@microsoft/api-extractor-model": "7.21.0", + "@microsoft/api-extractor-model": "7.23.1", "@microsoft/tsdoc": "0.14.1", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.49.0", - "@rushstack/rig-package": "0.3.13", - "@rushstack/ts-command-line": "4.12.1", + "@rushstack/node-core-library": "3.50.2", + "@rushstack/rig-package": "0.3.14", + "@rushstack/ts-command-line": "4.12.2", "colors": "~1.2.1", "lodash": "~4.17.15", "resolve": "~1.17.0", "semver": "~7.3.0", "source-map": "~0.6.1", - "typescript": "~4.6.3" + "typescript": "~4.7.4" }, "dependencies": { "resolve": { @@ -7827,24 +7814,18 @@ "requires": { "path-parse": "^1.0.6" } - }, - "typescript": { - "version": "4.6.4", - "resolved": "https://registry.npmmirror.com/typescript/-/typescript-4.6.4.tgz", - "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", - "dev": true } } }, "@microsoft/api-extractor-model": { - "version": "7.21.0", - "resolved": "https://registry.npmmirror.com/@microsoft/api-extractor-model/-/api-extractor-model-7.21.0.tgz", - "integrity": "sha512-NN4mXzoQWTuzznIcnLWeV6tGyn6Os9frDK6M/mmTXZ73vUYOvSWoKQ5SYzyzP7HF3YtvTmr1Rs+DsBb0HRx7WQ==", + "version": "7.23.1", + "resolved": "https://registry.npmmirror.com/@microsoft/api-extractor-model/-/api-extractor-model-7.23.1.tgz", + "integrity": "sha512-axlZ33H2LfYX7goAaWpzABWZl3JtX/EUkfVBsI4SuMn3AZYBJsP5MVpMCq7jt0PCefWGwwO+Rv+lCmmJIjFhlQ==", "dev": true, "requires": { "@microsoft/tsdoc": "0.14.1", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.49.0" + "@rushstack/node-core-library": "3.50.2" } }, "@microsoft/tsdoc": { @@ -7958,9 +7939,9 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, "@rushstack/node-core-library": { - "version": "3.49.0", - "resolved": "https://registry.npmmirror.com/@rushstack/node-core-library/-/node-core-library-3.49.0.tgz", - "integrity": "sha512-yBJRzGgUNFwulVrwwBARhbGaHsxVMjsZ9JwU1uSBbqPYCdac+t2HYdzi4f4q/Zpgb0eNbwYj2yxgHYpJORNEaw==", + "version": "3.50.2", + "resolved": "https://registry.npmmirror.com/@rushstack/node-core-library/-/node-core-library-3.50.2.tgz", + "integrity": "sha512-+zpZBcaX5s+wA0avF0Lk3sd5jbGRo5SmsEJpElJbqQd3KGFvc/hcyeNSMqV5+esJ1JuTfnE1QyRt8nvxFNTaQg==", "dev": true, "requires": { "@types/node": "12.20.24", @@ -7992,9 +7973,9 @@ } }, "@rushstack/rig-package": { - "version": "0.3.13", - "resolved": "https://registry.npmmirror.com/@rushstack/rig-package/-/rig-package-0.3.13.tgz", - "integrity": "sha512-4/2+yyA/uDl7LQvtYtFs1AkhSWuaIGEKhP9/KK2nNARqOVc5eCXmu1vyOqr5mPvNq7sHoIR+sG84vFbaKYGaDA==", + "version": "0.3.14", + "resolved": "https://registry.npmmirror.com/@rushstack/rig-package/-/rig-package-0.3.14.tgz", + "integrity": "sha512-Ic9EN3kWJCK6iOxEDtwED9nrM146zCDrQaUxbeGOF+q/VLZ/HNHPw+aLqrqmTl0ZT66Sf75Qk6OG+rySjTorvQ==", "dev": true, "requires": { "resolve": "~1.17.0", @@ -8013,9 +7994,9 @@ } }, "@rushstack/ts-command-line": { - "version": "4.12.1", - "resolved": "https://registry.npmmirror.com/@rushstack/ts-command-line/-/ts-command-line-4.12.1.tgz", - "integrity": "sha512-S1Nev6h/kNnamhHeGdp30WgxZTA+B76SJ/P721ctP7DrnC+rrjAc6h/R80I4V0cA2QuEEcMdVOQCtK2BTjsOiQ==", + "version": "4.12.2", + "resolved": "https://registry.npmmirror.com/@rushstack/ts-command-line/-/ts-command-line-4.12.2.tgz", + "integrity": "sha512-poBtnumLuWmwmhCEkVAgynWgtnF9Kygekxyp4qtQUSbBrkuyPQTL85c8Cva1YfoUpOdOXxezMAkUt0n5SNKGqw==", "dev": true, "requires": { "@types/argparse": "1.0.38", @@ -8181,9 +8162,9 @@ "dev": true }, "@types/lodash": { - "version": "4.14.182", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", - "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==", + "version": "4.14.184", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.184.tgz", + "integrity": "sha512-RoZphVtHbxPZizt4IcILciSWiC6dcn+eZ8oX9IWEYfDMcocdd42f7NPI6fQj+6zI8y4E0L7gu2pcZKLGTRaV9Q==", "dev": true }, "@types/long": { @@ -8276,9 +8257,9 @@ "dev": true }, "@types/semver": { - "version": "7.3.10", - "resolved": "https://registry.npmmirror.com/@types/semver/-/semver-7.3.10.tgz", - "integrity": "sha512-zsv3fsC7S84NN6nPK06u79oWgrPVd0NvOyqgghV1haPaFcVxIrP4DLomRwGAXk0ui4HZA7mOcSFL98sMVW9viw==", + "version": "7.3.12", + "resolved": "https://registry.npmmirror.com/@types/semver/-/semver-7.3.12.tgz", + "integrity": "sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==", "dev": true }, "@types/serve-static": { diff --git a/package.json b/package.json index fb193a45..26e61e43 100644 --- a/package.json +++ b/package.json @@ -53,18 +53,18 @@ "author": "OpenFunction", "license": "Apache-2.0", "devDependencies": { - "@microsoft/api-extractor": "^7.28.4", + "@microsoft/api-extractor": "^7.29.3", "@types/body-parser": "1.19.2", "@types/debug": "^4.1.7", "@types/express": "4.17.13", "@types/google-protobuf": "^3.15.6", - "@types/lodash": "^4.14.182", + "@types/lodash": "^4.14.184", "@types/minimist": "1.2.2", "@types/mocha": "9.1.1", "@types/node": "14.18.11", "@types/node-fetch": "^2.6.2", "@types/on-finished": "2.3.1", - "@types/semver": "^7.3.10", + "@types/semver": "^7.3.12", "@types/shelljs": "^0.8.11", "@types/sinon": "10.0.11", "@types/supertest": "2.0.12", diff --git a/src/function_wrappers.ts b/src/function_wrappers.ts index 33e05c08..b2772d34 100644 --- a/src/function_wrappers.ts +++ b/src/function_wrappers.ts @@ -16,6 +16,10 @@ import * as domain from 'domain'; import {Request, Response, RequestHandler} from 'express'; + +import {OpenFunctionContext} from './openfunction/context'; +import {OpenFunctionRuntime} from './openfunction/runtime'; + import {sendCrashResponse} from './logger'; import {sendResponse} from './invoker'; import {isBinaryCloudEvent, getBinaryCloudEventContext} from './cloud_events'; @@ -31,9 +35,6 @@ import { import {CloudEvent, OpenFunction} from './functions'; import {SignatureType} from './types'; -import {OpenFunctionContext} from './openfunction/function_context'; -import {OpenFunctionRuntime} from './openfunction/function_runtime'; - /** * The handler function used to signal completion of event functions. */ @@ -126,18 +127,25 @@ const wrapHttpFunction = (execute: HttpFunction): RequestHandler => { }; }; +/** + * It takes a user-defined function and a context object, and returns a function that can be used as an HTTP handler. + * @param userFunction - The function that you wrote in your index.js file. + * @param context - OpenFunctionContext object which hold all context data. + * @returns A function that takes a request and a response and returns a promise. + */ const wrapOpenFunction = ( userFunction: OpenFunction, context: OpenFunctionContext ): RequestHandler => { const ctx = OpenFunctionRuntime.ProxyContext(context); + const wrapper = OpenFunctionRuntime.WrapUserFunction(userFunction, ctx); const httpHandler = (req: Request, res: Response) => { const callback = getOnDoneCallback(res); ctx.setTrigger(req, res); - Promise.resolve() - .then(() => userFunction(ctx, req.body)) + Promise.resolve(req.body) + .then(wrapper) .then(result => callback(null, result)) .catch(err => callback(err, undefined)); }; diff --git a/src/functions.ts b/src/functions.ts index cc3cba1f..eb1cb049 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -19,7 +19,7 @@ import {Request as ExpressRequest, Response} from 'express'; import {CloudEventV1 as CloudEvent} from 'cloudevents'; -import {OpenFunctionRuntime} from './openfunction/function_runtime'; +import {OpenFunctionRuntime} from './openfunction/runtime'; /** * @public @@ -82,7 +82,7 @@ export interface CloudEventFunctionWithCallback { * @public */ export interface OpenFunction { - (ctx: OpenFunctionRuntime, data: {}): any; + (ctx: OpenFunctionRuntime, data: object): any; } /** diff --git a/src/index.ts b/src/index.ts index 673a5d5e..88198b8e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,4 +25,9 @@ export {http, cloudEvent, openfunction} from './function_registry'; /** * @public */ -export * from './openfunction/function_context'; +export * from './openfunction/context'; + +/** + * @public + */ +export {Plugin} from './openfunction/plugin'; diff --git a/src/loader.ts b/src/loader.ts index 5e943c5f..c77f687d 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -18,18 +18,20 @@ * @packageDocumentation */ +import * as fs from 'fs'; import * as path from 'path'; +import {pathToFileURL} from 'url'; + import * as semver from 'semver'; import * as readPkgUp from 'read-pkg-up'; -import * as fs from 'fs'; -import {pathToFileURL} from 'url'; -import {HandlerFunction, OpenFunctionRuntime} from './functions'; -import {SignatureType} from './types'; -import {getRegisteredFunction} from './function_registry'; -import {Plugin} from './openfunction/function_context'; -import {FrameworkOptions} from './options'; import {forEach} from 'lodash'; +import {Plugin, PluginStore, PluginMap} from './openfunction/plugin'; + +import {HandlerFunction} from './functions'; +import {getRegisteredFunction} from './function_registry'; +import {SignatureType} from './types'; + // Dynamic import function required to load user code packaged as an // ES module is only available on Node.js v13.2.0 and up. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#browser_compatibility @@ -81,6 +83,28 @@ const dynamicImport = new Function( // eslint-disable-next-line @typescript-eslint/no-explicit-any ) as (modulePath: string) => Promise; +async function loadModule(modulePath: string) { + let module; + + const esModule = await isEsModule(modulePath); + if (esModule) { + if (semver.lt(process.version, MIN_NODE_VERSION_ESMODULES)) { + console.error( + `Cannot load ES Module on Node.js ${process.version}. ` + + `Please upgrade to Node.js v${MIN_NODE_VERSION_ESMODULES} and up.` + ); + return null; + } + // Resolve module path to file:// URL. Required for windows support. + const fpath = pathToFileURL(modulePath); + module = await dynamicImport(fpath.href); + } else { + module = require(modulePath); + } + + return module; +} + /** * Returns user's function from function file. * Returns null if function can't be retrieved. @@ -102,25 +126,10 @@ export async function getUserFunction( return null; } - let functionModule; - const esModule = await isEsModule(functionModulePath); - if (esModule) { - if (semver.lt(process.version, MIN_NODE_VERSION_ESMODULES)) { - console.error( - `Cannot load ES Module on Node.js ${process.version}. ` + - `Please upgrade to Node.js v${MIN_NODE_VERSION_ESMODULES} and up.` - ); - return null; - } - // Resolve module path to file:// URL. Required for windows support. - const fpath = pathToFileURL(functionModulePath); - functionModule = await dynamicImport(fpath.href); - } else { - functionModule = require(functionModulePath); - } + // Firstly, we try to load function + const functionModule = await loadModule(functionModulePath); - // If the customer declaratively registered a function matching the target - // return that. + // If the customer declaratively registered a function matching the target return that const registeredFunction = getRegisteredFunction(functionTarget); if (registeredFunction) { return registeredFunction; @@ -199,170 +208,67 @@ function getFunctionModulePath(codeLocation: string): string | null { return path; } -type PluginClass = Record; /** - * Returns user's plugin from function file. - * Returns null if plugin can't be retrieved. - * @return User's plugins or null. + * It loads all the plugins from the provided code location. + * @param codeLocation - The path to the plugin source codes. + * @return A named plugin map object or null. */ -export async function getUserPlugins( - options: FrameworkOptions -): Promise { - // Get plugin set - const pluginSet: Set = new Set(); - if (options.context) { - if (options.context.prePlugins) { - forEach(options.context.prePlugins, plugin => { - typeof plugin === 'string' && pluginSet.add(plugin); - }); - } - if (options.context.postPlugins) { - forEach(options.context.postPlugins, plugin => { - typeof plugin === 'string' && pluginSet.add(plugin); - }); - } - - try { - type Instance = Record; - // Load plugin js files - const instances: Instance = {}; +export async function getFunctionPlugins( + codeLocation: string +): Promise { + const files = getPluginsModulePath(codeLocation); + if (!files) return null; - const pluginFiles = getPluginFiles(options.sourceLocation); - if (pluginFiles === null) { - console.warn('[warn-!!!] user plugins files load failed '); - options.context.prePlugins = []; - options.context.postPlugins = []; - return options; - } + // Try to load all plugin module files - // Find plugins class - const tempMap: PluginClass = {}; - for (const pluginFile of pluginFiles) { - const jsMoulde = require(pluginFile); - processJsModule(jsMoulde, tempMap); - } + const store = PluginStore.Instance(); + const plugins: PluginMap = {}; - // Instance plugin dynamic set ofn_plugin_name - const pluginNames = Array.from(pluginSet.values()); - for (const name of pluginNames) { - const module = tempMap[name]; - if (module) { - const instance = new module(); - instance[Plugin.OFN_PLUGIN_NAME] = module.Name; - instance[Plugin.OFN_PLUGIN_VERSION] = module.Version || 'v1'; + for (const file of files) { + try { + const modules = await loadModule(file); - //Set default method of pre post get - if (!instance.execPreHook) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - instance.execPreHook = (ctx: OpenFunctionRuntime) => { - console.log( - `This plugin ${name} method execPreHook is not implemented.` - ); - }; - } - if (!instance.execPostHook) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - instance.execPostHook = (ctx: OpenFunctionRuntime) => { - console.log( - `This plugin ${name} method execPostHook is not implemented.` - ); - }; - } - if (!instance.get) { - instance.get = (filedName: string) => { - for (const key in instance) { - if (key === filedName) { - return instance[key]; - } - } - }; - } - instances[name] = instance as Plugin; + forEach(modules, module => { + // All plugin modules should extend from Plugin abstract class + if (module.prototype instanceof Plugin) { + // Try to create plugin instance + const plugin = new module(); + // Register plugin instance to plugin store + store.register(plugin); + // Also save to return records + plugins[plugin.name] = plugin; } - } - - const prePlugins: Array = []; - const postPlugins: Array = []; - if (options.context.prePlugins) { - forEach(options.context.prePlugins, plugin => { - if (typeof plugin === 'string') { - const instance = instances[plugin]; - typeof instance === 'object' && prePlugins.push(instance); - } - }); - } - if (options.context.postPlugins) { - forEach(options.context.postPlugins, plugin => { - if (typeof plugin === 'string') { - const instance = instances[plugin]; - typeof instance === 'object' && postPlugins.push(instance); - } - }); - } - - options.context.prePlugins = prePlugins; - options.context.postPlugins = postPlugins; - } catch (error) { - console.error('load plugins error reason: \n'); - console.error(error); + }); + } catch (ex) { + const err = ex; + console.error( + "Provided module can't be loaded. Plesae make sure your module extend Plugin class properly." + + `\nDetailed stack trace: ${err.stack}` + ); } } - return options; + + return plugins; } /** - * Returns resolved path to the dir containing the user plugins. - * Returns null if the path is not exits + * Returns resolved path of the folder containing the user plugins. + * Returns null if the plugin folder does not exist. * @param codeLocation Directory with user's code. - * @return Resolved path or null. + * @return Resolved path of plugins or null. */ -function getPluginFiles(codeLocation: string): Array | null { - const pluginFiles: Array = []; +function getPluginsModulePath(codeLocation: string): string[] | null { try { const param = path.resolve(codeLocation + '/plugins'); const files = fs.readdirSync(param); + const pluginFiles: string[] = []; for (const file of files) { pluginFiles.push(require.resolve(path.join(param, file))); } + return pluginFiles; } catch (ex) { - const err: Error = ex; - console.error(err.message); + console.error('Fail to load plugins: %s', ex); return null; } - return pluginFiles; -} -/** - * Returns rdetermine whether it is a class. - * Returns boolean is can be class - * @param obj jsmodule. - * @return boolean of it is a class. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function couldBeClass(obj: any): boolean { - return ( - typeof obj === 'function' && - obj.prototype !== undefined && - obj.prototype.constructor === obj && - obj.toString().slice(0, 5) === 'class' - ); -} - -/** - * Process jsMoulde if it can be a plugin class put it into tempMap. - * @param obj jsmodule. - * @param tempMap PluginClass. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function processJsModule(obj: any, tempMap: PluginClass) { - if (typeof obj === 'object') { - for (const o in obj) { - if (couldBeClass(obj[o]) && obj[o].Name) { - tempMap[obj[o].Name] = obj[o]; - } - } - } - if (couldBeClass(obj) && obj.Name) { - tempMap[obj.Name] = obj; - } } diff --git a/src/main.ts b/src/main.ts index 3cc5f864..0833bdf9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,12 +21,9 @@ import * as process from 'process'; import {createHttpTerminator} from 'http-terminator'; import getAysncServer from './openfunction/async_server'; -import { - OpenFunctionContext, - ContextUtils, -} from './openfunction/function_context'; +import {OpenFunctionContext, ContextUtils} from './openfunction/context'; -import {getUserFunction, getUserPlugins} from './loader'; +import {getUserFunction, getFunctionPlugins} from './loader'; import {ErrorHandler} from './invoker'; import {getServer} from './server'; import {parseOptions, helpText, OptionsError} from './options'; @@ -44,6 +41,7 @@ export const main = async () => { console.error(helpText); return; } + const loadedFunction = await getUserFunction( options.sourceLocation, options.target, @@ -56,6 +54,9 @@ export const main = async () => { } const {userFunction, signatureType} = loadedFunction; + // Load function plugins before starting server + await getFunctionPlugins(options.sourceLocation); + // Try to determine the server runtime // Considering the async runtime in the first place if (ContextUtils.IsAsyncRuntime(options.context as OpenFunctionContext)) { @@ -69,9 +70,6 @@ export const main = async () => { // DaprServer uses httpTerminator in server.stop() handleShutdown(async () => await server.stop()); - - // Load Plugins - await getUserPlugins(options); } // Then taking sync runtime as the fallback else { diff --git a/src/openfunction/async_server.ts b/src/openfunction/async_server.ts index 6107f24e..2241010e 100644 --- a/src/openfunction/async_server.ts +++ b/src/openfunction/async_server.ts @@ -1,10 +1,10 @@ -import {forEach, invoke} from 'lodash'; +import {forEach} from 'lodash'; import {DaprServer} from '@dapr/dapr'; import {OpenFunction} from '../functions'; -import {OpenFunctionContext, ContextUtils} from './function_context'; -import {OpenFunctionRuntime} from './function_runtime'; +import {OpenFunctionContext, ContextUtils} from './context'; +import {OpenFunctionRuntime} from './runtime'; export type AsyncFunctionServer = DaprServer; @@ -12,36 +12,18 @@ export type AsyncFunctionServer = DaprServer; * Creates and configures an Dapr server and returns an HTTP server * which will run it. * @param userFunction User's function. - * @param functionSignatureType Type of user's function signature. + * @param context Context of user's function. * @return HTTP server. */ export default function ( userFunction: OpenFunction, context: OpenFunctionContext ): AsyncFunctionServer { + // Initailize Dapr server const app = new DaprServer('localhost', context.port); - const ctx = OpenFunctionRuntime.ProxyContext(context); - - const wrapper = async (data: object) => { - // Exec pre hooks - console.log(context.prePlugins); - if (context.prePlugins) { - await context.prePlugins.reduce(async (_, current) => { - await invoke(current, 'execPreHook', ctx); - return []; - }, Promise.resolve([])); - } - - await userFunction(ctx, data); - // Exec post hooks - if (context.postPlugins) { - await context.postPlugins.reduce(async (_, current) => { - await invoke(current, 'execPostHook', ctx); - return []; - }, Promise.resolve([])); - } - }; + // Create wrapper for user function + const wrapper = OpenFunctionRuntime.WrapUserFunction(userFunction, context); // Initialize the server with the user's function. // For server interfaces, refer to https://github.com/dapr/js-sdk/blob/master/src/interfaces/Server/ diff --git a/src/openfunction/function_context.ts b/src/openfunction/context.ts similarity index 69% rename from src/openfunction/function_context.ts rename to src/openfunction/context.ts index fbf4dd23..f2ac10ba 100644 --- a/src/openfunction/function_context.ts +++ b/src/openfunction/context.ts @@ -1,5 +1,3 @@ -import {OpenFunctionRuntime} from './function_runtime'; - /** * The OpenFunction's serving context. * @public @@ -33,13 +31,13 @@ export interface OpenFunctionContext { */ outputs?: OpenFunctionBinding; /** - * Optional pre function exec plugins. + * Optional plugins to be executed before user function. */ - prePlugins?: Array; + prePlugins?: string[]; /** - * Optional post function exec plugins. + * Optional plugins to be executed after user function. */ - postPlugins?: Array; + postPlugins?: string[]; } /** @@ -149,44 +147,3 @@ export class ContextUtils { return component?.componentType.split('.')[0] === ComponentType.PubSub; } } - -/** - * The OpenFunction's plugin template. - * @public - */ -export class Plugin { - static OFN_PLUGIN_NAME = 'ofn_plugin_name'; - static OFN_PLUGIN_VERSION = 'ofn_plugin_version'; - /** - * pre main function exec. - * @param ctx - The openfunction runtime . - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public async execPreHook(ctx?: OpenFunctionRuntime) { - console.log( - `This plugin ${this.get( - Plugin.OFN_PLUGIN_NAME - )} method execPreHook is not implemented.` - ); - } - /** - * post main function exec. - * @param ctx - The openfunction runtime . - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public async execPostHook(ctx?: OpenFunctionRuntime) { - console.log( - `This plugin ${this.get( - Plugin.OFN_PLUGIN_NAME - )} method execPostHook is not implemented.` - ); - } - /** - * get instance filed value. - * @param filedName - the instace filedName - * @returns filed value. - */ - public get(filedName: string) { - return filedName; - } -} diff --git a/src/openfunction/dapr_output_middleware.ts b/src/openfunction/dapr_output_middleware.ts deleted file mode 100644 index 478d6392..00000000 --- a/src/openfunction/dapr_output_middleware.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {Request, Response} from 'express'; -import * as interceptor from 'express-interceptor'; - -import * as Debug from 'debug'; -import {isEmpty} from 'lodash'; - -import {OpenFunctionContext, ContextUtils} from './function_context'; -import {OpenFunctionRuntime} from './function_runtime'; - -const debug = Debug('ofn:middleware:dapr:binding'); - -/** - * The handler to invoke Dapr output binding before sending the response. - * @param req express request object - * @param res express response object - */ -const daprOutputHandler = (req: Request, res: Response) => { - return { - isInterceptable: () => { - return ( - !isEmpty(res.locals.context?.outputs) && - ContextUtils.IsKnativeRuntime(res.locals.context as OpenFunctionContext) - ); - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - intercept: (body: any, send: Function) => { - const context = res.locals.context; - const runtime = OpenFunctionRuntime.Parse(context); - - runtime.send(body).then(data => { - debug('🎩 Dapr results: %j', data); - send(body); - }); - }, - }; -}; - -export default interceptor(daprOutputHandler); diff --git a/src/openfunction/decs.d.ts b/src/openfunction/decs.d.ts deleted file mode 100644 index 2b551564..00000000 --- a/src/openfunction/decs.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'express-interceptor'; diff --git a/src/openfunction/plugin.ts b/src/openfunction/plugin.ts new file mode 100644 index 00000000..946796f4 --- /dev/null +++ b/src/openfunction/plugin.ts @@ -0,0 +1,196 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import {get, invoke, omit, transform, trim} from 'lodash'; + +import {OpenFunctionRuntime} from './runtime'; + +/** + * Defining an abstract class to represent Plugin. + * @public + **/ +export class Plugin { + /** + * Name of the plugin. + */ + readonly name: string; + + /** + * Version of the plugin. + */ + readonly version: string; + + /** + * Constructor of the OpenFunction plugin. + */ + constructor(name: string, version = 'unknown') { + if (!trim(name)) { + throw new Error('Plugin name must be specified.'); + } + + this.name = name; + this.version = version; + } + + /** + * Get the value of a property on the plugin. + * @param prop - The property to get. + * @returns The value of the property. + */ + get(prop: string) { + return get(this, prop); + } + + /** + * This function is called before the user function is executed. + * @param ctx - Object that contains information about the function that is being executed. + * @param plugins - The collection of loaded pre and post hook plugins. + */ + async execPreHook( + ctx: OpenFunctionRuntime | null, + plugins: Record + ) { + console.warn( + `Plugin "${this.name}" has not implemented pre hook function.` + ); + } + + /** + * This function is called after the user function is executed. + * @param ctx - Object that contains information about the function that is being executed. + * @param plugins - The collection of loaded pre and post hook plugins. + */ + async execPostHook( + ctx: OpenFunctionRuntime | null, + plugins: Record + ) { + console.warn( + `Plugin "${this.name}" has not implemented post hook function.` + ); + } +} + +/** + * PluginMap type definition. + */ +export type PluginMap = Record & {_seq?: string[]}; + +enum PluginStoreType { + BUILTIN = 1, + CUSTOM, +} + +/** + * Initializes a store object for PluginStore singleton class + * with the keys of the PluginStoreType enum and the values of an empty object. + **/ +const stores = transform( + PluginStoreType, + (r, k, v) => { + r[v] = {_seq: []} as {} as PluginMap; + }, + >{} +); + +/** + * PluginStore is a class that manages a collection of plugins. + **/ +export class PluginStore { + /** + * Type of the plugin store. + */ + static Type = PluginStoreType; + + /** + * Singleton helper method to create PluginStore instance. + * @param type - PluginStoreType - The type of plugin store you want to create. + * @returns A new instance of the PluginStore class. + */ + static Instance(type = PluginStore.Type.CUSTOM): PluginStore { + return new PluginStore(type); + } + + /** + * Internal store object. + */ + #store: PluginMap | null = null; + + /** + * Private constructor of PluginStore. + * @param type - PluginStoreType - The type of store you want to use. + */ + private constructor(type: PluginStoreType) { + if (!this.#store) this.#store = stores[type]; + } + + /** + * Adds a plugin to the store. + * @param plugin - Plugin - The plugin to register. + */ + register(plugin: Plugin) { + this.#store![plugin.name] = plugin; + this.#store!._seq?.push(plugin.name); + } + + /** + * Removes a plugin from the store. + * @param plugin - Plugin - The plugin to register. + */ + unregister(plugin: Plugin) { + delete this.#store![plugin.name]; + omit(this.#store!._seq, plugin.name); + } + + /** + * Return the plugin with the given name from the store. + * @param name - The name of the plugin. + * @returns The plugin object. + */ + get(name: string): Plugin { + return this.#store![name]; + } + + /** + * It invokes the `execPreHook` function of each plugin in the order specified by the `seq` array + * @param ctx - The context object that is passed to the plugin. + * @param [seq] - The sequence of plugins to be executed. If not specified, all plugins will be executed. + */ + async execPreHooks(ctx: OpenFunctionRuntime | null, seq?: string[]) { + await this.#invokePluginBySeq( + ctx, + 'execPreHook', + seq || get(ctx, 'prePlugins', null) + ); + } + + /** + * It invokes the `execPostHook` function of each plugin in the order specified by the `seq` array + * @param ctx - The context object that is passed to the plugin. + * @param [seq] - The sequence of plugins to be executed. If not specified, all plugins will be executed. + */ + async execPostHooks(ctx: OpenFunctionRuntime | null, seq?: string[]) { + await this.#invokePluginBySeq( + ctx, + 'execPostHook', + seq || get(ctx, 'postPlugins', null) + ); + } + + /** + * It invokes a method on each plugin in the sequence. + * @param ctx - OpenFunctionRuntime context object. + * @param method - The method to invoke on the plugin. + * @param [seq] - The sequence of plugins to invoke. If not provided, the default sequence will be used. + */ + async #invokePluginBySeq( + ctx: OpenFunctionRuntime | null, + method: keyof Plugin, + seq: string[] + ) { + const pluginNames = seq ?? this.#store!._seq; + const plugins = this.#store!; + + for (const pluginName of pluginNames) { + const plugin = plugins[pluginName]; + await invoke(plugin, method, ctx, plugins); + } + } +} diff --git a/src/openfunction/function_runtime.ts b/src/openfunction/runtime.ts similarity index 67% rename from src/openfunction/function_runtime.ts rename to src/openfunction/runtime.ts index ea28a44c..ab758e4d 100644 --- a/src/openfunction/function_runtime.ts +++ b/src/openfunction/runtime.ts @@ -1,14 +1,18 @@ import {env} from 'process'; -import {chain, get, has, extend} from 'lodash'; +import {chain, get, has, extend, isPlainObject} from 'lodash'; import {Request, Response} from 'express'; import {DaprClient, CommunicationProtocolEnum} from '@dapr/dapr'; +import {OpenFunction} from '../functions'; + import { OpenFunctionComponent, OpenFunctionContext, ContextUtils, -} from './function_context'; +} from './context'; + +import {Plugin, PluginStore} from './plugin'; /** * Defining the interface of the HttpTarget. @@ -40,11 +44,19 @@ export abstract class OpenFunctionRuntime { */ protected trigger?: OpenFunctionTrigger; + /** + * An object to hold local data. + * TODO: Clarify the usage of this property + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly locals: Record; + /** * Constructor of the OpenFunctionRuntime. */ constructor(context: OpenFunctionContext) { this.context = context; + this.locals = {}; } /** @@ -74,6 +86,40 @@ export abstract class OpenFunctionRuntime { }); } + /** + * It takes a user function and a context object, and returns a function that executes the user + * function with the context object, and executes all the pre and post hooks before and after the user function. + * @param userFunction - The function that you want to wrap. + * @param context - This is the context object that is passed to the user function. + * @returns A function that takes in data and returns a promise. + */ + static WrapUserFunction( + userFunction: OpenFunction, + context: OpenFunctionContext | OpenFunctionRuntime + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): (data: any) => Promise { + const ctx: OpenFunctionRuntime = !isPlainObject(context) + ? (context as OpenFunctionRuntime) + : OpenFunctionRuntime.ProxyContext(context as OpenFunctionContext); + + // Load plugin stores + const userPlugins = PluginStore.Instance(); + const sysPlugins = PluginStore.Instance(PluginStore.Type.BUILTIN); + + return async data => { + // Execute pre hooks, system plugins go first + await sysPlugins.execPreHooks(ctx); + await userPlugins.execPreHooks(ctx); + + // Execute user function + await userFunction(ctx, data); + + // Execute pre hooks, system plugins go last + await userPlugins.execPostHooks(ctx); + await sysPlugins.execPostHooks(ctx); + }; + } + /** * Getter for the port of Dapr sidecar */ @@ -109,6 +155,19 @@ export abstract class OpenFunctionRuntime { this.trigger = extend(this.trigger, {req, res}); } + /** + * Get a plugin from the plugin store, or if it doesn't exist, get it from the built-in plugin store. + * + * @param name - The name of the plugin to get. + * @returns A plugin object + */ + getPlugin(name: string): Plugin { + return ( + PluginStore.Instance().get(name) || + PluginStore.Instance(PluginStore.Type.BUILTIN).get(name) + ); + } + /** * The promise that send data to certain ouput. */ diff --git a/src/options.ts b/src/options.ts index c3611304..cb78aea8 100644 --- a/src/options.ts +++ b/src/options.ts @@ -17,8 +17,9 @@ import {resolve} from 'path'; import * as Debug from 'debug'; import * as minimist from 'minimist'; +import {OpenFunctionContext} from './openfunction/context'; + import {SignatureType, isValidSignatureType} from './types'; -import {OpenFunctionContext} from './openfunction/function_context'; const debug = Debug('common:options'); diff --git a/test/data/mock/context.ts b/test/data/mock/context.ts new file mode 100644 index 00000000..d1f43650 --- /dev/null +++ b/test/data/mock/context.ts @@ -0,0 +1,69 @@ +import {OpenFunctionContext} from '../../../src'; + +export const KnativeBase: OpenFunctionContext = { + name: 'test-context', + version: '1.0.0', + runtime: 'Knative', + outputs: { + file1: { + componentName: 'local', + componentType: 'bindings.localstorage', + metadata: { + fileName: 'my-file1.txt', + }, + }, + file2: { + componentName: 'local', + componentType: 'bindings.localstorage', + metadata: { + fileName: 'my-file2.txt', + }, + }, + }, +}; + +export const AsyncBase: OpenFunctionContext = { + name: 'test-context', + version: '1.0.0', + runtime: 'Async', + port: '8080', + inputs: { + cron: { + uri: 'cron_input', + componentName: 'binding-cron', + componentType: 'bindings.cron', + }, + mqtt_binding: { + uri: 'default', + componentName: 'binding-mqtt', + componentType: 'bindings.mqtt', + }, + mqtt_sub: { + uri: 'webup', + componentName: 'pubsub-mqtt', + componentType: 'pubsub.mqtt', + }, + }, + outputs: { + cron: { + uri: 'cron_output', + operation: 'delete', + componentName: 'binding-cron', + componentType: 'bindings.cron', + }, + localfs: { + uri: 'localstorage', + operation: 'create', + componentName: 'binding-localfs', + componentType: 'bindings.localstorage', + metadata: { + fileName: 'output-file.txt', + }, + }, + mqtt_pub: { + uri: 'webup_pub', + componentName: 'pubsub-mqtt', + componentType: 'pubsub.mqtt', + }, + }, +}; diff --git a/test/data/mock/index.ts b/test/data/mock/index.ts new file mode 100644 index 00000000..eba4e29c --- /dev/null +++ b/test/data/mock/index.ts @@ -0,0 +1,2 @@ +export * as Context from './context'; +export * as Payload from './payload'; diff --git a/test/data/mock/payload.ts b/test/data/mock/payload.ts new file mode 100644 index 00000000..57bca547 --- /dev/null +++ b/test/data/mock/payload.ts @@ -0,0 +1,19 @@ +import {CloudEvent} from '../../../src'; + +export interface IPayload { + RAW: object; + CE?: CloudEvent; +} + +export const Plain: IPayload = {RAW: {some: 'payload'}}; + +Plain.CE = { + specversion: '1.0', + id: 'test-1234-1234', + type: 'ce.openfunction', + time: '2020-05-13T01:23:45Z', + subject: 'test-subject', + source: 'https://github.com/OpenFunction/functions-framework-nodejs', + datacontenttype: 'application/json', + data: Plain.RAW, +}; diff --git a/test/data/plugins/constants.mjs b/test/data/plugins/constants.mjs new file mode 100644 index 00000000..179502bc --- /dev/null +++ b/test/data/plugins/constants.mjs @@ -0,0 +1,11 @@ +import {Plugin} from '../../../build/src/index.js'; + +export class Numbers extends Plugin { + bin = 2; + oct = 8; + hex = 16; + + constructor() { + super('numbers', 'v1'); + } +} diff --git a/test/data/plugins/counters.mjs b/test/data/plugins/counters.mjs new file mode 100644 index 00000000..5ba83214 --- /dev/null +++ b/test/data/plugins/counters.mjs @@ -0,0 +1,65 @@ +import {Plugin} from '../../../build/src/index.js'; + +const sleep = ms => new Promise(r => setTimeout(r, ms)); + +export class TickTock extends Plugin { + value = 0; + + constructor() { + super('ticktock', 'v1'); + } + + async execPreHook(ctx, plugins) { + this.value++; + } + + async execPostHook(ctx, plugins) { + this.value--; + } +} + +export class Countdown extends Plugin { + // start: number; + // end: number; + // step: number; + // current: number; + + constructor() { + super('countdown', 'v1'); + } + + async execPreHook(ctx, plugins) { + // Initialize step value from another plugin + this.step = plugins['numbers'].bin; + } + + async execPostHook(ctx, plugins) { + // Load start value from context local data + if (!this.start) { + this.start = ctx.locals.start ?? 0; + this.end = ctx.locals.end ?? 0; + this.current = this.start; + } + if (this.current === this.end) return; + + // Sleep "10 milliseconds" x "current value" + await sleep(10 * this.current); + + // Execute main countdown logic + this.countdown(ctx, plugins); + + // Try to end the test if necessary + if (this.current === this.end) ctx.locals.done?.(); + } + + countdown(ctx, plugins) { + // Load self plugin instance + const self = plugins[this.name]; + + // Count down by step value, and save current stop + self.current -= self.step; + + // Calibrate current value if it goes below the end value + if (self.current < self.end) self.current = self.end; + } +} diff --git a/test/data/plugins/errorMissAll.js b/test/data/plugins/errorMissAll.js deleted file mode 100644 index 413b0248..00000000 --- a/test/data/plugins/errorMissAll.js +++ /dev/null @@ -1,6 +0,0 @@ -class ErrorPlugin{ - static Version = "v1"; - static Name = "error-miss-all-plugin"; -} - -module.exports = ErrorPlugin; diff --git a/test/data/plugins/errorMissName.js b/test/data/plugins/errorMissName.js deleted file mode 100644 index c1266036..00000000 --- a/test/data/plugins/errorMissName.js +++ /dev/null @@ -1,22 +0,0 @@ -class ErrorPlugin{ - static Version = "v1"; - // static Name = "error-plugin" - constructor(){ - console.log(`init error plugins`); - } - async execPreHook(ctx){ - console.log(`-----------error plugin pre hook-----------`); - } - execPostHook(ctx){ - console.log(`-----------error plugin post hook-----------`); - } - get(filedName){ - for(let key in this){ - if(key === filedName){ - return this[key]; - } - } - } -} - -module.exports = {ErrorPlugin}; diff --git a/test/data/plugins/errorMissVersion.js b/test/data/plugins/errorMissVersion.js deleted file mode 100644 index aaf3087c..00000000 --- a/test/data/plugins/errorMissVersion.js +++ /dev/null @@ -1,22 +0,0 @@ -class ErrorPlugin{ - // static Version = "v1" - static Name = "error-miss-version-plugin" - constructor(){ - console.log(`init error plugins`); - } - async execPreHook(ctx){ - console.log(`-----------error plugin pre hook-----------`); - } - execPostHook(ctx){ - console.log(`-----------error plugin post hook-----------`); - } - get(filedName){ - for(let key in this){ - if(key === filedName){ - return this[key]; - } - } - } -} - -module.exports = {ErrorPlugin}; diff --git a/test/data/plugins/noname.mjs b/test/data/plugins/noname.mjs new file mode 100644 index 00000000..be122097 --- /dev/null +++ b/test/data/plugins/noname.mjs @@ -0,0 +1,7 @@ +import {Plugin} from '../../../build/src/index.js'; + +export class Noname extends Plugin { + constructor() { + super(' ', 'v0'); + } +} diff --git a/test/data/plugins/plugindemo.js b/test/data/plugins/plugindemo.js deleted file mode 100644 index 6fd4ea12..00000000 --- a/test/data/plugins/plugindemo.js +++ /dev/null @@ -1,32 +0,0 @@ -function sleep(){ - return new Promise(resolve => setTimeout(resolve,3000)); -} -class DemoPlugin{ - static Version = "v1"; - static Name = "demo-plugin"; - id = '666'; - constructor(){ - console.log(`init demo plugins`); - } - async execPreHook(ctx){ - console.log(`-----------demo plugin pre hook-----------`); - ctx['pre'] = 'pre-exec'; - await sleep(); - console.log(`-----------pre sleep 3----------`) - } - async execPostHook(ctx){ - console.log(`-----------demo plugin post hook-----------`); - ctx['post'] = 'post-exec'; - console.log(`-----------send post-----------`); - } - get(filedName){ - for(let key in this){ - if(key === filedName){ - return this[key]; - } - } - } -} - -// module.exports = {DemoPlugin}; -exports.DemoPlugin = DemoPlugin; diff --git a/test/data/test_data/async_plugin.ts b/test/data/test_data/async_plugin.ts deleted file mode 100644 index c281a314..00000000 --- a/test/data/test_data/async_plugin.ts +++ /dev/null @@ -1,111 +0,0 @@ -import {OpenFunctionContext} from '../../../src/openfunction/function_context'; -import {FrameworkOptions} from '../../../src/options'; - -export const TEST_CONTEXT: OpenFunctionContext = { - name: 'test-context', - version: '1.0.0', - runtime: 'Async', - port: '8080', - inputs: { - cron: { - uri: 'cron_input', - componentName: 'binding-cron', - componentType: 'bindings.cron', - }, - mqtt_binding: { - uri: 'default', - componentName: 'binding-mqtt', - componentType: 'bindings.mqtt', - }, - mqtt_sub: { - uri: 'webup', - componentName: 'pubsub-mqtt', - componentType: 'pubsub.mqtt', - }, - }, - outputs: { - cron: { - uri: 'cron_output', - operation: 'delete', - componentName: 'binding-cron', - componentType: 'bindings.cron', - }, - localfs: { - uri: 'localstorage', - operation: 'create', - componentName: 'binding-localfs', - componentType: 'bindings.localstorage', - metadata: { - fileName: 'output-file.txt', - }, - }, - mqtt_pub: { - uri: 'webup_pub', - componentName: 'pubsub-mqtt', - componentType: 'pubsub.mqtt', - }, - }, -}; -export const TEST_PLUGIN_OPTIONS: FrameworkOptions = { - port: '', - target: '', - sourceLocation: process.cwd() + '/test/data', - signatureType: 'event', - printHelp: false, - context: { - name: 'test-context-plugin', - version: '1.0.0', - runtime: 'Async', - port: '8080', - inputs: { - cron: { - uri: 'cron_input', - componentName: 'binding-cron', - componentType: 'bindings.cron', - }, - mqtt_binding: { - uri: 'default', - componentName: 'binding-mqtt', - componentType: 'bindings.mqtt', - }, - mqtt_sub: { - uri: 'webup', - componentName: 'pubsub-mqtt', - componentType: 'pubsub.mqtt', - }, - }, - outputs: { - cron: { - uri: 'cron_output', - operation: 'delete', - componentName: 'binding-cron', - componentType: 'bindings.cron', - }, - localfs: { - uri: 'localstorage', - operation: 'create', - componentName: 'binding-localfs', - componentType: 'bindings.localstorage', - metadata: { - fileName: 'output-file.txt', - }, - }, - mqtt_pub: { - uri: 'webup_pub', - componentName: 'pubsub-mqtt', - componentType: 'pubsub.mqtt', - }, - }, - prePlugins: ['demo-plugin'], - postPlugins: ['demo-plugin'], - }, -}; -export const TEST_PAYLOAD = {data: 'hello world'}; -export const TEST_CLOUD_EVENT = { - specversion: '1.0', - id: 'test-1234-1234', - type: 'ce.openfunction', - source: 'https://github.com/OpenFunction/functions-framework-nodejs', - traceparent: '00-65088630f09e0a5359677a7429456db7-97f23477fb2bf5ec-01', - data: TEST_PAYLOAD, -}; diff --git a/test/function_wrappers.ts b/test/function_wrappers.ts index 67e3591a..48ef82f2 100644 --- a/test/function_wrappers.ts +++ b/test/function_wrappers.ts @@ -2,7 +2,7 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import {Request, Response} from 'express'; -import {OpenFunctionContext} from '../src/openfunction/function_context'; +import {OpenFunctionContext} from '../src/openfunction/context'; import {Context, CloudEvent, OpenFunctionRuntime} from '../src/functions'; import {wrapUserFunction} from '../src/function_wrappers'; diff --git a/test/integration/async_server.ts b/test/integration/async_server.ts index e9cddd53..8009a44e 100644 --- a/test/integration/async_server.ts +++ b/test/integration/async_server.ts @@ -1,20 +1,20 @@ -/* eslint-disable no-restricted-properties */ import {deepStrictEqual, ifError, ok} from 'assert'; import {createServer} from 'net'; -import {get, isEmpty} from 'lodash'; +import {fill, get, isEmpty} from 'lodash'; import * as shell from 'shelljs'; import * as MQTT from 'aedes'; import getAysncServer from '../../src/openfunction/async_server'; +import {getFunctionPlugins} from '../../src/loader'; -import { - TEST_CLOUD_EVENT, - TEST_CONTEXT, - TEST_PAYLOAD, -} from '../data/test_data/async_plugin'; +import {Context, Payload} from '../data/mock'; -describe('OpenFunction - Async - Binding', () => { +const TEST_CONTEXT = Context.AsyncBase; +const TEST_PAYLOAD = Payload.Plain.RAW; +const TEST_PAYLOAD_CE = Payload.Plain.CE; + +describe('OpenFunction - Async', () => { const APPID = 'async.dapr'; const broker = MQTT.Server(); const server = createServer(broker.handle); @@ -43,20 +43,24 @@ describe('OpenFunction - Async - Binding', () => { }); it('stop cron after first trigger recived', done => { - const app = getAysncServer((ctx, data) => { - // Assert that user function receives data from input binding - ok(data); - - // Assert that context data was passed through - deepStrictEqual(get(ctx, 'runtime'), TEST_CONTEXT.runtime); - deepStrictEqual( - get(ctx, 'inputs.cron.uri'), - TEST_CONTEXT.inputs!.cron.uri - ); + const app = getAysncServer( + (ctx, data) => { + // Assert that user function receives data from input binding + ok(data); - // Then stop the cron scheduler - ctx.send({}, 'cron').then(() => app.stop().finally(done)); - }, TEST_CONTEXT); + // Assert that context data was passed through + deepStrictEqual(get(ctx, 'runtime'), TEST_CONTEXT.runtime); + deepStrictEqual( + get(ctx, 'inputs.cron.uri'), + TEST_CONTEXT.inputs!.cron.uri + ); + + // Then stop the cron scheduler + ctx.send({}, 'cron').then(() => app.stop().finally(done)); + }, + + TEST_CONTEXT + ); app.start(); }); @@ -95,27 +99,31 @@ describe('OpenFunction - Async - Binding', () => { }); it('mqtt sub w/ pub output', done => { - const app = getAysncServer((ctx, data) => { - if (isEmpty(data)) return; - - // Assert that user function receives correct data from input binding - deepStrictEqual(data, TEST_PAYLOAD); - - // Then forward received data to output channel - const output = 'mqtt_pub'; - broker.subscribe( - get(TEST_CONTEXT, `outputs.${output}.uri`), - // eslint-disable-next-line @typescript-eslint/no-unused-vars - (packet, _) => { - const payload = JSON.parse(Buffer.from(packet.payload).toString()); - deepStrictEqual(payload.data, TEST_PAYLOAD); - app.stop().finally(done); - }, - () => { - ctx.send(TEST_PAYLOAD, output); - } - ); - }, TEST_CONTEXT); + const app = getAysncServer( + (ctx, data) => { + if (isEmpty(data)) return; + + // Assert that user function receives correct data from input binding + deepStrictEqual(data, TEST_PAYLOAD); + + // Then forward received data to output channel + const output = 'mqtt_pub'; + broker.subscribe( + get(TEST_CONTEXT, `outputs.${output}.uri`), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + (packet, _) => { + const payload = JSON.parse(Buffer.from(packet.payload).toString()); + deepStrictEqual(payload.data, TEST_PAYLOAD); + app.stop().finally(done); + }, + () => { + ctx.send(TEST_PAYLOAD, output); + } + ); + }, + + TEST_CONTEXT + ); // First, we start the async server app.start().then(() => { @@ -124,7 +132,7 @@ describe('OpenFunction - Async - Binding', () => { { cmd: 'publish', topic: TEST_CONTEXT.inputs!.mqtt_sub.uri!, - payload: JSON.stringify(TEST_CLOUD_EVENT), + payload: JSON.stringify(TEST_PAYLOAD_CE), qos: 0, retain: false, dup: false, @@ -133,4 +141,45 @@ describe('OpenFunction - Async - Binding', () => { ); }); }); + + it('mqtt binding w/ custom plugins', done => { + getFunctionPlugins(process.cwd() + '/test/data').then(plugins => { + const start = get(plugins!.numbers, 'oct'); + + const app = getAysncServer( + (ctx, data) => { + // Assert that user function receives correct data from input binding + deepStrictEqual(get(data, 'start'), start); + + // Set local data for post hook plugin + ctx.locals.start = start; + ctx.locals.end = -start; + + // Passthrough test done handler + ctx.locals.done = done; + }, + { + ...TEST_CONTEXT, + prePlugins: ['countdown'], + postPlugins: fill(Array(start), 'countdown'), + } + ); + + // First, we start the async server + app.start().then(() => { + // Then, we send a number as start value to user function + broker.publish( + { + cmd: 'publish', + topic: 'default', + payload: JSON.stringify({start}), + qos: 0, + retain: false, + dup: false, + }, + err => ifError(err) + ); + }); + }); + }); }); diff --git a/test/integration/async_server_plugin.ts b/test/integration/async_server_plugin.ts deleted file mode 100644 index c7e228ea..00000000 --- a/test/integration/async_server_plugin.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* eslint-disable no-restricted-properties */ -import {deepStrictEqual, ifError} from 'assert'; -import {createServer} from 'net'; - -import {get, isEmpty} from 'lodash'; -import * as shell from 'shelljs'; -import * as MQTT from 'aedes'; - -import getAysncServer from '../../src/openfunction/async_server'; -import {getUserPlugins} from '../../src/loader'; -import assert = require('assert'); -import { - TEST_CLOUD_EVENT, - TEST_CONTEXT, - TEST_PAYLOAD, - TEST_PLUGIN_OPTIONS, -} from '../data/test_data/async_plugin'; - -describe('OpenFunction - Async - Binding with plugin', () => { - const APPID = 'async.dapr'; - const broker = MQTT.Server(); - const server = createServer(broker.handle); - - before(done => { - // Start simple plain MQTT server via aedes - server.listen(1883, () => { - // Try to run Dapr sidecar and listen for the async server - shell.exec( - `dapr run -H 3500 -G 50001 -p ${TEST_CONTEXT.port} -d ./test/data/components/async -a ${APPID} --log-level debug`, - { - silent: true, - async: true, - } - ); - done(); - }); - }); - - after(done => { - // Stop dapr sidecar process - shell.exec(`dapr stop ${APPID}`, { - silent: true, - }); - server.close(); - broker.close(done); - }); - - it('mqtt sub w/ pub output with demo plugin', done => { - const app = getAysncServer((ctx, data) => { - if (isEmpty(data)) return; - - const context: any = ctx as any; - assert(context['pre'] === 'pre-exec'); - context['pre'] = 'main-exec'; - - // Assert that user function receives correct data from input binding - deepStrictEqual(data, TEST_PAYLOAD); - console.log(data); - // Then forward received data to output channel - const output = 'mqtt_pub'; - broker.subscribe( - get(TEST_PLUGIN_OPTIONS.context!, `outputs.${output}.uri`), - // eslint-disable-next-line @typescript-eslint/no-unused-vars - (packet, _) => { - const payload = JSON.parse(Buffer.from(packet.payload).toString()); - deepStrictEqual(payload.data, TEST_PAYLOAD); - app - .stop() - .then(() => { - assert(context['pre'] === 'main-exec'); - assert(context['post'] === 'post-exec'); - }) - .finally(done); - }, - () => { - ctx.send(TEST_PAYLOAD, output); - } - ); - }, TEST_PLUGIN_OPTIONS.context!); - - // First, we start the async server - app.start().then(async () => { - await getUserPlugins(TEST_PLUGIN_OPTIONS); - console.log(TEST_PLUGIN_OPTIONS); - // Then, we send a cloudevent format message to server - broker.publish( - { - cmd: 'publish', - topic: TEST_PLUGIN_OPTIONS.context!.inputs!.mqtt_sub.uri!, - payload: JSON.stringify(TEST_CLOUD_EVENT), - qos: 0, - retain: false, - dup: false, - }, - err => ifError(err) - ); - }); - }); -}); diff --git a/test/integration/cloud_event.ts b/test/integration/cloud_event.ts index ce21b81d..223af5e8 100644 --- a/test/integration/cloud_event.ts +++ b/test/integration/cloud_event.ts @@ -21,19 +21,10 @@ import * as functions from '../../src/index'; import {getTestServer} from '../../src/testing'; import {FUNCTION_STATUS_HEADER_FIELD} from '../../src/types'; +import {Plain} from '../data/mock/payload'; + // A structured CloudEvent -const TEST_CLOUD_EVENT = { - specversion: '1.0', - type: 'com.google.cloud.storage', - source: 'https://github.com/OpenFunction/functions-framework-nodejs', - subject: 'test-subject', - id: 'test-1234-1234', - time: '2020-05-13T01:23:45Z', - datacontenttype: 'application/json', - data: { - some: 'payload', - }, -}; +const TEST_CLOUD_EVENT = Plain.CE!; const TEST_EXTENSIONS = { traceparent: '00-65088630f09e0a5359677a7429456db7-97f23477fb2bf5ec-01', diff --git a/test/integration/http_binding.ts b/test/integration/http_binding.ts index 7c91aa2c..9517cb88 100644 --- a/test/integration/http_binding.ts +++ b/test/integration/http_binding.ts @@ -3,36 +3,18 @@ import {deepStrictEqual} from 'assert'; import * as sinon from 'sinon'; import * as supertest from 'supertest'; import * as shell from 'shelljs'; -import {cloneDeep, forEach, set} from 'lodash'; - -import {OpenFunctionContext} from '../../src/openfunction/function_context'; +import {cloneDeep, forEach, get, set} from 'lodash'; +import {PluginStore} from '../../src/openfunction/plugin'; import {OpenFunctionRuntime} from '../../src/functions'; import {getServer} from '../../src/server'; +import {getFunctionPlugins} from '../../src/loader'; import {FUNCTION_STATUS_HEADER_FIELD} from '../../src/types'; -const TEST_CONTEXT: OpenFunctionContext = { - name: 'test-context', - version: '1.0.0', - runtime: 'Knative', - outputs: { - file1: { - componentName: 'local', - componentType: 'bindings.localstorage', - metadata: { - fileName: 'my-file1.txt', - }, - }, - file2: { - componentName: 'local', - componentType: 'bindings.localstorage', - metadata: { - fileName: 'my-file2.txt', - }, - }, - }, -}; -const TEST_PAYLOAD = {echo: 'hello world'}; +import {Context, Payload} from '../data/mock'; + +const TEST_CONTEXT = Context.KnativeBase; +const TEST_PAYLOAD = Payload.Plain.RAW; describe('OpenFunction - HTTP Binding', () => { const APPID = 'http.dapr'; @@ -50,8 +32,10 @@ describe('OpenFunction - HTTP Binding', () => { } ); - // Wait 5 seconds for dapr sidecar to start - setTimeout(done, 5000); + getFunctionPlugins(process.cwd() + '/test/data').then(() => { + // Wait 5 seconds for dapr sidecar to start + setTimeout(done, 5000); + }); }); after(() => { @@ -79,6 +63,7 @@ describe('OpenFunction - HTTP Binding', () => { testData.forEach(test => { it(test.name, async () => { const context = cloneDeep(TEST_CONTEXT); + context.prePlugins = context.postPlugins = ['ticktock']; forEach(context.outputs, output => set(output, 'operation', test.operation) ); @@ -99,12 +84,17 @@ describe('OpenFunction - HTTP Binding', () => { .send(TEST_PAYLOAD) .expect(test.operation ? 200 : 500) .expect(res => { - !test.operation - ? deepStrictEqual( - res.headers[FUNCTION_STATUS_HEADER_FIELD.toLowerCase()], - 'error' - ) - : deepStrictEqual(res.body, TEST_PAYLOAD); + const tick = get(PluginStore.Instance().get('ticktock'), 'value'); + if (!test.operation) { + deepStrictEqual( + res.headers[FUNCTION_STATUS_HEADER_FIELD.toLowerCase()], + 'error' + ); + deepStrictEqual(tick, 1); + } else { + deepStrictEqual(res.body, TEST_PAYLOAD); + deepStrictEqual(tick, 0); + } }); forEach(context.outputs, output => { diff --git a/test/loader.ts b/test/loader.ts index 35bc528c..52a7589c 100644 --- a/test/loader.ts +++ b/test/loader.ts @@ -13,15 +13,14 @@ // limitations under the License. import * as assert from 'assert'; + import * as express from 'express'; import * as semver from 'semver'; -import * as functions from '../src/functions'; + +import {http, cloudEvent, HttpFunction} from '../src'; import * as loader from '../src/loader'; -import * as FunctionRegistry from '../src/function_registry'; -import {FrameworkOptions} from '../src/options'; -import {Plugin} from '../src'; -describe('loading function', () => { +describe('Load function and plugins', () => { interface TestData { name: string; codeLocation: string; @@ -48,8 +47,7 @@ describe('loading function', () => { test.target, 'http' ); - const userFunction = - loadedFunction?.userFunction as functions.HttpFunction; + const userFunction = loadedFunction?.userFunction as HttpFunction; const returned = userFunction(express.request, express.response); assert.strictEqual(returned, 'PASS'); }); @@ -74,13 +72,13 @@ describe('loading function', () => { ]; for (const test of esmTestData) { - const loadFn: () => Promise = async () => { + const loadFn: () => Promise = async () => { const loadedFunction = await loader.getUserFunction( process.cwd() + test.codeLocation, test.target, 'http' ); - return loadedFunction?.userFunction as functions.HttpFunction; + return loadedFunction?.userFunction as HttpFunction; }; if (semver.lt(process.version, loader.MIN_NODE_VERSION_ESMODULES)) { it(`should fail to load function in an ES module ${test.name}`, async () => { @@ -96,7 +94,7 @@ describe('loading function', () => { } it('loads a declaratively registered function', async () => { - FunctionRegistry.http('registeredFunction', () => { + http('registeredFunction', () => { return 'PASS'; }); const loadedFunction = await loader.getUserFunction( @@ -104,13 +102,13 @@ describe('loading function', () => { 'registeredFunction', 'http' ); - const userFunction = loadedFunction?.userFunction as functions.HttpFunction; + const userFunction = loadedFunction?.userFunction as HttpFunction; const returned = userFunction(express.request, express.response); assert.strictEqual(returned, 'PASS'); }); it('allows a mix of registered and non registered functions', async () => { - FunctionRegistry.http('registeredFunction', () => { + http('registeredFunction', () => { return 'FAIL'; }); const loadedFunction = await loader.getUserFunction( @@ -118,13 +116,13 @@ describe('loading function', () => { 'testFunction', 'http' ); - const userFunction = loadedFunction?.userFunction as functions.HttpFunction; + const userFunction = loadedFunction?.userFunction as HttpFunction; const returned = userFunction(express.request, express.response); assert.strictEqual(returned, 'PASS'); }); it('respects the registered signature type', async () => { - FunctionRegistry.cloudEvent('registeredFunction', () => {}); + cloudEvent('registeredFunction', () => {}); const loadedFunction = await loader.getUserFunction( process.cwd() + '/test/data/with_main', 'registeredFunction', @@ -132,202 +130,16 @@ describe('loading function', () => { ); assert.strictEqual(loadedFunction?.signatureType, 'cloudevent'); }); -}); - -describe('loading plugins', () => { - interface ExceptData { - prePlugins: Array; - postPlugins: Array; - } - interface TestData { - options: FrameworkOptions; - except: ExceptData; - } - const testData: TestData[] = [ - { - options: { - port: '8080', - target: 'helloWorld', - sourceLocation: process.cwd() + '/test/data', - signatureType: 'event', - printHelp: false, - context: { - name: 'demo', - version: '', - runtime: 'ASYNC', - prePlugins: ['demo-plugin'], - postPlugins: ['demo-plugin'], - }, - }, - except: { - prePlugins: ['demo-plugin'], - postPlugins: ['demo-plugin'], - }, - }, - { - options: { - port: '8080', - target: 'helloWorld', - sourceLocation: process.cwd() + '/test/data', - signatureType: 'event', - printHelp: false, - context: { - name: 'demo', - version: '', - runtime: 'ASYNC', - prePlugins: ['demo-plugin'], - postPlugins: [], - }, - }, - except: { - prePlugins: ['demo-plugin'], - postPlugins: [], - }, - }, - { - options: { - port: '8080', - target: 'helloWorld', - sourceLocation: process.cwd() + '/test/data', - signatureType: 'event', - printHelp: false, - context: { - name: 'demo', - version: '', - runtime: 'ASYNC', - prePlugins: [], - postPlugins: [], - }, - }, - except: { - prePlugins: [], - postPlugins: [], - }, - }, - { - options: { - port: '8080', - target: 'helloWorld', - sourceLocation: process.cwd() + '/test/data', - signatureType: 'event', - printHelp: false, - context: { - name: 'error', - version: '', - runtime: 'ASYNC', - prePlugins: ['error-plugin'], - postPlugins: ['error-plugin'], - }, - }, - except: { - prePlugins: [], - postPlugins: [], - }, - }, - { - options: { - port: '8080', - target: 'helloWorld', - sourceLocation: process.cwd() + '/test/data', - signatureType: 'event', - printHelp: false, - context: { - name: 'error', - version: '', - runtime: 'ASYNC', - prePlugins: ['error-miss-version-plugin', 'demo-plugin'], - postPlugins: ['error-miss-version-plugin'], - }, - }, - except: { - prePlugins: ['error-miss-version-plugin', 'demo-plugin'], - postPlugins: ['error-miss-version-plugin'], - }, - }, - ]; - - it('load exits plugins', async () => { - for (const test of testData) { - const options = await loader.getUserPlugins(test.options); - const current: ExceptData = { - prePlugins: [], - postPlugins: [], - }; - - options.context!.prePlugins!.forEach(item => { - assert(typeof item === 'object'); - assert(item.get(Plugin.OFN_PLUGIN_VERSION) === 'v1'); - current.prePlugins.push(item.get(Plugin.OFN_PLUGIN_NAME)); - }); - options.context!.postPlugins!.forEach(item => { - assert(typeof item === 'object'); - assert(item.get(Plugin.OFN_PLUGIN_VERSION) === 'v1'); - current.postPlugins.push(item.get(Plugin.OFN_PLUGIN_NAME)); - }); - - assert.deepStrictEqual(current, test.except); - } - }); - - const test: TestData = { - options: { - port: '8080', - target: 'helloWorld', - sourceLocation: process.cwd() + '/test/data', - signatureType: 'event', - printHelp: false, - context: { - name: 'error', - version: '', - runtime: 'ASYNC', - prePlugins: [''], - postPlugins: [''], - }, - }, - except: { - prePlugins: [''], - postPlugins: [''], - }, - }; - - function copyAndSet(name: string): TestData { - const data: TestData = JSON.parse(JSON.stringify(test)); - data.options.context!.prePlugins![0] = name; - data.options.context!.postPlugins![0] = name; - data.except.postPlugins[0] = name; - data.except.prePlugins[0] = name; - return data; - } - - it('user plugin miss all', async () => { - const data = copyAndSet('error-miss-all-plugin'); - const options = await loader.getUserPlugins(data.options); - assert(typeof options.context!.prePlugins![0] === 'object'); - assert( - options.context!.prePlugins![0].get(Plugin.OFN_PLUGIN_NAME) === - 'error-miss-all-plugin' + it('should only load valid plugin class and ignore other', async () => { + const loadedPlugins = await loader.getFunctionPlugins( + process.cwd() + '/test/data' ); - assert(options.context!.prePlugins![0].execPreHook); - assert(options.context!.prePlugins![0].execPostHook); - }); + assert.ok(loadedPlugins); - it('load multi plugins ', async () => { - const data: FrameworkOptions = { - port: '8080', - target: 'helloWorld', - sourceLocation: process.cwd() + '/test/data', - signatureType: 'event', - printHelp: false, - context: { - name: 'demo', - version: '', - runtime: 'ASYNC', - prePlugins: ['demo-plugin', 'error-miss-all-plugin'], - postPlugins: ['demo-plugin', 'error-miss-all-plugin'], - }, - }; - assert.ok(await loader.getUserPlugins(data)); - console.log(data); + ['noname'].forEach(v => assert.ok(!loadedPlugins[v])); + ['numbers', 'ticktock', 'countdown'].forEach(v => + assert.strictEqual(loadedPlugins[v]?.name, v) + ); }); }); diff --git a/test/plugin.ts b/test/plugin.ts new file mode 100644 index 00000000..000bd46c --- /dev/null +++ b/test/plugin.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import * as assert from 'assert'; + +import {fill, get, random, range} from 'lodash'; + +import {PluginStore, Plugin} from '../src/openfunction/plugin'; +import {getFunctionPlugins} from '../src/loader'; + +class Concater extends Plugin { + index = 0; + value = ''; + + constructor() { + super('concater', 'v1'); + } + + async execPreHook(): Promise { + await new Promise(r => setTimeout(r, random(0, 10) * 10)); + this.value += this.index++; + } +} + +describe('Store for custom and builtin plugins', () => { + before(async () => await getFunctionPlugins(process.cwd() + '/test/data')); + + const customs = PluginStore.Instance(); + + it('can retrieve plugin by name', () => { + ['numbers', 'ticktock', 'countdown'].forEach(v => + assert.strictEqual(customs.get(v)?.name, v) + ); + }); + + it('can execute custom plugins by sequence', async () => { + const size = random(3, 5); + const ticktock = customs.get('ticktock'); + const seq = fill(Array(size), ticktock.name); + + await customs.execPreHooks(null, seq); + assert.strictEqual(get(ticktock, 'value'), size); + + await customs.execPostHooks(null, seq); + assert.strictEqual(get(ticktock, 'value'), 0); + }); + + it('ensures the sequence of long running async plugins', async () => { + customs.register(new Concater()); + + const size = random(0, 9); + const concater = customs.get('concater'); + const seq = fill(Array(size), concater.name); + + await customs.execPreHooks(null, seq); + assert.strictEqual(get(concater, 'index'), size); + assert.strictEqual(get(concater, 'value'), range(size).join('')); + }); +});