mirror of
https://github.com/directus/directus.git
synced 2026-01-22 22:58:00 -05:00
* Typecheck across packages that are built with esbuild * Boilerplate new Errors package * No need, tsup checks with --dts * Switch to tsup * Setup dev script * Add readme * More boilerplaty things * Finish createError function * Install @directus/random * Downgrade node types * Add utility function to check if an error is a DirectusError * Use new is-error check * Install errors package * Add failed validation common error * Export common errors * Move joi convertion to utils * Export failed validation * Use new failed validation error in validate-batch * Enhance typing output of createError * Remove outdir (handled by tsup now) * Replace Exception with Error * Replace exception in test * Remove exceptions from app * Remove exceptions from app * Remove failed validation exception from users service * Remove old failed validation exception from shared * Remove exceptions package in favor of errors * Uninstall exceptions * Replace baseexception check * Migrate content too large error * Critical detail * Replace ForbiddenException * WIP remove exceptions * Add ForbiddenError to errors * HitRateLimitError * Move validation related error/helper to new validation package * Add index * Add docs * Install random * Convert TokenExpired * Convert user-suspended * Convert invalid-credentials * Move UnsupportedMediaType * Replace wrong imports for forbidden * Convert invalid-ip * Move invalid provider * Move InvalidOtp * Convert InvalidToken * Move MethodNotAllowed * Convert range not satisfiable * Move unexpect response * Move UnprocessableContent * Move IllegalAssetTransformation * Move RouteNotFound * Finalize not found * Various db errors * Move value too long * Move not null * Move record-not-unique * Move value out of range * Finish db errors * Service unavailable * GQL errors * Update packages/validation/src/errors/failed-validation.ts Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com> * Update packages/validation/src/errors/failed-validation.ts Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com> * InvalidQuery * Add test for invalid query message constructor * Invalid Payload * Finalize exceptions move * Improve type of isDirectusError * Various fixes * Fix build in api * Update websocket exceptions use * Allow optional reason for invalid config * Update errors usage in utils * Remove unused package from errors * Update lockfile * Update api/src/auth/drivers/ldap.ts Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com> * Update packages/validation/src/utils/joi-to-error-extensions.ts Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com> * Put error codes in shared enum * Replace instanceof checks in api * Fix tests I think * Tweak override names * Fix linter warnings * Set snapshots * Start fixing BB tests * Fix blackbox tests * Add changeset * Update changeset * Update extension docs to use new createError abstraction * 🙄 * Fix graphql validation error name * 🥳 * use ErrorCode.Forbidden * fix blackbox auth login test * Add license files * Rename preMutationException to preMutationError * Remove unused ms dep & sort package.json * Remove periods from error messages for consistency Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com> * Add optional code check * Use updated error code checker * Rename InvalidConfigError to InvalidProviderConfigError --------- Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com> Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch> Co-authored-by: ian <licitdev@gmail.com>
675 lines
18 KiB
TypeScript
675 lines
18 KiB
TypeScript
import {
|
|
APP_EXTENSION_TYPES,
|
|
APP_SHARED_DEPS,
|
|
HYBRID_EXTENSION_TYPES,
|
|
JAVASCRIPT_FILE_EXTS,
|
|
NESTED_EXTENSION_TYPES,
|
|
} from '@directus/constants';
|
|
import type {
|
|
ActionHandler,
|
|
ApiExtension,
|
|
BundleExtension,
|
|
EmbedHandler,
|
|
EndpointConfig,
|
|
Extension,
|
|
ExtensionInfo,
|
|
ExtensionType,
|
|
FilterHandler,
|
|
HookConfig,
|
|
HybridExtension,
|
|
InitHandler,
|
|
NestedExtensionType,
|
|
OperationApiConfig,
|
|
ScheduleHandler,
|
|
} from '@directus/types';
|
|
import { isIn, isTypeIn, pluralize } from '@directus/utils';
|
|
import {
|
|
ensureExtensionDirs,
|
|
generateExtensionsEntrypoint,
|
|
getLocalExtensions,
|
|
getPackageExtensions,
|
|
pathToRelativeUrl,
|
|
resolvePackage,
|
|
resolvePackageExtensions,
|
|
} from '@directus/utils/node';
|
|
import aliasDefault from '@rollup/plugin-alias';
|
|
import nodeResolveDefault from '@rollup/plugin-node-resolve';
|
|
import virtualDefault from '@rollup/plugin-virtual';
|
|
import chokidar, { FSWatcher } from 'chokidar';
|
|
import express, { Router } from 'express';
|
|
import { clone, escapeRegExp } from 'lodash-es';
|
|
import { readdir } from 'node:fs/promises';
|
|
import { createRequire } from 'node:module';
|
|
import { dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import path from 'path';
|
|
import { rollup } from 'rollup';
|
|
import getDatabase from './database/index.js';
|
|
import emitter, { Emitter } from './emitter.js';
|
|
import env from './env.js';
|
|
import { getFlowManager } from './flows.js';
|
|
import logger from './logger.js';
|
|
import * as services from './services/index.js';
|
|
import type { EventHandler } from './types/index.js';
|
|
import getModuleDefault from './utils/get-module-default.js';
|
|
import { getSchema } from './utils/get-schema.js';
|
|
import { JobQueue } from './utils/job-queue.js';
|
|
import { scheduleSynchronizedJob, validateCron } from './utils/schedule.js';
|
|
import { Url } from './utils/url.js';
|
|
|
|
// Workaround for https://github.com/rollup/plugins/issues/1329
|
|
const virtual = virtualDefault as unknown as typeof virtualDefault.default;
|
|
const alias = aliasDefault as unknown as typeof aliasDefault.default;
|
|
const nodeResolve = nodeResolveDefault as unknown as typeof nodeResolveDefault.default;
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
let extensionManager: ExtensionManager | undefined;
|
|
|
|
export function getExtensionManager(): ExtensionManager {
|
|
if (extensionManager) {
|
|
return extensionManager;
|
|
}
|
|
|
|
extensionManager = new ExtensionManager();
|
|
|
|
return extensionManager;
|
|
}
|
|
|
|
type BundleConfig = {
|
|
endpoints: { name: string; config: EndpointConfig }[];
|
|
hooks: { name: string; config: HookConfig }[];
|
|
operations: { name: string; config: OperationApiConfig }[];
|
|
};
|
|
|
|
type AppExtensions = string | null;
|
|
|
|
type ApiExtensions = { path: string }[];
|
|
|
|
type Options = {
|
|
schedule: boolean;
|
|
watch: boolean;
|
|
};
|
|
|
|
const defaultOptions: Options = {
|
|
schedule: true,
|
|
watch: env['EXTENSIONS_AUTO_RELOAD'] && env['NODE_ENV'] !== 'development',
|
|
};
|
|
|
|
class ExtensionManager {
|
|
private isLoaded = false;
|
|
private options: Options;
|
|
|
|
private extensions: Extension[] = [];
|
|
|
|
private appExtensions: AppExtensions = null;
|
|
private appExtensionChunks: Map<string, string>;
|
|
private apiExtensions: ApiExtensions = [];
|
|
|
|
private apiEmitter: Emitter;
|
|
private hookEvents: EventHandler[] = [];
|
|
private endpointRouter: Router;
|
|
private hookEmbedsHead: string[] = [];
|
|
private hookEmbedsBody: string[] = [];
|
|
|
|
private reloadQueue: JobQueue;
|
|
private watcher: FSWatcher | null = null;
|
|
|
|
constructor() {
|
|
this.options = defaultOptions;
|
|
|
|
this.apiEmitter = new Emitter();
|
|
this.endpointRouter = Router();
|
|
|
|
this.reloadQueue = new JobQueue();
|
|
|
|
this.appExtensionChunks = new Map();
|
|
}
|
|
|
|
public async initialize(options: Partial<Options> = {}): Promise<void> {
|
|
this.options = {
|
|
...defaultOptions,
|
|
...options,
|
|
};
|
|
|
|
const wasWatcherInitialized = this.watcher !== null;
|
|
|
|
if (this.options.watch && !wasWatcherInitialized) {
|
|
this.initializeWatcher();
|
|
} else if (!this.options.watch && wasWatcherInitialized) {
|
|
await this.closeWatcher();
|
|
}
|
|
|
|
if (!this.isLoaded) {
|
|
await this.load();
|
|
|
|
const loadedExtensions = this.getExtensionsList();
|
|
|
|
if (loadedExtensions.length > 0) {
|
|
logger.info(`Loaded extensions: ${loadedExtensions.map((ext) => ext.name).join(', ')}`);
|
|
}
|
|
}
|
|
|
|
if (this.options.watch && !wasWatcherInitialized) {
|
|
this.updateWatchedExtensions(this.extensions);
|
|
}
|
|
}
|
|
|
|
public reload(): void {
|
|
this.reloadQueue.enqueue(async () => {
|
|
if (this.isLoaded) {
|
|
logger.info('Reloading extensions');
|
|
|
|
const prevExtensions = clone(this.extensions);
|
|
|
|
await this.unload();
|
|
await this.load();
|
|
|
|
const added = this.extensions.filter(
|
|
(extension) => !prevExtensions.some((prevExtension) => extension.path === prevExtension.path)
|
|
);
|
|
|
|
const removed = prevExtensions.filter(
|
|
(prevExtension) => !this.extensions.some((extension) => prevExtension.path === extension.path)
|
|
);
|
|
|
|
this.updateWatchedExtensions(added, removed);
|
|
|
|
const addedExtensions = added.map((extension) => extension.name);
|
|
const removedExtensions = removed.map((extension) => extension.name);
|
|
|
|
if (addedExtensions.length > 0) {
|
|
logger.info(`Added extensions: ${addedExtensions.join(', ')}`);
|
|
}
|
|
|
|
if (removedExtensions.length > 0) {
|
|
logger.info(`Removed extensions: ${removedExtensions.join(', ')}`);
|
|
}
|
|
} else {
|
|
logger.warn('Extensions have to be loaded before they can be reloaded');
|
|
}
|
|
});
|
|
}
|
|
|
|
public getExtensionsList(type?: ExtensionType) {
|
|
if (type === undefined) {
|
|
return this.extensions.map(mapInfo);
|
|
} else {
|
|
return this.extensions.map(mapInfo).filter((extension) => extension.type === type);
|
|
}
|
|
|
|
function mapInfo(extension: Extension): ExtensionInfo {
|
|
const extensionInfo: ExtensionInfo = {
|
|
name: extension.name,
|
|
type: extension.type,
|
|
local: extension.local,
|
|
entries: [],
|
|
};
|
|
|
|
if (extension.host) extensionInfo.host = extension.host;
|
|
if (extension.version) extensionInfo.version = extension.version;
|
|
|
|
if (extension.type === 'bundle') {
|
|
const bundleExtensionInfo: Omit<BundleExtension, 'entrypoint' | 'path'> = {
|
|
name: extensionInfo.name,
|
|
type: 'bundle',
|
|
local: extensionInfo.local,
|
|
entries: extension.entries.map((entry) => ({
|
|
name: entry.name,
|
|
type: entry.type,
|
|
})) as { name: ExtensionInfo['name']; type: NestedExtensionType }[],
|
|
};
|
|
|
|
return bundleExtensionInfo;
|
|
} else {
|
|
return extensionInfo;
|
|
}
|
|
}
|
|
}
|
|
|
|
public getExtension(name: string): Extension | undefined {
|
|
return this.extensions.find((extension) => extension.name === name);
|
|
}
|
|
|
|
public getAppExtensions(): string | null {
|
|
return this.appExtensions;
|
|
}
|
|
|
|
public getAppExtensionChunk(name: string): string | null {
|
|
return this.appExtensionChunks.get(name) ?? null;
|
|
}
|
|
|
|
public getEndpointRouter(): Router {
|
|
return this.endpointRouter;
|
|
}
|
|
|
|
public getEmbeds() {
|
|
return {
|
|
head: wrapEmbeds('Custom Embed Head', this.hookEmbedsHead),
|
|
body: wrapEmbeds('Custom Embed Body', this.hookEmbedsBody),
|
|
};
|
|
|
|
function wrapEmbeds(label: string, content: string[]): string {
|
|
if (content.length === 0) return '';
|
|
return `<!-- Start ${label} -->\n${content.join('\n')}\n<!-- End ${label} -->`;
|
|
}
|
|
}
|
|
|
|
private async load(): Promise<void> {
|
|
try {
|
|
await ensureExtensionDirs(env['EXTENSIONS_PATH'], NESTED_EXTENSION_TYPES);
|
|
|
|
this.extensions = await this.getExtensions();
|
|
} catch (err: any) {
|
|
logger.warn(`Couldn't load extensions`);
|
|
logger.warn(err);
|
|
}
|
|
|
|
await this.registerHooks();
|
|
await this.registerEndpoints();
|
|
await this.registerOperations();
|
|
await this.registerBundles();
|
|
|
|
if (env['SERVE_APP']) {
|
|
this.appExtensions = await this.generateExtensionBundle();
|
|
}
|
|
|
|
this.isLoaded = true;
|
|
}
|
|
|
|
private async unload(): Promise<void> {
|
|
await this.unregisterApiExtensions();
|
|
|
|
this.apiEmitter.offAll();
|
|
|
|
if (env['SERVE_APP']) {
|
|
this.appExtensions = null;
|
|
}
|
|
|
|
this.isLoaded = false;
|
|
}
|
|
|
|
private initializeWatcher(): void {
|
|
logger.info('Watching extensions for changes...');
|
|
|
|
const extensionDirUrl = pathToRelativeUrl(env['EXTENSIONS_PATH']);
|
|
|
|
const localExtensionUrls = NESTED_EXTENSION_TYPES.flatMap((type) => {
|
|
const typeDir = path.posix.join(extensionDirUrl, pluralize(type));
|
|
|
|
if (isIn(type, HYBRID_EXTENSION_TYPES)) {
|
|
return [
|
|
path.posix.join(typeDir, '*', `app.{${JAVASCRIPT_FILE_EXTS.join()}}`),
|
|
path.posix.join(typeDir, '*', `api.{${JAVASCRIPT_FILE_EXTS.join()}}`),
|
|
];
|
|
} else {
|
|
return path.posix.join(typeDir, '*', `index.{${JAVASCRIPT_FILE_EXTS.join()}}`);
|
|
}
|
|
});
|
|
|
|
this.watcher = chokidar.watch(
|
|
[path.resolve('package.json'), path.posix.join(extensionDirUrl, '*', 'package.json'), ...localExtensionUrls],
|
|
{
|
|
ignoreInitial: true,
|
|
}
|
|
);
|
|
|
|
this.watcher
|
|
.on('add', () => this.reload())
|
|
.on('change', () => this.reload())
|
|
.on('unlink', () => this.reload());
|
|
}
|
|
|
|
private async closeWatcher(): Promise<void> {
|
|
if (this.watcher) {
|
|
await this.watcher.close();
|
|
|
|
this.watcher = null;
|
|
}
|
|
}
|
|
|
|
private updateWatchedExtensions(added: Extension[], removed: Extension[] = []): void {
|
|
if (this.watcher) {
|
|
const toPackageExtensionPaths = (extensions: Extension[]) =>
|
|
extensions
|
|
.filter((extension) => !extension.local || extension.type === 'bundle')
|
|
.flatMap((extension) =>
|
|
isTypeIn(extension, HYBRID_EXTENSION_TYPES) || extension.type === 'bundle'
|
|
? [
|
|
path.resolve(extension.path, extension.entrypoint.app),
|
|
path.resolve(extension.path, extension.entrypoint.api),
|
|
]
|
|
: path.resolve(extension.path, extension.entrypoint)
|
|
);
|
|
|
|
const addedPackageExtensionPaths = toPackageExtensionPaths(added);
|
|
const removedPackageExtensionPaths = toPackageExtensionPaths(removed);
|
|
|
|
this.watcher.add(addedPackageExtensionPaths);
|
|
this.watcher.unwatch(removedPackageExtensionPaths);
|
|
}
|
|
}
|
|
|
|
private async getExtensions(): Promise<Extension[]> {
|
|
const packageExtensions = await getPackageExtensions(env['PACKAGE_FILE_LOCATION']);
|
|
const localPackageExtensions = await resolvePackageExtensions(env['EXTENSIONS_PATH']);
|
|
const localExtensions = await getLocalExtensions(env['EXTENSIONS_PATH']);
|
|
|
|
return [...packageExtensions, ...localPackageExtensions, ...localExtensions].filter(
|
|
(extension) => env['SERVE_APP'] || APP_EXTENSION_TYPES.includes(extension.type as any) === false
|
|
);
|
|
}
|
|
|
|
private async generateExtensionBundle(): Promise<string | null> {
|
|
const sharedDepsMapping = await this.getSharedDepsMapping(APP_SHARED_DEPS);
|
|
|
|
const internalImports = Object.entries(sharedDepsMapping).map(([name, path]) => ({
|
|
find: name,
|
|
replacement: path,
|
|
}));
|
|
|
|
const entrypoint = generateExtensionsEntrypoint(this.extensions);
|
|
|
|
try {
|
|
const bundle = await rollup({
|
|
input: 'entry',
|
|
external: Object.values(sharedDepsMapping),
|
|
makeAbsoluteExternalsRelative: false,
|
|
plugins: [virtual({ entry: entrypoint }), alias({ entries: internalImports }), nodeResolve({ browser: true })],
|
|
});
|
|
|
|
const { output } = await bundle.generate({ format: 'es', compact: true });
|
|
|
|
for (const out of output) {
|
|
if (out.type === 'chunk') {
|
|
this.appExtensionChunks.set(out.fileName, out.code);
|
|
}
|
|
}
|
|
|
|
await bundle.close();
|
|
|
|
return output[0].code;
|
|
} catch (error: any) {
|
|
logger.warn(`Couldn't bundle App extensions`);
|
|
logger.warn(error);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private async getSharedDepsMapping(deps: string[]): Promise<Record<string, string>> {
|
|
const appDir = await readdir(path.join(resolvePackage('@directus/app', __dirname), 'dist', 'assets'));
|
|
|
|
const depsMapping: Record<string, string> = {};
|
|
|
|
for (const dep of deps) {
|
|
const depRegex = new RegExp(`${escapeRegExp(dep.replace(/\//g, '_'))}\\.[0-9a-f]{8}\\.entry\\.js`);
|
|
const depName = appDir.find((file) => depRegex.test(file));
|
|
|
|
if (depName) {
|
|
const depUrl = new Url(env['PUBLIC_URL']).addPath('admin', 'assets', depName);
|
|
|
|
depsMapping[dep] = depUrl.toString({ rootRelative: true });
|
|
} else {
|
|
logger.warn(`Couldn't find shared extension dependency "${dep}"`);
|
|
}
|
|
}
|
|
|
|
return depsMapping;
|
|
}
|
|
|
|
private async registerHooks(): Promise<void> {
|
|
const hooks = this.extensions.filter((extension): extension is ApiExtension => extension.type === 'hook');
|
|
|
|
for (const hook of hooks) {
|
|
try {
|
|
const hookPath = path.resolve(hook.path, hook.entrypoint);
|
|
|
|
const hookInstance: HookConfig | { default: HookConfig } = await import(
|
|
`./${pathToRelativeUrl(hookPath, __dirname)}?t=${Date.now()}`
|
|
);
|
|
|
|
const config = getModuleDefault(hookInstance);
|
|
|
|
this.registerHook(config, hook.name);
|
|
|
|
this.apiExtensions.push({ path: hookPath });
|
|
} catch (error: any) {
|
|
logger.warn(`Couldn't register hook "${hook.name}"`);
|
|
logger.warn(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async registerEndpoints(): Promise<void> {
|
|
const endpoints = this.extensions.filter((extension): extension is ApiExtension => extension.type === 'endpoint');
|
|
|
|
for (const endpoint of endpoints) {
|
|
try {
|
|
const endpointPath = path.resolve(endpoint.path, endpoint.entrypoint);
|
|
|
|
const endpointInstance: EndpointConfig | { default: EndpointConfig } = await import(
|
|
`./${pathToRelativeUrl(endpointPath, __dirname)}?t=${Date.now()}`
|
|
);
|
|
|
|
const config = getModuleDefault(endpointInstance);
|
|
|
|
this.registerEndpoint(config, endpoint.name);
|
|
|
|
this.apiExtensions.push({ path: endpointPath });
|
|
} catch (error: any) {
|
|
logger.warn(`Couldn't register endpoint "${endpoint.name}"`);
|
|
logger.warn(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async registerOperations(): Promise<void> {
|
|
const internalOperations = await readdir(path.join(__dirname, 'operations'));
|
|
|
|
for (const operation of internalOperations) {
|
|
const operationInstance: OperationApiConfig | { default: OperationApiConfig } = await import(
|
|
`./operations/${operation}/index.js`
|
|
);
|
|
|
|
const config = getModuleDefault(operationInstance);
|
|
|
|
this.registerOperation(config);
|
|
}
|
|
|
|
const operations = this.extensions.filter(
|
|
(extension): extension is HybridExtension => extension.type === 'operation'
|
|
);
|
|
|
|
for (const operation of operations) {
|
|
try {
|
|
const operationPath = path.resolve(operation.path, operation.entrypoint.api!);
|
|
|
|
const operationInstance: OperationApiConfig | { default: OperationApiConfig } = await import(
|
|
`./${pathToRelativeUrl(operationPath, __dirname)}?t=${Date.now()}`
|
|
);
|
|
|
|
const config = getModuleDefault(operationInstance);
|
|
|
|
this.registerOperation(config);
|
|
|
|
this.apiExtensions.push({ path: operationPath });
|
|
} catch (error: any) {
|
|
logger.warn(`Couldn't register operation "${operation.name}"`);
|
|
logger.warn(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async registerBundles(): Promise<void> {
|
|
const bundles = this.extensions.filter((extension): extension is BundleExtension => extension.type === 'bundle');
|
|
|
|
for (const bundle of bundles) {
|
|
try {
|
|
const bundlePath = path.resolve(bundle.path, bundle.entrypoint.api);
|
|
|
|
const bundleInstances: BundleConfig | { default: BundleConfig } = await import(
|
|
`./${pathToRelativeUrl(bundlePath, __dirname)}?t=${Date.now()}`
|
|
);
|
|
|
|
const configs = getModuleDefault(bundleInstances);
|
|
|
|
for (const { config, name } of configs.hooks) {
|
|
this.registerHook(config, name);
|
|
}
|
|
|
|
for (const { config, name } of configs.endpoints) {
|
|
this.registerEndpoint(config, name);
|
|
}
|
|
|
|
for (const { config } of configs.operations) {
|
|
this.registerOperation(config);
|
|
}
|
|
|
|
this.apiExtensions.push({ path: bundlePath });
|
|
} catch (error: any) {
|
|
logger.warn(`Couldn't register bundle "${bundle.name}"`);
|
|
logger.warn(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private registerHook(register: HookConfig, name: string): void {
|
|
let scheduleIndex = 0;
|
|
|
|
const registerFunctions = {
|
|
filter: (event: string, handler: FilterHandler) => {
|
|
emitter.onFilter(event, handler);
|
|
|
|
this.hookEvents.push({
|
|
type: 'filter',
|
|
name: event,
|
|
handler,
|
|
});
|
|
},
|
|
action: (event: string, handler: ActionHandler) => {
|
|
emitter.onAction(event, handler);
|
|
|
|
this.hookEvents.push({
|
|
type: 'action',
|
|
name: event,
|
|
handler,
|
|
});
|
|
},
|
|
init: (event: string, handler: InitHandler) => {
|
|
emitter.onInit(event, handler);
|
|
|
|
this.hookEvents.push({
|
|
type: 'init',
|
|
name: event,
|
|
handler,
|
|
});
|
|
},
|
|
schedule: (cron: string, handler: ScheduleHandler) => {
|
|
if (validateCron(cron)) {
|
|
const job = scheduleSynchronizedJob(`${name}:${scheduleIndex}`, cron, async () => {
|
|
if (this.options.schedule) {
|
|
try {
|
|
await handler();
|
|
} catch (error: any) {
|
|
logger.error(error);
|
|
}
|
|
}
|
|
});
|
|
|
|
scheduleIndex++;
|
|
|
|
this.hookEvents.push({
|
|
type: 'schedule',
|
|
job,
|
|
});
|
|
} else {
|
|
logger.warn(`Couldn't register cron hook. Provided cron is invalid: ${cron}`);
|
|
}
|
|
},
|
|
embed: (position: 'head' | 'body', code: string | EmbedHandler) => {
|
|
const content = typeof code === 'function' ? code() : code;
|
|
|
|
if (content.trim().length === 0) {
|
|
logger.warn(`Couldn't register embed hook. Provided code is empty!`);
|
|
return;
|
|
}
|
|
|
|
if (position === 'head') {
|
|
this.hookEmbedsHead.push(content);
|
|
}
|
|
|
|
if (position === 'body') {
|
|
this.hookEmbedsBody.push(content);
|
|
}
|
|
},
|
|
};
|
|
|
|
register(registerFunctions, {
|
|
services,
|
|
env,
|
|
database: getDatabase(),
|
|
emitter: this.apiEmitter,
|
|
logger,
|
|
getSchema,
|
|
});
|
|
}
|
|
|
|
private registerEndpoint(config: EndpointConfig, name: string): void {
|
|
const register = typeof config === 'function' ? config : config.handler;
|
|
const routeName = typeof config === 'function' ? name : config.id;
|
|
|
|
const scopedRouter = express.Router();
|
|
this.endpointRouter.use(`/${routeName}`, scopedRouter);
|
|
|
|
register(scopedRouter, {
|
|
services,
|
|
env,
|
|
database: getDatabase(),
|
|
emitter: this.apiEmitter,
|
|
logger,
|
|
getSchema,
|
|
});
|
|
}
|
|
|
|
private registerOperation(config: OperationApiConfig): void {
|
|
const flowManager = getFlowManager();
|
|
|
|
flowManager.addOperation(config.id, config.handler);
|
|
}
|
|
|
|
private async unregisterApiExtensions(): Promise<void> {
|
|
for (const event of this.hookEvents) {
|
|
switch (event.type) {
|
|
case 'filter':
|
|
emitter.offFilter(event.name, event.handler);
|
|
break;
|
|
case 'action':
|
|
emitter.offAction(event.name, event.handler);
|
|
break;
|
|
case 'init':
|
|
emitter.offInit(event.name, event.handler);
|
|
break;
|
|
case 'schedule':
|
|
await event.job.stop();
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.hookEvents = [];
|
|
|
|
this.endpointRouter.stack = [];
|
|
|
|
const flowManager = getFlowManager();
|
|
|
|
flowManager.clearOperations();
|
|
|
|
for (const apiExtension of this.apiExtensions) {
|
|
delete require.cache[require.resolve(apiExtension.path)];
|
|
}
|
|
|
|
this.apiExtensions = [];
|
|
}
|
|
}
|