Massive cleanup and plugbox cleanup
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { createSandbox } from "../environment/node_sandbox";
|
||||
import { expect, test } from "@jest/globals";
|
||||
import { Manifest } from "../types";
|
||||
import express from "express";
|
||||
import request from "supertest";
|
||||
import { EndpointFeature, EndpointHook } from "./endpoint";
|
||||
import { System } from "../system";
|
||||
|
||||
test("Run a plugbox endpoint server", async () => {
|
||||
let system = new System<EndpointHook>("server");
|
||||
let plug = await system.load(
|
||||
"test",
|
||||
{
|
||||
functions: {
|
||||
testhandler: {
|
||||
code: `(() => {
|
||||
return {
|
||||
default: (req) => {
|
||||
console.log("Req", req);
|
||||
return {status: 200, body: [1, 2, 3], headers: {"Content-type": "application/json"}};
|
||||
}
|
||||
};
|
||||
})()`,
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
endpoints: [{ method: "GET", path: "/", handler: "testhandler" }],
|
||||
},
|
||||
} as Manifest<EndpointHook>,
|
||||
createSandbox(system)
|
||||
);
|
||||
|
||||
const app = express();
|
||||
const port = 3123;
|
||||
|
||||
system.addFeature(new EndpointFeature(app));
|
||||
|
||||
let server = app.listen(port, () => {
|
||||
console.log(`Listening on port ${port}`);
|
||||
});
|
||||
let resp = await request(app)
|
||||
.get("/_/test/?name=Pete")
|
||||
.expect((resp) => {
|
||||
expect(resp.status).toBe(200);
|
||||
expect(resp.header["content-type"]).toContain("application/json");
|
||||
expect(resp.text).toBe(JSON.stringify([1, 2, 3]));
|
||||
});
|
||||
server.close();
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import { Feature, Manifest } from "../types";
|
||||
import { Express, NextFunction, Request, Response } from "express";
|
||||
import { System } from "../system";
|
||||
|
||||
export type EndpointRequest = {
|
||||
method: string;
|
||||
path: string;
|
||||
query: { [key: string]: string };
|
||||
headers: { [key: string]: string };
|
||||
body: any;
|
||||
};
|
||||
|
||||
export type EndpointResponse = {
|
||||
status: number;
|
||||
headers?: { [key: string]: string };
|
||||
body: any;
|
||||
};
|
||||
|
||||
export type EndpointHook = {
|
||||
endpoints?: EndPointDef[];
|
||||
};
|
||||
|
||||
export type EndPointDef = {
|
||||
method: "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS";
|
||||
path: string;
|
||||
handler: string; // function name
|
||||
};
|
||||
|
||||
const endPointPrefix = "/_";
|
||||
|
||||
export class EndpointFeature implements Feature<EndpointHook> {
|
||||
private app: Express;
|
||||
|
||||
constructor(app: Express) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
apply(system: System<EndpointHook>): void {
|
||||
this.app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.path.startsWith(endPointPrefix)) {
|
||||
return next();
|
||||
}
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
for (const [plugName, plug] of system.loadedPlugs.entries()) {
|
||||
const manifest = plug.manifest;
|
||||
if (!manifest) {
|
||||
continue;
|
||||
}
|
||||
const endpoints = manifest.hooks?.endpoints;
|
||||
if (endpoints) {
|
||||
let prefix = `${endPointPrefix}/${plugName}`;
|
||||
if (!req.path.startsWith(prefix)) {
|
||||
continue;
|
||||
}
|
||||
for (const { path, method, handler } of endpoints) {
|
||||
let prefixedPath = `${prefix}${path}`;
|
||||
if (prefixedPath === req.path && method === req.method) {
|
||||
try {
|
||||
const response: EndpointResponse = await plug.invoke(
|
||||
handler,
|
||||
[
|
||||
{
|
||||
path: req.path,
|
||||
method: req.method,
|
||||
body: req.body,
|
||||
query: req.query,
|
||||
headers: req.headers,
|
||||
} as EndpointRequest,
|
||||
]
|
||||
);
|
||||
let resp = res.status(response.status);
|
||||
if (response.headers) {
|
||||
for (const [key, value] of Object.entries(
|
||||
response.headers
|
||||
)) {
|
||||
resp = resp.header(key, value);
|
||||
}
|
||||
}
|
||||
resp.send(response.body);
|
||||
return;
|
||||
} catch (e: any) {
|
||||
console.error("Error executing function", e);
|
||||
res.status(500).send(e.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
next();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
next(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
validateManifest(manifest: Manifest<EndpointHook>): string[] {
|
||||
const endpoints = manifest.hooks.endpoints;
|
||||
let errors = [];
|
||||
if (endpoints) {
|
||||
for (let { method, path, handler } of endpoints) {
|
||||
if (!path) {
|
||||
errors.push("Path not defined for endpoint");
|
||||
}
|
||||
if (["GET", "POST", "PUT", "DELETE"].indexOf(method) === -1) {
|
||||
errors.push(
|
||||
`Invalid method ${method} for end point with with ${path}`
|
||||
);
|
||||
}
|
||||
if (!manifest.functions[handler]) {
|
||||
errors.push(`Endpoint handler function ${handler} not found`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Feature, Manifest } from "../types";
|
||||
import cron, { ScheduledTask } from "node-cron";
|
||||
import { safeRun } from "../util";
|
||||
import { System } from "../system";
|
||||
|
||||
export type CronHook = {
|
||||
crons?: CronDef[];
|
||||
};
|
||||
|
||||
export type CronDef = {
|
||||
cron: string;
|
||||
handler: string; // function name
|
||||
};
|
||||
|
||||
export class NodeCronFeature implements Feature<CronHook> {
|
||||
apply(system: System<CronHook>): void {
|
||||
let tasks: ScheduledTask[] = [];
|
||||
system.on({
|
||||
plugLoaded: (name, plug) => {
|
||||
reloadCrons();
|
||||
},
|
||||
plugUnloaded(name, plug) {
|
||||
reloadCrons();
|
||||
},
|
||||
});
|
||||
|
||||
reloadCrons();
|
||||
|
||||
function reloadCrons() {
|
||||
// ts-ignore
|
||||
tasks.forEach((task) => task.stop());
|
||||
tasks = [];
|
||||
for (let plug of system.loadedPlugs.values()) {
|
||||
const crons = plug.manifest?.hooks?.crons;
|
||||
if (crons) {
|
||||
for (let cronDef of crons) {
|
||||
tasks.push(
|
||||
cron.schedule(cronDef.cron, () => {
|
||||
console.log("Now acting on cron", cronDef.cron);
|
||||
safeRun(async () => {
|
||||
try {
|
||||
await plug.invoke(cronDef.handler, []);
|
||||
} catch (e: any) {
|
||||
console.error("Execution of cron function failed", e);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateManifest(manifest: Manifest<CronHook>): string[] {
|
||||
const crons = manifest.hooks.crons;
|
||||
let errors = [];
|
||||
if (crons) {
|
||||
for (let cronDef of crons) {
|
||||
if (!cron.validate(cronDef.cron)) {
|
||||
errors.push(`Invalid cron expression ${cronDef.cron}`);
|
||||
}
|
||||
if (!manifest.functions[cronDef.handler]) {
|
||||
errors.push(`Cron handler function ${cronDef.handler} not found`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user