faas-js-runtime
Advanced tools
Comparing version 0.9.1 to 0.9.3
import { Server } from 'http'; | ||
import { CloudEvent } from 'cloudevents'; | ||
import { Context } from './lib/context'; | ||
import { CloudEventFunction, HTTPFunction } from './lib/types'; | ||
// Invokable describes the function signature for a function that can be invoked by the server. | ||
export type Invokable = CloudEventFunction | HTTPFunction; | ||
// start starts the server for the given function. | ||
export declare const start: { | ||
@@ -9,2 +12,3 @@ (func: Invokable, options?: InvokerOptions): Promise<Server> | ||
// InvokerOptions allow the user to configure the server. | ||
export type InvokerOptions = { | ||
@@ -20,7 +24,3 @@ 'logLevel'?: LogLevel, | ||
export interface Invokable { | ||
(context: Context, cloudevent?: CloudEvent<any>): any | ||
} | ||
// re-export | ||
export { Context, Logger, CloudEventResponse } from './lib/context'; | ||
export * from './lib/types'; |
14
index.js
@@ -46,3 +46,3 @@ const qs = require('qs'); | ||
}); | ||
server.addContentTypeParser('*', { parseAs: 'buffer' }, function(req, body, done) { | ||
@@ -52,3 +52,3 @@ try { | ||
} catch (err) { | ||
err.statusCode = 400; | ||
err.statusCode = 500; | ||
done(err, undefined); | ||
@@ -62,3 +62,3 @@ } | ||
server.addHook('preHandler', (req, reply, done) => { | ||
req.fcontext = new Context(req, reply); | ||
req.fcontext = new Context(req); | ||
done(); | ||
@@ -76,3 +76,7 @@ }); | ||
return new Promise((resolve, reject) => { | ||
server.listen(port, '0.0.0.0', err => { | ||
server.listen({ | ||
port, | ||
host: '::' | ||
}, | ||
err => { // callback function | ||
if (err) return reject(err); | ||
@@ -87,3 +91,3 @@ resolve(server.server); | ||
if (!fileOrDirPath) fileOrDirPath = './'; | ||
let baseDir; | ||
@@ -90,0 +94,0 @@ let maybeDir = fs.statSync(fileOrDirPath); |
@@ -5,4 +5,4 @@ const { CloudEvent } = require('cloudevents'); | ||
constructor(request) { | ||
this.body = request.body; | ||
this.query = { ...request.query }; | ||
this.body = { ...request.body }; | ||
this.headers = { ...request.headers }; | ||
@@ -13,2 +13,3 @@ this.method = request.raw.method; | ||
this.httpVersionMinor = request.raw.httpVersionMinor; | ||
this.contentType = request.headers['content-type']; | ||
@@ -22,3 +23,2 @@ Object.assign(this, request.query); | ||
} | ||
} | ||
@@ -25,0 +25,0 @@ |
@@ -22,4 +22,4 @@ const invoker = require('./invoker'); | ||
function sendReply(reply, payload) { | ||
const contentType = payload.headers['content-type']; | ||
if (contentType.startsWith('text/plain') && (typeof payload.response !== 'string')) { | ||
const contentType = payload.headers?.['content-type']; | ||
if (contentType?.startsWith('text/plain') && (typeof payload.response !== 'string')) { | ||
payload.response = JSON.stringify(payload.response); | ||
@@ -26,0 +26,0 @@ } |
@@ -1,2 +0,4 @@ | ||
'use strict'; | ||
/** | ||
* The invoker module is responsible for invoking the user function. | ||
*/ | ||
const { CloudEvent, HTTP } = require('cloudevents'); | ||
@@ -18,58 +20,89 @@ | ||
let fnReturn; | ||
const scope = Object.freeze({}); | ||
try { | ||
if (context.cloudevent) { | ||
// If there is a cloud event, provide the data | ||
// as the first parameter | ||
payload.response = await func.bind(scope)(context, context.cloudevent); | ||
// Invoke the function with the context and the CloudEvent | ||
fnReturn = await func.bind(scope)(context, context.cloudevent); | ||
// If the response is a CloudEvent, we need to convert it | ||
// to a Message first and respond with the headers/body | ||
if (fnReturn instanceof CloudEvent) { | ||
try { | ||
const message = HTTP.binary(fnReturn); | ||
payload.headers = {...payload.headers, ...message.headers}; | ||
payload.response = message.body; | ||
// In this case, where the function is invoked with a CloudEvent | ||
// and returns a CloudEvent we don't need to continue processing the | ||
// response. Just return it using the HTTP.binary format. | ||
return payload; | ||
} catch (err) { | ||
return handleError(err, log); | ||
} | ||
} | ||
} else { | ||
// Invoke with context | ||
// TODO: Should this actually just get the Node.js request object? | ||
payload.response = await func.bind(scope)(context); | ||
// It's an HTTP function - extract the request body | ||
let body = context.body; | ||
if (context.contentType === 'application/json' && typeof body === 'string') { | ||
try { | ||
body = JSON.parse(body); | ||
} catch (err) { | ||
console.error('Error parsing JSON body', err); | ||
} | ||
} | ||
// Invoke with context and the raw body | ||
fnReturn = await func.bind(scope)(context, body); | ||
} | ||
} catch (err) { | ||
payload.response = handleError(err, log); | ||
return handleError(err, log); | ||
} | ||
// Raw HTTP functions, and CloudEvent functions that return something | ||
// other than a CloudEvent, will end up here. | ||
// Return 204 No Content if the function returns | ||
// null, undefined or empty string | ||
if (!payload.response) { | ||
if (!fnReturn) { | ||
payload.headers['content-type'] = 'text/plain'; | ||
payload.code = 204; | ||
payload.response = ''; | ||
return payload; | ||
} | ||
// Check for user defined headers | ||
if (typeof payload.response.headers === 'object') { | ||
// If the function returns a string, set the content type to text/plain | ||
// and return it as the response | ||
if (typeof fnReturn === 'string') { | ||
payload.headers['content-type'] = 'text/plain; charset=utf8'; | ||
payload.response = fnReturn; | ||
return payload; | ||
} | ||
// The function returned an object or an array, check for | ||
// user defined headers or datacontenttype | ||
if (typeof fnReturn?.headers === 'object') { | ||
const headers = {}; | ||
// normalize the headers as lowercase | ||
for (const header in payload.response.headers) { | ||
headers[header.toLocaleLowerCase()] = payload.response.headers[header]; | ||
for (const header in fnReturn.headers) { | ||
headers[header.toLocaleLowerCase()] = fnReturn.headers[header]; | ||
} | ||
payload.headers = { ...payload.headers, ...headers }; | ||
delete payload.response.headers; | ||
} | ||
// If the response is a CloudEvent, we need to convert it | ||
// to a Message first and respond with the headers/body | ||
if (payload.response instanceof CloudEvent) { | ||
try { | ||
const message = HTTP.binary(payload.response); | ||
payload.headers = {...payload.headers, ...message.headers}; | ||
payload.response = message.body; | ||
} catch (err) { | ||
payload.response = handleError(err, log); | ||
return payload; | ||
} | ||
} | ||
// Check for user defined status code | ||
if (payload.response.statusCode) { | ||
payload.code = payload.response.statusCode; | ||
delete payload.response.statusCode; | ||
if (fnReturn.statusCode) { | ||
payload.code = fnReturn.statusCode; | ||
} | ||
// Check for user supplied body | ||
if (payload.response.body !== undefined) { | ||
payload.response = payload.response.body; | ||
delete payload.response.body; | ||
if (fnReturn.body !== undefined) { | ||
if (typeof fnReturn.body === 'string') { | ||
payload.headers['content-type'] = 'text/plain; charset=utf8'; | ||
} else if (typeof fnReturn.body === 'object') { | ||
payload.headers['content-type'] = 'application/json; charset=utf8'; | ||
} | ||
payload.response = fnReturn.body; | ||
} else if (typeof fnReturn === 'object' && !fnReturn?.body) { | ||
// Finally, the user may have supplied a simple object response | ||
payload.headers['content-type'] = 'application/json; charset=utf8'; | ||
payload.response = fnReturn; | ||
} | ||
@@ -81,7 +114,7 @@ return payload; | ||
function handleError(err, log) { | ||
log.error(err); | ||
log.error('Error processing user function', err); | ||
return { | ||
statusCode: err.code ? err.code : 500, | ||
statusMessage: err.message | ||
code: err.code ? err.code : 500, | ||
response: err.message | ||
}; | ||
} |
{ | ||
"name": "faas-js-runtime", | ||
"version": "0.9.1", | ||
"version": "0.9.3", | ||
"repository": { | ||
@@ -10,6 +10,9 @@ "type": "git", | ||
"license": "Apache-2.0", | ||
"engines": { | ||
"node": "^18 || ^16 || ^14" | ||
}, | ||
"scripts": { | ||
"lint": "eslint --ignore-path .gitignore .", | ||
"test": "npm run test:source && npm run test:types", | ||
"test:source": "nyc tape test/test*.js | colortape", | ||
"test:source": "nyc --reporter=lcovonly tape test/test*.js | colortape", | ||
"test:types": "tsd", | ||
@@ -25,15 +28,17 @@ "pretest": "npm run lint" | ||
], | ||
"bugs": { | ||
"url": "https://github.com/boson-project/faas-js-runtime/issues" | ||
}, | ||
"types": "index.d.ts", | ||
"bin": "./bin/cli.js", | ||
"dependencies": { | ||
"chalk": "^4.1.2", | ||
"cloudevents": "^5.3.1", | ||
"commander": "^8.1.0", | ||
"commander": "^9.4.0", | ||
"death": "^1.1.0", | ||
"fastify": "^3.20.1", | ||
"fastify": "^4.9.2", | ||
"js-yaml": "^4.1.0", | ||
"node-os-utils": "^1.3.5", | ||
"overload-protection": "^1.2.0", | ||
"prom-client": "^14.0.0", | ||
"qs": "^6.10.1" | ||
"overload-protection": "^1.2.3", | ||
"prom-client": "^14.1.0", | ||
"qs": "^6.11.0" | ||
}, | ||
@@ -49,3 +54,3 @@ "devDependencies": { | ||
"nyc": "^15.1.0", | ||
"supertest": "^6.1.4", | ||
"supertest": "^6.3.1", | ||
"tape": "^5.3.1", | ||
@@ -55,7 +60,2 @@ "tsd": "^0.17.0", | ||
}, | ||
"standardx": { | ||
"ignore": [ | ||
"lib/context.js" | ||
] | ||
}, | ||
"tsd": { | ||
@@ -62,0 +62,0 @@ "directory": "test/types", |
@@ -1,24 +0,76 @@ | ||
## FaaS Node.js Runtime Framework | ||
# Node.js Function Framework | ||
[![Node.js CI](https://github.com/boson-project/faas-js-runtime/workflows/Node.js%20CI/badge.svg)](https://github.com/boson-project/faas-js-runtime/actions?query=workflow%3A%22Node.js+CI%22+branch%3Amaster) | ||
[![codecov](https://codecov.io/gh/boson-project/faas-js-runtime/branch/main/graph/badge.svg?token=Z72LKANFJI)](https://codecov.io/gh/boson-project/faas-js-runtime) | ||
This module provides a Node.js framework for executing a function that | ||
exists in a user provided directory path as an `index.js` file. The | ||
exists in a user-provided directory path as an `index.js` file. The | ||
directory may also contain an optional `package.json` file which can | ||
be used to declare runtime dependencies for the function. | ||
be used to declare runtime dependencies for the function. You can also | ||
provide a path to an arbitrary JavaScript file instead of a directory | ||
path, allowing you to execute a single file as a function. | ||
The function is loaded, and then invoked for incoming HTTP requests | ||
The function is loaded and then invoked for incoming HTTP requests | ||
at `localhost:8080`. The incoming request may be a | ||
[Cloud Event](https://github.com/cloudevents/sdk-javascript#readme.) or | ||
just a simple HTTP GET request. In either case, the function will receive | ||
a `Context` object instance that has a `cloudevent` property. For a raw HTTP | ||
request, the incoming request is converted to a Cloud Event. | ||
just a simple HTTP GET/POST request. The invoked user function can be | ||
`async` but that is not required. | ||
The invoked user function can be `async` but that is not required. | ||
## Function Signatures | ||
### CLI | ||
This module supports two different function signatures: HTTP or CloudEvents. In the type definitions below, we use TypeScript to express interfaces and types, but this module is usable from JavaScript as well. | ||
### HTTP Functions | ||
The HTTP function signature is the simplest. It is invoked for every HTTP request that does not contain a CloudEvent. | ||
```typescript | ||
interface HTTPFunction { | ||
(context: Context, body?: IncomingBody): HTTPFunctionReturn; | ||
} | ||
``` | ||
Where the `IncomingBody` is either a string, a Buffer, a JavaScript object, or undefined, depending on what was supplied in the HTTP POST message body. The `HTTTPFunctionReturn` type is defined as: | ||
```typescript | ||
type HTTPFunctionReturn = Promise<StructuredReturn> | StructuredReturn | ResponseBody | void; | ||
``` | ||
Where the `StructuredReturn` is a JavaScript object with the following properties: | ||
```typescript | ||
interface StructuredReturn { | ||
statusCode?: number; | ||
headers?: Record<string, string>; | ||
body?: ResponseBody; | ||
} | ||
``` | ||
If the function returns a `StructuredReturn` object, then the `statusCode` and `headers` properties are used to construct the HTTP response. If the `body` property is present, it is used as the response body. If the function returns `void` or `undefined`, then the response body is empty. | ||
The `ResponseBody` is either a string, a JavaScript object, or a Buffer. JavaScript objects will be serialized as JSON. Buffers will be sent as binary data. | ||
### CloudEvent Functions | ||
CloudEvent functions are used in environments where the incoming HTTP request is a CloudEvent. The function signature is: | ||
```typescript | ||
interface CloudEventFunction { | ||
(context: Context, event: CloudEvent): CloudEventFunctionReturn; | ||
} | ||
``` | ||
Where the return type is defined as: | ||
```typescript | ||
type CloudEventFunctionReturn = Promise<CloudEvent> | CloudEvent | HTTPFunctionReturn; | ||
``` | ||
The function return type can be anything that a simple HTTP function can return or a CloudEvent. Whatever is returned, it will be sent back to the caller as a response. | ||
## CLI | ||
The easiest way to get started is to use the CLI. You can call it | ||
with the path to any JavaScript file which has a default export that | ||
is a function. For example, | ||
is a function. For example, | ||
@@ -39,2 +91,5 @@ ```js | ||
Additionally, if your JavaScript file exports more than a single function, | ||
an exported `handle` function will be invoked. | ||
You can expose this function as an HTTP endpoint at `localhost:8080` | ||
@@ -47,5 +102,5 @@ with the CLI. | ||
### Usage | ||
## Usage as a Module | ||
In my current working directory, I have an `index.js` file like this. | ||
In the current working directory, there is an `index.js` file like this. | ||
@@ -61,6 +116,7 @@ ```js | ||
// My function directory is in ./function-dir | ||
// The function directory is in ./function-dir | ||
start(require(`${__dirname}/function-dir/`), server => { | ||
// The server is now listening on localhost:8080 | ||
// and the function will be invoked for each HTTP | ||
// and the function defined in `function-dir/index.js` | ||
// will be invoked for each HTTP | ||
// request to this endpoint. | ||
@@ -79,3 +135,3 @@ console.log('Server listening'); | ||
module.exports = async function myFunction(context) { | ||
const ret = 'This is a test function for Node.js FaaS. Success.'; | ||
const ret = 'This is a test for Node.js functions. Success.'; | ||
return new Promise((resolve, reject) => { | ||
@@ -110,3 +166,3 @@ setTimeout(_ => { | ||
-H'Ce-type: dev.knative.example' \ | ||
-H'Ce-specversion: 0.2' \ | ||
-H'Ce-specversion: 1.0' \ | ||
http://localhost:8080 | ||
@@ -117,7 +173,2 @@ ``` | ||
You can see this in action, executing the function at `test/fixtures/async` | ||
by running `node hack/run.js`. | ||
### Tests | ||
Just run `npm test`. | ||
You can see this in action by running `node hack/run.js`. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
9
634
0
169
40383
14
+ Added@fastify/ajv-compiler@3.6.0(transitive)
+ Added@fastify/error@3.4.1(transitive)
+ Added@fastify/fast-json-stringify-compiler@4.3.0(transitive)
+ Added@fastify/merge-json-schemas@0.1.1(transitive)
+ Addedabort-controller@3.0.0(transitive)
+ Addedajv-formats@2.1.13.0.1(transitive)
+ Addedavvio@8.4.0(transitive)
+ Addedbase64-js@1.5.1(transitive)
+ Addedbuffer@6.0.3(transitive)
+ Addedcommander@9.5.0(transitive)
+ Addedcookie@0.6.0(transitive)
+ Addedevent-target-shim@5.0.1(transitive)
+ Addedevents@3.3.0(transitive)
+ Addedfast-json-stringify@5.16.1(transitive)
+ Addedfast-querystring@1.1.2(transitive)
+ Addedfastify@4.28.1(transitive)
+ Addedfind-my-way@8.2.2(transitive)
+ Addedieee754@1.2.1(transitive)
+ Addedjson-schema-ref-resolver@1.0.1(transitive)
+ Addedlight-my-request@5.13.0(transitive)
+ Addedon-exit-leak-free@2.1.2(transitive)
+ Addedpino@9.4.0(transitive)
+ Addedpino-abstract-transport@1.2.0(transitive)
+ Addedpino-std-serializers@7.0.0(transitive)
+ Addedprocess@0.11.10(transitive)
+ Addedprocess-warning@3.0.04.0.0(transitive)
+ Addedreadable-stream@4.5.2(transitive)
+ Addedreal-require@0.2.0(transitive)
+ Addedret@0.4.3(transitive)
+ Addedsafe-buffer@5.2.1(transitive)
+ Addedsafe-regex2@3.1.0(transitive)
+ Addedsafe-stable-stringify@2.5.0(transitive)
+ Addedsonic-boom@4.1.0(transitive)
+ Addedsplit2@4.2.0(transitive)
+ Addedstring_decoder@1.3.0(transitive)
+ Addedthread-stream@3.1.0(transitive)
+ Addedtoad-cache@3.7.0(transitive)
- Removedchalk@^4.1.2
- Removed@fastify/ajv-compiler@1.1.0(transitive)
- Removed@fastify/error@2.0.0(transitive)
- Removedansi-styles@4.3.0(transitive)
- Removedarchy@1.0.0(transitive)
- Removedavvio@7.2.5(transitive)
- Removedchalk@4.1.2(transitive)
- Removedcolor-convert@2.0.1(transitive)
- Removedcolor-name@1.1.4(transitive)
- Removedcommander@8.3.0(transitive)
- Removedcookie@0.5.0(transitive)
- Removeddebug@4.3.7(transitive)
- Removeddeepmerge@4.3.1(transitive)
- Removedfast-json-stringify@2.7.13(transitive)
- Removedfast-safe-stringify@2.1.1(transitive)
- Removedfastify@3.29.5(transitive)
- Removedfind-my-way@4.5.1(transitive)
- Removedflatstr@1.0.12(transitive)
- Removedhas-flag@4.0.0(transitive)
- Removedlight-my-request@4.12.0(transitive)
- Removedms@2.1.3(transitive)
- Removedpino@6.14.0(transitive)
- Removedpino-std-serializers@3.2.0(transitive)
- Removedprocess-warning@1.0.0(transitive)
- Removedqueue-microtask@1.2.3(transitive)
- Removedret@0.2.2(transitive)
- Removedsafe-regex2@2.0.0(transitive)
- Removedsemver-store@0.3.0(transitive)
- Removedsonic-boom@1.4.1(transitive)
- Removedstring-similarity@4.0.4(transitive)
- Removedsupports-color@7.2.0(transitive)
- Removedtiny-lru@8.0.2(transitive)
Updatedcommander@^9.4.0
Updatedfastify@^4.9.2
Updatedoverload-protection@^1.2.3
Updatedprom-client@^14.1.0
Updatedqs@^6.11.0