Tons of refactoring, moving commands and slash commands into hooks
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { createSandbox } from "../environments/node_sandbox";
|
||||
import { expect, test } from "@jest/globals";
|
||||
import { Manifest } from "../types";
|
||||
import express from "express";
|
||||
import request from "supertest";
|
||||
import { EndpointHook, EndpointHookT } from "./endpoint";
|
||||
import { System } from "../system";
|
||||
|
||||
test("Run a plugos endpoint server", async () => {
|
||||
let system = new System<EndpointHookT>("server");
|
||||
let plug = await system.load(
|
||||
"test",
|
||||
{
|
||||
functions: {
|
||||
testhandler: {
|
||||
http: {
|
||||
path: "/",
|
||||
},
|
||||
code: `(() => {
|
||||
return {
|
||||
default: (req) => {
|
||||
console.log("Req", req);
|
||||
return {status: 200, body: [1, 2, 3], headers: {"Content-type": "application/json"}};
|
||||
}
|
||||
};
|
||||
})()`,
|
||||
},
|
||||
},
|
||||
} as Manifest<EndpointHookT>,
|
||||
createSandbox
|
||||
);
|
||||
|
||||
const app = express();
|
||||
const port = 3123;
|
||||
|
||||
system.addHook(new EndpointHook(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,134 @@
|
||||
import { Hook, 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 EndpointHookT = {
|
||||
http?: EndPointDef | EndPointDef[];
|
||||
};
|
||||
|
||||
export type EndPointDef = {
|
||||
method?: "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "ANY";
|
||||
path: string;
|
||||
};
|
||||
|
||||
export class EndpointHook implements Hook<EndpointHookT> {
|
||||
private app: Express;
|
||||
private prefix: string;
|
||||
|
||||
constructor(app: Express, prefix: string) {
|
||||
this.app = app;
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
apply(system: System<EndpointHookT>): void {
|
||||
this.app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.path.startsWith(this.prefix)) {
|
||||
return next();
|
||||
}
|
||||
console.log("Endpoint request", req.path);
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
// Iterate over all loaded plugins
|
||||
for (const [plugName, plug] of system.loadedPlugs.entries()) {
|
||||
const manifest = plug.manifest;
|
||||
if (!manifest) {
|
||||
continue;
|
||||
}
|
||||
const functions = manifest.functions;
|
||||
console.log("Checking plug", plugName);
|
||||
let prefix = `${this.prefix}/${plugName}`;
|
||||
if (!req.path.startsWith(prefix)) {
|
||||
continue;
|
||||
}
|
||||
for (const [name, functionDef] of Object.entries(functions)) {
|
||||
if (!functionDef.http) {
|
||||
continue;
|
||||
}
|
||||
let endpoints = Array.isArray(functionDef.http)
|
||||
? functionDef.http
|
||||
: [functionDef.http];
|
||||
console.log(endpoints);
|
||||
for (const { path, method } of endpoints) {
|
||||
let prefixedPath = `${prefix}${path}`;
|
||||
if (
|
||||
prefixedPath === req.path &&
|
||||
((method || "GET") === req.method || method === "ANY")
|
||||
) {
|
||||
try {
|
||||
const response: EndpointResponse = await plug.invoke(name, [
|
||||
{
|
||||
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<EndpointHookT>): string[] {
|
||||
let errors = [];
|
||||
for (const [name, functionDef] of Object.entries(manifest.functions)) {
|
||||
if (!functionDef.http) {
|
||||
continue;
|
||||
}
|
||||
let endpoints = Array.isArray(functionDef.http)
|
||||
? functionDef.http
|
||||
: [functionDef.http];
|
||||
for (let { path, method } of endpoints) {
|
||||
if (!path) {
|
||||
errors.push("Path not defined for endpoint");
|
||||
}
|
||||
if (
|
||||
method &&
|
||||
["GET", "POST", "PUT", "DELETE", "ANY"].indexOf(method) === -1
|
||||
) {
|
||||
errors.push(
|
||||
`Invalid method ${method} for end point with with ${path}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Hook, Manifest } from "../types";
|
||||
import { System } from "../system";
|
||||
|
||||
// System events:
|
||||
// - plug:load (plugName: string)
|
||||
|
||||
export type EventHookT = {
|
||||
events?: string[];
|
||||
};
|
||||
|
||||
export class EventHook implements Hook<EventHookT> {
|
||||
private system?: System<EventHookT>;
|
||||
|
||||
async dispatchEvent(eventName: string, data?: any): Promise<any[]> {
|
||||
if (!this.system) {
|
||||
throw new Error("Event hook is not initialized");
|
||||
}
|
||||
let promises: Promise<any>[] = [];
|
||||
for (const plug of this.system.loadedPlugs.values()) {
|
||||
for (const [name, functionDef] of Object.entries(
|
||||
plug.manifest!.functions
|
||||
)) {
|
||||
if (functionDef.events && functionDef.events.includes(eventName)) {
|
||||
// Only dispatch functions that can run in this environment
|
||||
if (plug.canInvoke(name)) {
|
||||
promises.push(plug.invoke(name, [data]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
apply(system: System<EventHookT>): void {
|
||||
this.system = system;
|
||||
this.system.on({
|
||||
plugLoaded: (name) => {
|
||||
this.dispatchEvent("plug:load", name);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
validateManifest(manifest: Manifest<EventHookT>): string[] {
|
||||
let errors = [];
|
||||
for (const [name, functionDef] of Object.entries(manifest.functions)) {
|
||||
if (functionDef.events && !Array.isArray(functionDef.events)) {
|
||||
errors.push("'events' key must be an array of strings");
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Hook, Manifest } from "../types";
|
||||
import cron, { ScheduledTask } from "node-cron";
|
||||
import { safeRun } from "../util";
|
||||
import { System } from "../system";
|
||||
|
||||
export type CronHookT = {
|
||||
cron?: string | string[];
|
||||
};
|
||||
|
||||
export class NodeCronHook implements Hook<CronHookT> {
|
||||
apply(system: System<CronHookT>): void {
|
||||
let tasks: ScheduledTask[] = [];
|
||||
system.on({
|
||||
plugLoaded: (name, plug) => {
|
||||
reloadCrons();
|
||||
},
|
||||
plugUnloaded(name, plug) {
|
||||
reloadCrons();
|
||||
},
|
||||
});
|
||||
|
||||
reloadCrons();
|
||||
|
||||
function reloadCrons() {
|
||||
tasks.forEach((task) => task.stop());
|
||||
tasks = [];
|
||||
for (let plug of system.loadedPlugs.values()) {
|
||||
if (!plug.manifest) {
|
||||
continue;
|
||||
}
|
||||
for (const [name, functionDef] of Object.entries(
|
||||
plug.manifest.functions
|
||||
)) {
|
||||
if (!functionDef.cron) {
|
||||
continue;
|
||||
}
|
||||
const crons = Array.isArray(functionDef.cron)
|
||||
? functionDef.cron
|
||||
: [functionDef.cron];
|
||||
for (let cronDef of crons) {
|
||||
tasks.push(
|
||||
cron.schedule(cronDef, () => {
|
||||
console.log("Now acting on cron", cronDef);
|
||||
safeRun(async () => {
|
||||
try {
|
||||
await plug.invoke(name, [cronDef]);
|
||||
} catch (e: any) {
|
||||
console.error("Execution of cron function failed", e);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateManifest(manifest: Manifest<CronHookT>): string[] {
|
||||
let errors = [];
|
||||
for (const [name, functionDef] of Object.entries(manifest.functions)) {
|
||||
if (!functionDef.cron) {
|
||||
continue;
|
||||
}
|
||||
const crons = Array.isArray(functionDef.cron)
|
||||
? functionDef.cron
|
||||
: [functionDef.cron];
|
||||
for (let cronDef of crons) {
|
||||
if (!cron.validate(cronDef)) {
|
||||
errors.push(`Invalid cron expression ${cronDef}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user