Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions adminforth/basePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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();
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const globalPlugins = []
2 changes: 2 additions & 0 deletions adminforth/commands/createApp/templates/index.ts.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';

Expand Down Expand Up @@ -65,6 +66,7 @@ export const admin = new AdminForth({
resourceId: 'adminuser'
}
],
globalPlugins: globalPlugins,
});

if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
Expand Down
5 changes: 5 additions & 0 deletions adminforth/commands/createApp/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {},
}
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
15 changes: 12 additions & 3 deletions adminforth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
22 changes: 22 additions & 0 deletions adminforth/modules/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}
`;
}
15 changes: 14 additions & 1 deletion adminforth/types/Back.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;

/**
Expand Down Expand Up @@ -1745,6 +1754,10 @@ export interface AdminForthInputConfig {
*/
componentsToExplicitRegister?: AdminForthComponentDeclarationFull[]

/**
* List of plugins that should be applied in global scope.
*/
globalPlugins?: Array<IAdminForthPlugin>,
}


Expand Down
69 changes: 69 additions & 0 deletions dev-demo/globalPlugins.ts
Original file line number Diff line number Diff line change
@@ -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',
},
}),
];
3 changes: 3 additions & 0 deletions dev-demo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -229,6 +231,7 @@ export const admin = new AdminForth({
resourceId: 'turns',
}
],
globalPlugins: globalPlugins,
});

if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
Expand Down