Files
outline/server/utils/PluginManager.ts
Tom Moor f9a11a28d8 chore: Plugin registration (#6623)
* first pass

* test

* test

* priority

* Reduce boilerplate further

* Update server/utils/PluginManager.ts

Co-authored-by: Apoorv Mishra <apoorvmishra101092@gmail.com>

* fix: matchesNode error in destroyed editor transaction

* fix: Individual imported files do not display source correctly in 'Insights'

* chore: Add sleep before Slack notification

* docs

* fix: Error logged about missing plugin.json

* Remove email template glob

---------

Co-authored-by: Apoorv Mishra <apoorvmishra101092@gmail.com>
2024-03-08 20:32:05 -08:00

174 lines
4.6 KiB
TypeScript

import path from "path";
import { glob } from "glob";
import type Router from "koa-router";
import sortBy from "lodash/sortBy";
import { v4 as uuid } from "uuid";
import { UnfurlSignature } from "@shared/types";
import type BaseEmail from "@server/emails/templates/BaseEmail";
import env from "@server/env";
import Logger from "@server/logging/Logger";
import type BaseProcessor from "@server/queues/processors/BaseProcessor";
import type BaseTask from "@server/queues/tasks/BaseTask";
export enum PluginPriority {
VeryHigh = 0,
High = 100,
Normal = 200,
Low = 300,
VeryLow = 500,
}
/**
* The different types of server plugins that can be registered.
*/
export enum PluginType {
API = "api",
AuthProvider = "authProvider",
EmailTemplate = "emailTemplate",
Processor = "processor",
Task = "task",
UnfurlProvider = "unfurl",
}
/**
* A map of plugin types to their values, for example an API plugin would have a value of type
* Router. Registering an API plugin causes the router to be mounted.
*/
type PluginValueMap = {
[PluginType.API]: Router;
[PluginType.AuthProvider]: Router;
[PluginType.EmailTemplate]: typeof BaseEmail;
[PluginType.Processor]: typeof BaseProcessor;
[PluginType.Task]: typeof BaseTask<any>;
[PluginType.UnfurlProvider]: UnfurlSignature;
};
export type Plugin<T extends PluginType> = {
/** A unique ID for the plugin */
id: string;
/** The plugin's display name */
name?: string;
/** A brief description of the plugin */
description?: string;
/** The plugin content */
value: PluginValueMap[T];
/** An optional priority, will affect order in menus and execution. Lower is earlier. */
priority?: number;
/** Whether the plugin is enabled (default: true) */
enabled?: boolean;
};
export class PluginManager {
private static plugins = new Map<PluginType, Plugin<PluginType>[]>();
/**
* Register a plugin of a given type.
*
* @param type The plugin type
* @param value The plugin value
* @param options Additional options, including whether the plugin is enabled and it's priority.
* @returns The PluginManager instance, for chaining.
*/
public static register<T extends PluginType>(
type: T,
value: PluginValueMap[T],
options: Omit<Plugin<T>, "value"> = {
id: uuid(),
}
) {
if (!this.plugins.has(type)) {
this.plugins.set(type, []);
}
const plugin = {
value,
priority: PluginPriority.Normal,
...options,
};
Logger.debug(
"plugins",
`Plugin ${options.enabled === false ? "disabled" : "enabled"} "${
options.id
}" ${options.description ? `(${options.description})` : ""}`
);
this.plugins.get(type)!.push(plugin);
// allow chaining
return this;
}
/**
* Syntactic sugar for registering a background Task.
*
* @param value The task class
* @param options Additional options
*/
public static registerTask(
value: PluginValueMap[PluginType.Task],
options?: Omit<Plugin<PluginType.Task>, "id" | "value">
) {
return this.register(PluginType.Task, value, {
id: value.name,
...options,
});
}
/**
* Syntactic sugar for registering a background Processor.
*
* @param value The processor class
* @param options Additional options
*/
public static registerProcessor(
value: PluginValueMap[PluginType.Processor],
options?: Omit<Plugin<PluginType.Processor>, "id" | "value">
) {
return this.register(PluginType.Processor, value, {
id: value.name,
...options,
});
}
/**
* Returns all the plugins of a given type in order of priority.
*
* @param type The type of plugin to filter by
* @returns A list of plugins
*/
public static getPlugins<T extends PluginType>(type: T) {
this.loadPlugins();
return sortBy(this.plugins.get(type) || [], "priority") as Plugin<T>[];
}
/**
* Returns all the enabled plugins of a given type in order of priority.
*
* @param type The type of plugin to filter by
* @returns A list of plugins
*/
public static getEnabledPlugins<T extends PluginType>(type: T) {
return this.getPlugins(type).filter((plugin) => plugin.enabled !== false);
}
/**
* Load plugin server components (anything in the `/server/` directory of a plugin will be loaded)
*/
public static loadPlugins() {
if (this.loaded) {
return;
}
const rootDir = env.ENVIRONMENT === "test" ? "" : "build";
glob
.sync(path.join(rootDir, "plugins/*/server/!(*.test|schema).[jt]s"))
.forEach((filePath: string) => {
require(path.join(process.cwd(), filePath));
});
this.loaded = true;
}
private static loaded = false;
}