From 22d1f656a4a496ee8260ebe751f004610cdf681b Mon Sep 17 00:00:00 2001 From: yaroslav8765 Date: Tue, 28 Apr 2026 11:01:36 +0300 Subject: [PATCH 1/4] feat: add global plugins --- adminforth/basePlugin.ts | 22 +++++++++--- adminforth/index.ts | 15 ++++++-- adminforth/modules/utils.ts | 22 ++++++++++++ adminforth/types/Back.ts | 15 +++++++- dev-demo/globalPlugins.ts | 69 +++++++++++++++++++++++++++++++++++++ dev-demo/index.ts | 3 ++ 6 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 dev-demo/globalPlugins.ts diff --git a/adminforth/basePlugin.ts b/adminforth/basePlugin.ts index 54844b4c4..6ce257678 100644 --- a/adminforth/basePlugin.ts +++ b/adminforth/basePlugin.ts @@ -20,6 +20,7 @@ export default class AdminForthPlugin implements IAdminForthPlugin { resourceConfig: AdminForthResource; className: string; activationOrder: number = 0; + pluginsScope: 'resource' | 'global' = 'resource'; shouldHaveSingleInstancePerWholeApp?: () => boolean; constructor(pluginOptions: any, metaUrl: string) { @@ -41,14 +42,27 @@ export default class AdminForthPlugin implements IAdminForthPlugin { } + initializePluginInstanceId = (resourceConfig?: AdminForthResource) => { + const uniqueness = this.instanceUniqueRepresentation(this.pluginOptions); + let seed = ''; + if (resourceConfig) { + seed = `af_pl_${this.constructor.name}_${resourceConfig?.resourceId || '_'}_${uniqueness}`; + } else { + seed = `af_pl_${this.constructor.name}_global_${uniqueness}`; + } + this.pluginInstanceId = md5hash(seed); + afLogger.trace({seed, pluginInstanceId: this.pluginInstanceId}, `🪲 AdminForthPlugin.initializePluginInstanceId`); + } + modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource, allPluginInstances?: {pi: AdminForthPlugin, resource: AdminForthResource}[]) { this.resourceConfig = resourceConfig; - const uniqueness = this.instanceUniqueRepresentation(this.pluginOptions); + this.initializePluginInstanceId(resourceConfig); + this.adminforth = adminforth; + } - const seed = `af_pl_${this.constructor.name}_${resourceConfig.resourceId}_${uniqueness}`; - this.pluginInstanceId = md5hash(seed); - afLogger.trace({seed, pluginInstanceId: this.pluginInstanceId}, `🪲 AdminForthPlugin.modifyResourceConfig`); + modifyGlobalConfig(adminforth: IAdminForth) { this.adminforth = adminforth; + this.initializePluginInstanceId(); } /** diff --git a/adminforth/index.ts b/adminforth/index.ts index 98889dd69..887dbc7ee 100644 --- a/adminforth/index.ts +++ b/adminforth/index.ts @@ -7,7 +7,7 @@ import CodeInjector from './modules/codeInjector.js'; import ExpressServer from './servers/express.js'; import OpenApiRegistry from './servers/openapi.js'; // import FastifyServer from './servers/fastify.js'; -import { ADMINFORTH_VERSION, listify, suggestIfTypo, RateLimiter, RAMLock, getClientIp, isProbablyUUIDColumn, convertPeriodToSeconds, hookResponseError } from './modules/utils.js'; +import { ADMINFORTH_VERSION, listify, suggestIfTypo, RateLimiter, RAMLock, getClientIp, isProbablyUUIDColumn, convertPeriodToSeconds, hookResponseError, formatHugePluginError } from './modules/utils.js'; import { type AdminForthConfig, type IAdminForth, @@ -237,13 +237,18 @@ class AdminForth implements IAdminForth { activatePlugins() { afLogger.trace('🔌🔌🔌 Activating plugins'); const allPluginInstances = []; + const globalPlugins = this.config.globalPlugins || []; for (let resource of this.config.resources) { afLogger.trace(`🔌 Checking plugins for resource: ${resource.resourceId}`); for (let pluginInstance of resource.plugins || []) { afLogger.trace(`🔌 Found plugin: ${pluginInstance.constructor.name} for resource ${resource.resourceId}`); + if (pluginInstance.pluginsScope === 'global') { + throw new Error(formatHugePluginError(`Move plugin ${pluginInstance.constructor.name} to index.ts config.globalPlugins array`)); + } allPluginInstances.push({pi: pluginInstance, resource}); } } + allPluginInstances.push(...globalPlugins.map((pluginInstance) => ({pi: pluginInstance, resource: null}))); afLogger.trace(`🔌 Total plugins to activate: ${allPluginInstances.length}`); let activationLoopCounter = 0; @@ -272,8 +277,12 @@ class AdminForth implements IAdminForth { unactivatedPlugins.forEach( ({pi: pluginInstance, resource}, index) => { afLogger.trace(`Activating plugin: ${pluginInstance.constructor.name}`); - afLogger.trace(`🔌 Activating plugin ${index + 1}/${unactivatedPlugins.length}: ${pluginInstance.constructor.name} for resource ${resource.resourceId}`); - pluginInstance.modifyResourceConfig(this, resource, allPluginInstances); + afLogger.trace(`🔌 Activating plugin ${index + 1}/${unactivatedPlugins.length}: ${pluginInstance.constructor.name} for resource ${resource ? resource.resourceId : 'global'}`); + if (pluginInstance.pluginsScope === 'global'){ + pluginInstance.modifyGlobalConfig(this); + } else { + pluginInstance.modifyResourceConfig(this, resource, allPluginInstances); + } afLogger.trace(`🔌 Plugin ${pluginInstance.constructor.name} modifyResourceConfig completed`); const plugin = this.activatedPlugins.find((p) => p.pluginInstanceId === pluginInstance.pluginInstanceId); diff --git a/adminforth/modules/utils.ts b/adminforth/modules/utils.ts index c14e76003..199b7d0f3 100644 --- a/adminforth/modules/utils.ts +++ b/adminforth/modules/utils.ts @@ -535,4 +535,26 @@ export async function cascadeChildrenDelete(resource: AdminForthResource, primar return { error: hookResponse.error }; } return null; + } + + export function formatHugePluginError(message: string) { + const RED = '\x1b[31m'; + const BG = '\x1b[41m'; + const WHITE = '\x1b[97m'; + const BOLD = '\x1b[1m'; + const RESET = '\x1b[0m'; + + const horizontal = '═'.repeat(100); + + return ` + ${BG}${WHITE}${BOLD} + ╔${horizontal}╗ + ║${' '.repeat(100)}║ + ║ 🚨 PLUGIN CONFIGURATION ERROR${' '.repeat(69)}║ + ║${' '.repeat(100)}║ + ║ ${message.padEnd(98)}║ + ║${' '.repeat(100)}║ + ╚${horizontal}╝ + ${RESET} +`; } \ No newline at end of file diff --git a/adminforth/types/Back.ts b/adminforth/types/Back.ts index 4425023da..4980c3429 100644 --- a/adminforth/types/Back.ts +++ b/adminforth/types/Back.ts @@ -574,6 +574,7 @@ export interface IAdminForthPlugin { pluginOptions: any; resourceConfig: AdminForthResource; className: string; + pluginsScope: 'resource' | 'global'; /** * Before activating all plugins are sorted by this number and then activated in order. @@ -591,7 +592,15 @@ export interface IAdminForthPlugin { * @param adminforth Instance of IAdminForth * @param resourceConfig Resource configuration object which will be modified by plugin */ - modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource, allPluginInstances?: {pi: IAdminForthPlugin, resource: AdminForthResource}[]): void; + modifyResourceConfig?(adminforth: IAdminForth, resourceConfig: AdminForthResource, allPluginInstances?: {pi: IAdminForthPlugin, resource: AdminForthResource}[]): void; + + + /** + * This method is used for plugins, applied in global scope (pluginsScope = 'global') + * @param adminforth Instance of IAdminForth + */ + modifyGlobalConfig?(adminforth: IAdminForth): void; + componentPath(componentFile: string): string; /** @@ -1745,6 +1754,10 @@ export interface AdminForthInputConfig { */ componentsToExplicitRegister?: AdminForthComponentDeclarationFull[] + /** + * List of plugins that should be applied in global scope. + */ + globalPlugins?: Array, } diff --git a/dev-demo/globalPlugins.ts b/dev-demo/globalPlugins.ts new file mode 100644 index 000000000..89d0e5309 --- /dev/null +++ b/dev-demo/globalPlugins.ts @@ -0,0 +1,69 @@ +import CompletionAdapterOpenAIResponses from '../adapters/adminforth-completion-adapter-openai-responses/index.js'; +import AdminForthAgent from '../plugins/adminforth-agent/index.js'; +import AdminForthPlugin from '../adminforth/basePlugin.js'; + +const OVH_AI_ENDPOINTS_BASE_URL = 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1'; +const ovhAiEndpointsAccessToken = process.env.OVH_AI_ENDPOINTS_ACCESS_TOKEN; +const openAiResponsesApiKey = ovhAiEndpointsAccessToken || process.env.OPENAI_API_KEY; +const usesOvhAiEndpoints = Boolean(ovhAiEndpointsAccessToken); + +function createAgentCompletionAdapter( + model: string, + effort: 'low' | 'medium' | 'xhigh', +) { + return new CompletionAdapterOpenAIResponses({ + openAiApiKey: openAiResponsesApiKey as string, + baseUrl: usesOvhAiEndpoints ? OVH_AI_ENDPOINTS_BASE_URL : undefined, + model: usesOvhAiEndpoints ? 'gpt-oss-120b' : model, + extraRequestBodyParameters: { + ...(usesOvhAiEndpoints ? { store: false } : {}), + reasoning: { + effort, + }, + }, + }); +} + +export const globalPlugins = [ + new AdminForthAgent({ + placeholderMessages: async ({ adminUser, httpExtra }) => { + return [ + "What is a cars count in SQLite", + "Build average car price by days chart in SQLite", + ] + }, + modes: [ + { + name: 'Balanced', + completionAdapter: createAgentCompletionAdapter('gpt-5.4-mini', 'medium'), + }, + { + name: 'Fast', + completionAdapter: createAgentCompletionAdapter('gpt-5.4-mini', 'low'), + }, + { + name: 'Smart Thinking', + completionAdapter: createAgentCompletionAdapter('gpt-5.4', 'xhigh'), + }, + ], + maxTokens: 10000, + reasoning: 'none', + sessionResource: { + resourceId: 'sessions', + idField: 'id', + titleField: 'title', + turnsField: 'turns', + askerIdField: 'asker_id', + createdAtField: 'created_at', + }, + turnResource: { + resourceId: 'turns', + idField: 'id', + sessionIdField: 'session_id', + createdAtField: 'created_at', + promptField: 'prompt', + responseField: 'response', + debugField: 'dubbug', + }, + }), +]; \ No newline at end of file diff --git a/dev-demo/index.ts b/dev-demo/index.ts index 2f75403af..9a530bb6e 100644 --- a/dev-demo/index.ts +++ b/dev-demo/index.ts @@ -31,6 +31,8 @@ import carsDescriptionImage from './resources/cars_description_image.js'; import translations from "./resources/translations.js"; import { logger } from '../adminforth/modules/logger.js'; +import { globalPlugins } from './globalPlugins.js'; + const ADMIN_BASE_URL = ''; export const admin = new AdminForth({ @@ -229,6 +231,7 @@ export const admin = new AdminForth({ resourceId: 'turns', } ], + globalPlugins: globalPlugins, }); if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) { From 9e86450697c62ccd7beeea09857e6da775ff373f Mon Sep 17 00:00:00 2001 From: yaroslav8765 Date: Tue, 28 Apr 2026 11:51:08 +0300 Subject: [PATCH 2/4] feat: add global plugins file to the newly created app --- adminforth/commands/createApp/templates/globalPlugins.ts.hbs | 1 + adminforth/commands/createApp/templates/index.ts.hbs | 2 ++ adminforth/commands/createApp/utils.js | 5 +++++ 3 files changed, 8 insertions(+) create mode 100644 adminforth/commands/createApp/templates/globalPlugins.ts.hbs diff --git a/adminforth/commands/createApp/templates/globalPlugins.ts.hbs b/adminforth/commands/createApp/templates/globalPlugins.ts.hbs new file mode 100644 index 000000000..5aecd2a82 --- /dev/null +++ b/adminforth/commands/createApp/templates/globalPlugins.ts.hbs @@ -0,0 +1 @@ +export const globalPlugins = [] \ No newline at end of file diff --git a/adminforth/commands/createApp/templates/index.ts.hbs b/adminforth/commands/createApp/templates/index.ts.hbs index 3fbabed30..97168fc5b 100644 --- a/adminforth/commands/createApp/templates/index.ts.hbs +++ b/adminforth/commands/createApp/templates/index.ts.hbs @@ -6,6 +6,7 @@ import path from 'path'; import { Filters } from 'adminforth'; import { initApi } from './api.js'; import { logger } from 'adminforth'; +import { globalPlugins } from './globalPlugins.js'; const ADMIN_BASE_URL = ''; @@ -65,6 +66,7 @@ export const admin = new AdminForth({ resourceId: 'adminuser' } ], + globalPlugins: globalPlugins, }); if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) { diff --git a/adminforth/commands/createApp/utils.js b/adminforth/commands/createApp/utils.js index 072db41a7..44b1c8e75 100644 --- a/adminforth/commands/createApp/utils.js +++ b/adminforth/commands/createApp/utils.js @@ -355,6 +355,11 @@ async function writeTemplateFiles(dirname, cwd, useNpm, options) { src: 'custom/package.json.hbs', dest: 'custom/package.json', data: {} + }, + { + src: 'globalPlugins.ts.hbs', + dest: 'globalPlugins.ts', + data: {}, } ]; From a44e568bbd2d7ebe313e607234be0f46b79d5ae3 Mon Sep 17 00:00:00 2001 From: yaroslav8765 Date: Tue, 28 Apr 2026 12:11:49 +0300 Subject: [PATCH 3/4] docs: update docs for the plugin development guide to explain how to use global scope plugins --- adminforth/basePlugin.ts | 2 +- .../09-Advanced/01-plugin-development.md | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/adminforth/basePlugin.ts b/adminforth/basePlugin.ts index 6ce257678..56bcb0a73 100644 --- a/adminforth/basePlugin.ts +++ b/adminforth/basePlugin.ts @@ -17,7 +17,7 @@ export default class AdminForthPlugin implements IAdminForthPlugin { pluginInstanceId: string; customFolderPath: string; pluginOptions: any; - resourceConfig: AdminForthResource; + resourceConfig?: AdminForthResource; className: string; activationOrder: number = 0; pluginsScope: 'resource' | 'global' = 'resource'; diff --git a/adminforth/documentation/docs/tutorial/09-Advanced/01-plugin-development.md b/adminforth/documentation/docs/tutorial/09-Advanced/01-plugin-development.md index 66f679c89..dc5f7bfe4 100644 --- a/adminforth/documentation/docs/tutorial/09-Advanced/01-plugin-development.md +++ b/adminforth/documentation/docs/tutorial/09-Advanced/01-plugin-development.md @@ -506,6 +506,38 @@ Default value of activationOrder for most plugins is `0`. Plugins with higher ac To ensure that plugin activates before some other plugins set `activationOrder` to negative value. +## Making plugin global + +If you want to make your plugin to be global and add it to the main `index.ts` in `globalPlugins` section, you need to use `pluginsScope: 'global'` and `modifyGlobalConfig` instead of `modifyResourceConfig`: + + +```ts title="./your-global-plugin/index.ts" + + ... + + export default class YourPugin extends AdminForthPlugin { + options: PluginOptions; + //diff-add + pluginsScope: 'global' + + ... + + //diff-remove + async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) { + //diff-remove + super.modifyResourceConfig(adminforth, resourceConfig); + //diff-add + modifyGlobalConfig(adminforth: IAdminForth) { + //diff-add + super.modifyGlobalConfig(adminforth); + + } + + ... + +``` + + ## Splitting frontend logic into multiple files In case your plugin `.vue` files getting too big, you can split them into multiple files (components). From 27b093d8df0fed7200586f9b8405e26826d310e5 Mon Sep 17 00:00:00 2001 From: yaroslav8765 Date: Tue, 28 Apr 2026 12:13:27 +0300 Subject: [PATCH 4/4] fix: rebuild --- adminforth/basePlugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adminforth/basePlugin.ts b/adminforth/basePlugin.ts index 56bcb0a73..6ce257678 100644 --- a/adminforth/basePlugin.ts +++ b/adminforth/basePlugin.ts @@ -17,7 +17,7 @@ export default class AdminForthPlugin implements IAdminForthPlugin { pluginInstanceId: string; customFolderPath: string; pluginOptions: any; - resourceConfig?: AdminForthResource; + resourceConfig: AdminForthResource; className: string; activationOrder: number = 0; pluginsScope: 'resource' | 'global' = 'resource';