diff --git a/package.json b/package.json index dbd9ce23..ad0df6b1 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ }, "repository": "GoogleCloudPlatform/functions-framework-nodejs", "main": "build/src/index.js", - "types": "build/src/functions.d.ts", + "types": "build/src/index.d.ts", "dependencies": { "body-parser": "^1.18.3", "express": "^4.16.4", diff --git a/src/function_registry.ts b/src/function_registry.ts new file mode 100644 index 00000000..7c113e0f --- /dev/null +++ b/src/function_registry.ts @@ -0,0 +1,73 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {HttpFunction, CloudEventFunction, HandlerFunction} from './functions'; +import {SignatureType} from './types'; + +interface RegisteredFunction { + signatureType: SignatureType; + userFunction: HandlerFunction; +} + +/** + * Singleton map to hold the registered functions + */ +const registrationContainer = new Map(); + +/** + * Helper method to store a registered function in the registration container + */ +const register = ( + functionName: string, + signatureType: SignatureType, + userFunction: HandlerFunction +): void => { + registrationContainer.set(functionName, { + signatureType, + userFunction, + }); +}; + +/** + * Get a declaratively registered function + * @param functionName the name with which the function was registered + * @returns the registered function and signature type or undefined no function matching + * the provided name has been registered. + */ +export const getRegisteredFunction = ( + functionName: string +): RegisteredFunction | undefined => { + return registrationContainer.get(functionName); +}; + +/** + * Register a function that responds to HTTP requests. + * @param functionName the name of the function + * @param handler the function to invoke when handling HTTP requests + */ +export const http = (functionName: string, handler: HttpFunction): void => { + register(functionName, 'http', handler); +}; + +/** + * Register a function that handles CloudEvents. + * @param functionName the name of the function + * @param handler the function to trigger when handling cloudevents + */ +export const cloudevent = ( + functionName: string, + handler: CloudEventFunction +): void => { + register(functionName, 'cloudevent', handler); +}; diff --git a/src/index.ts b/src/index.ts index 88debeef..eb7d4819 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,49 +14,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Functions framework entry point that configures and starts Node.js server -// that runs user's code on HTTP request. -import {getUserFunction} from './loader'; -import {ErrorHandler} from './invoker'; -import {getServer} from './server'; -import {parseOptions, helpText, OptionsError} from './options'; +import {main} from './main'; -(async () => { - try { - const options = parseOptions(); +export * from './functions'; - if (options.printHelp) { - console.error(helpText); - return; - } - const userFunction = await getUserFunction( - options.sourceLocation, - options.target - ); - if (!userFunction) { - console.error('Could not load the function, shutting down.'); - // eslint-disable-next-line no-process-exit - process.exit(1); - } - const server = getServer(userFunction!, options.signatureType); - const errorHandler = new ErrorHandler(server); - server - .listen(options.port, () => { - errorHandler.register(); - if (process.env.NODE_ENV !== 'production') { - console.log('Serving function...'); - console.log(`Function: ${options.target}`); - console.log(`Signature type: ${options.signatureType}`); - console.log(`URL: http://localhost:${options.port}/`); - } - }) - .setTimeout(0); // Disable automatic timeout on incoming connections. - } catch (e) { - if (e instanceof OptionsError) { - console.error(e.message); - // eslint-disable-next-line no-process-exit - process.exit(1); - } - throw e; - } -})(); +export {http, cloudevent} from './function_registry'; + +// Call the main method to load the user code and start the http server. +main(); diff --git a/src/loader.ts b/src/loader.ts index fd24c6d8..b655ef3a 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -22,10 +22,9 @@ import * as path from 'path'; import * as semver from 'semver'; import * as readPkgUp from 'read-pkg-up'; import {pathToFileURL} from 'url'; -/** - * Import function signature type's definition. - */ import {HandlerFunction} from './functions'; +import {SignatureType} from './types'; +import {getRegisteredFunction} from './function_registry'; // Dynamic import function required to load user code packaged as an // ES module is only available on Node.js v13.2.0 and up. @@ -85,8 +84,12 @@ const dynamicImport = new Function( */ export async function getUserFunction( codeLocation: string, - functionTarget: string -): Promise { + functionTarget: string, + signatureType: SignatureType +): Promise<{ + userFunction: HandlerFunction; + signatureType: SignatureType; +} | null> { try { const functionModulePath = getFunctionModulePath(codeLocation); if (functionModulePath === null) { @@ -111,6 +114,13 @@ export async function getUserFunction( functionModule = require(functionModulePath); } + // If the customer declaratively registered a function matching the target + // return that. + const registeredFunction = getRegisteredFunction(functionTarget); + if (registeredFunction) { + return registeredFunction; + } + let userFunction = functionTarget .split('.') .reduce((code, functionTargetPart) => { @@ -143,7 +153,7 @@ export async function getUserFunction( return null; } - return userFunction as HandlerFunction; + return {userFunction: userFunction as HandlerFunction, signatureType}; } catch (ex) { let additionalHint: string; // TODO: this should be done based on ex.code rather than string matching. diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 00000000..0a9df43b --- /dev/null +++ b/src/main.ts @@ -0,0 +1,66 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Functions framework entry point that configures and starts Node.js server +// that runs user's code on HTTP request. +import {getUserFunction} from './loader'; +import {ErrorHandler} from './invoker'; +import {getServer} from './server'; +import {parseOptions, helpText, OptionsError} from './options'; + +/** + * Main entrypoint for the functions framework that loads the user's function + * and starts the HTTP server. + */ +export const main = async () => { + try { + const options = parseOptions(); + + if (options.printHelp) { + console.error(helpText); + return; + } + const loadedFunction = await getUserFunction( + options.sourceLocation, + options.target, + options.signatureType + ); + if (!loadedFunction) { + console.error('Could not load the function, shutting down.'); + // eslint-disable-next-line no-process-exit + process.exit(1); + } + const {userFunction, signatureType} = loadedFunction; + const server = getServer(userFunction!, signatureType); + const errorHandler = new ErrorHandler(server); + server + .listen(options.port, () => { + errorHandler.register(); + if (process.env.NODE_ENV !== 'production') { + console.log('Serving function...'); + console.log(`Function: ${options.target}`); + console.log(`Signature type: ${signatureType}`); + console.log(`URL: http://localhost:${options.port}/`); + } + }) + .setTimeout(0); // Disable automatic timeout on incoming connections. + } catch (e) { + if (e instanceof OptionsError) { + console.error(e.message); + // eslint-disable-next-line no-process-exit + process.exit(1); + } + throw e; + } +}; diff --git a/test/function_registry.ts b/test/function_registry.ts new file mode 100644 index 00000000..6ff40960 --- /dev/null +++ b/test/function_registry.ts @@ -0,0 +1,37 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import * as assert from 'assert'; +import * as FunctionRegistry from '../src/function_registry'; + +describe('function_registry', () => { + it('can register http functions', () => { + FunctionRegistry.http('httpFunction', () => 'HTTP_PASS'); + const { + userFunction, + signatureType, + } = FunctionRegistry.getRegisteredFunction('httpFunction')!; + assert.deepStrictEqual('http', signatureType); + assert.deepStrictEqual((userFunction as () => string)(), 'HTTP_PASS'); + }); + + it('can register cloudevent functions', () => { + FunctionRegistry.cloudevent('ceFunction', () => 'CE_PASS'); + const { + userFunction, + signatureType, + } = FunctionRegistry.getRegisteredFunction('ceFunction')!; + assert.deepStrictEqual('cloudevent', signatureType); + assert.deepStrictEqual((userFunction as () => string)(), 'CE_PASS'); + }); +}); diff --git a/test/loader.ts b/test/loader.ts index b8b3fe19..0c68bd93 100644 --- a/test/loader.ts +++ b/test/loader.ts @@ -17,6 +17,7 @@ import * as express from 'express'; import * as semver from 'semver'; import * as functions from '../src/functions'; import * as loader from '../src/loader'; +import * as FunctionRegistry from '../src/function_registry'; describe('loading function', () => { interface TestData { @@ -40,11 +41,13 @@ describe('loading function', () => { for (const test of testData) { it(`should load ${test.name}`, async () => { - const loadedFunction = (await loader.getUserFunction( + const loadedFunction = await loader.getUserFunction( process.cwd() + test.codeLocation, - test.target - )) as functions.HttpFunction; - const returned = loadedFunction(express.request, express.response); + test.target, + 'http' + ); + const userFunction = loadedFunction?.userFunction as functions.HttpFunction; + const returned = userFunction(express.request, express.response); assert.strictEqual(returned, 'PASS'); }); } @@ -69,10 +72,12 @@ describe('loading function', () => { for (const test of esmTestData) { const loadFn: () => Promise = async () => { - return loader.getUserFunction( + const loadedFunction = await loader.getUserFunction( process.cwd() + test.codeLocation, - test.target - ) as Promise; + test.target, + 'http' + ); + return loadedFunction?.userFunction as functions.HttpFunction; }; if (semver.lt(process.version, loader.MIN_NODE_VERSION_ESMODULES)) { it(`should fail to load function in an ES module ${test.name}`, async () => { @@ -86,4 +91,42 @@ describe('loading function', () => { }); } } + + it('loads a declaratively registered function', async () => { + FunctionRegistry.http('registeredFunction', () => { + return 'PASS'; + }); + const loadedFunction = await loader.getUserFunction( + process.cwd() + '/test/data/with_main', + 'registeredFunction', + 'http' + ); + const userFunction = loadedFunction?.userFunction as functions.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', () => { + return 'FAIL'; + }); + const loadedFunction = await loader.getUserFunction( + process.cwd() + '/test/data/with_main', + 'testFunction', + 'http' + ); + const userFunction = loadedFunction?.userFunction as functions.HttpFunction; + const returned = userFunction(express.request, express.response); + assert.strictEqual(returned, 'PASS'); + }); + + it('respects the registered signature type', async () => { + FunctionRegistry.cloudevent('registeredFunction', () => {}); + const loadedFunction = await loader.getUserFunction( + process.cwd() + '/test/data/with_main', + 'registeredFunction', + 'http' + ); + assert.strictEqual(loadedFunction?.signatureType, 'cloudevent'); + }); });