Skip to content

Commit f1370b7

Browse files
authored
Merge pull request #680 from devforth/next
Next
2 parents da9817e + 819d2a5 commit f1370b7

10 files changed

Lines changed: 197 additions & 20 deletions

File tree

adminforth/basePlugin.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export default class AdminForthPlugin implements IAdminForthPlugin {
2020
resourceConfig: AdminForthResource;
2121
className: string;
2222
activationOrder: number = 0;
23+
pluginsScope: 'resource' | 'global' = 'resource';
2324
shouldHaveSingleInstancePerWholeApp?: () => boolean;
2425

2526
constructor(pluginOptions: any, metaUrl: string) {
@@ -41,14 +42,27 @@ export default class AdminForthPlugin implements IAdminForthPlugin {
4142
}
4243

4344

45+
initializePluginInstanceId = (resourceConfig?: AdminForthResource) => {
46+
const uniqueness = this.instanceUniqueRepresentation(this.pluginOptions);
47+
let seed = '';
48+
if (resourceConfig) {
49+
seed = `af_pl_${this.constructor.name}_${resourceConfig?.resourceId || '_'}_${uniqueness}`;
50+
} else {
51+
seed = `af_pl_${this.constructor.name}_global_${uniqueness}`;
52+
}
53+
this.pluginInstanceId = md5hash(seed);
54+
afLogger.trace({seed, pluginInstanceId: this.pluginInstanceId}, `🪲 AdminForthPlugin.initializePluginInstanceId`);
55+
}
56+
4457
modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource, allPluginInstances?: {pi: AdminForthPlugin, resource: AdminForthResource}[]) {
4558
this.resourceConfig = resourceConfig;
46-
const uniqueness = this.instanceUniqueRepresentation(this.pluginOptions);
59+
this.initializePluginInstanceId(resourceConfig);
60+
this.adminforth = adminforth;
61+
}
4762

48-
const seed = `af_pl_${this.constructor.name}_${resourceConfig.resourceId}_${uniqueness}`;
49-
this.pluginInstanceId = md5hash(seed);
50-
afLogger.trace({seed, pluginInstanceId: this.pluginInstanceId}, `🪲 AdminForthPlugin.modifyResourceConfig`);
63+
modifyGlobalConfig(adminforth: IAdminForth) {
5164
this.adminforth = adminforth;
65+
this.initializePluginInstanceId();
5266
}
5367

5468
/**
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const globalPlugins = []

adminforth/commands/createApp/templates/index.ts.hbs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import path from 'path';
66
import { Filters } from 'adminforth';
77
import { initApi } from './api.js';
88
import { logger } from 'adminforth';
9+
import { globalPlugins } from './globalPlugins.js';
910

1011
const ADMIN_BASE_URL = '';
1112

@@ -66,6 +67,7 @@ export const admin = new AdminForth({
6667
resourceId: 'adminuser'
6768
},
6869
],
70+
globalPlugins: globalPlugins,
6971
});
7072

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

adminforth/commands/createApp/utils.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,11 @@ async function writeTemplateFiles(dirname, cwd, useNpm, includePrismaMigrations,
428428
src: 'custom/package.json.hbs',
429429
dest: 'custom/package.json',
430430
data: {}
431+
},
432+
{
433+
src: 'globalPlugins.ts.hbs',
434+
dest: 'globalPlugins.ts',
435+
data: {},
431436
}
432437
];
433438

adminforth/documentation/docs/tutorial/10-Advanced/01-plugin-development.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,42 @@ Default value of activationOrder for most plugins is `0`. Plugins with higher ac
510510
511511
To ensure that plugin activates before some other plugins set `activationOrder` to negative value.
512512
513+
## Making plugin global
514+
515+
Each plugin should define a scope: resource-level or global level.
516+
* Resource-level plugins should be used when plugin instance works only with one resource config or it's fields. Instances of resource-scope plugins should be installed into "plugins' field of resource config.
517+
* All other plugins should be global-scoped. Such plugins are installed to `globalPlugins` of root adminforth config. They might modify configs of many resources like default AuditLog plugin or use several database resources to store data like default Agent plugin.
518+
519+
To make your plugin global, you need to use `pluginsScope: 'global'` and `modifyGlobalConfig` instead of `modifyResourceConfig`
520+
521+
522+
```ts title="./your-global-plugin/index.ts"
523+
524+
...
525+
526+
export default class YourPugin extends AdminForthPlugin {
527+
options: PluginOptions;
528+
//diff-add
529+
pluginsScope: 'global'
530+
531+
...
532+
533+
//diff-remove
534+
async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
535+
//diff-remove
536+
super.modifyResourceConfig(adminforth, resourceConfig);
537+
//diff-add
538+
modifyGlobalConfig(adminforth: IAdminForth) {
539+
//diff-add
540+
super.modifyGlobalConfig(adminforth);
541+
542+
}
543+
544+
...
545+
546+
```
547+
548+
513549
## Splitting frontend logic into multiple files
514550
515551
In case your plugin `.vue` files getting too big, you can split them into multiple files (components).

adminforth/index.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import CodeInjector from './modules/codeInjector.js';
33
import ExpressServer from './servers/express.js';
44
import OpenApiRegistry from './servers/openapi.js';
55
// import FastifyServer from './servers/fastify.js';
6-
import { ADMINFORTH_VERSION, listify, suggestIfTypo, RateLimiter, RAMLock, getClientIp, isProbablyUUIDColumn, convertPeriodToSeconds, hookResponseError, md5hash, applyRegexValidation } from './modules/utils.js';
6+
import { ADMINFORTH_VERSION, listify, suggestIfTypo, RateLimiter, RAMLock, getClientIp, isProbablyUUIDColumn, convertPeriodToSeconds, hookResponseError, md5hash, applyRegexValidation, formatHugePluginError } from './modules/utils.js';
77
import {
88
type AdminForthConfig,
99
type IAdminForth,
@@ -290,13 +290,21 @@ class AdminForth implements IAdminForth {
290290
activatePlugins() {
291291
afLogger.trace('🔌🔌🔌 Activating plugins');
292292
const allPluginInstances = [];
293+
const globalPlugins = this.config.globalPlugins || [];
293294
for (let resource of this.config.resources) {
294295
afLogger.trace(`🔌 Checking plugins for resource: ${resource.resourceId}`);
295296
for (let pluginInstance of resource.plugins || []) {
296297
afLogger.trace(`🔌 Found plugin: ${pluginInstance.constructor.name} for resource ${resource.resourceId}`);
298+
if (pluginInstance.pluginsScope === 'global') {
299+
throw new Error(formatHugePluginError(`Please move plugin ${pluginInstance.constructor.name} instance to index.ts config.globalPlugins array.
300+
Details: Previously adminforth had only resource-level plugins. To keep project structure clean, in recent version of adminforth we introduced globalPlugins.
301+
Global plugins are installed on whole application and not only one resource (like agent, audit logs etc)
302+
`));
303+
}
297304
allPluginInstances.push({pi: pluginInstance, resource});
298305
}
299306
}
307+
allPluginInstances.push(...globalPlugins.map((pluginInstance) => ({pi: pluginInstance, resource: null})));
300308
afLogger.trace(`🔌 Total plugins to activate: ${allPluginInstances.length}`);
301309

302310
let activationLoopCounter = 0;
@@ -325,8 +333,12 @@ class AdminForth implements IAdminForth {
325333
unactivatedPlugins.forEach(
326334
({pi: pluginInstance, resource}, index) => {
327335
afLogger.trace(`Activating plugin: ${pluginInstance.constructor.name}`);
328-
afLogger.trace(`🔌 Activating plugin ${index + 1}/${unactivatedPlugins.length}: ${pluginInstance.constructor.name} for resource ${resource.resourceId}`);
329-
pluginInstance.modifyResourceConfig(this, resource, allPluginInstances);
336+
afLogger.trace(`🔌 Activating plugin ${index + 1}/${unactivatedPlugins.length}: ${pluginInstance.constructor.name} for resource ${resource ? resource.resourceId : 'global'}`);
337+
if (pluginInstance.pluginsScope === 'global'){
338+
pluginInstance.modifyGlobalConfig(this);
339+
} else {
340+
pluginInstance.modifyResourceConfig(this, resource, allPluginInstances);
341+
}
330342
afLogger.trace(`🔌 Plugin ${pluginInstance.constructor.name} modifyResourceConfig completed`);
331343

332344
const plugin = this.activatedPlugins.find((p) => p.pluginInstanceId === pluginInstance.pluginInstanceId);

adminforth/modules/utils.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -572,18 +572,18 @@ export async function cascadeChildrenDelete(resource: AdminForthResource, primar
572572
return { error: null };
573573
}
574574

575-
export function hookResponseError(hookResponse: {ok: boolean, error?: string | null}) {
576-
if (!hookResponse || typeof hookResponse.ok !== 'boolean') {
577-
throw new Error(`Hook beforeSave must return { ok: boolean, error?: string | null }`);
578-
}
579-
if (hookResponse.ok === false && !hookResponse.error) {
580-
return { error: hookResponse.error ?? 'Operation aborted by hook' };
581-
}
582-
if (hookResponse.error) {
583-
return { error: hookResponse.error };
575+
export function hookResponseError(hookResponse: {ok: boolean, error?: string | null}) {
576+
if (!hookResponse || typeof hookResponse.ok !== 'boolean') {
577+
throw new Error(`Hook beforeSave must return { ok: boolean, error?: string | null }`);
578+
}
579+
if (hookResponse.ok === false && !hookResponse.error) {
580+
return { error: hookResponse.error ?? 'Operation aborted by hook' };
581+
}
582+
if (hookResponse.error) {
583+
return { error: hookResponse.error };
584+
}
585+
return null;
584586
}
585-
return null;
586-
}
587587

588588
export function checkIfFieldIsInsideResourceColumns(fieldName: string, resource: AdminForthResource): boolean {
589589
for (const column of resource.columns) {
@@ -622,4 +622,26 @@ export function applyRegexValidation(value, validation) {
622622
}
623623
}
624624
}
625-
}
625+
}
626+
627+
export function formatHugePluginError(message: string) {
628+
const RED = '\x1b[31m';
629+
const BG = '\x1b[41m';
630+
const WHITE = '\x1b[97m';
631+
const BOLD = '\x1b[1m';
632+
const RESET = '\x1b[0m';
633+
634+
const horizontal = '═'.repeat(100);
635+
636+
return `
637+
${BG}${WHITE}${BOLD}
638+
${horizontal}
639+
${' '.repeat(100)}
640+
║ 🚨 PLUGIN CONFIGURATION ERROR${' '.repeat(69)}
641+
${' '.repeat(100)}
642+
${message.padEnd(98)}
643+
${' '.repeat(100)}
644+
${horizontal}
645+
${RESET}
646+
`;
647+
}

adminforth/types/Back.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,7 @@ export interface IAdminForthPlugin {
636636
pluginOptions: any;
637637
resourceConfig: AdminForthResource;
638638
className: string;
639+
pluginsScope: 'resource' | 'global';
639640

640641
/**
641642
* Before activating all plugins are sorted by this number and then activated in order.
@@ -653,7 +654,15 @@ export interface IAdminForthPlugin {
653654
* @param adminforth Instance of IAdminForth
654655
* @param resourceConfig Resource configuration object which will be modified by plugin
655656
*/
656-
modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource, allPluginInstances?: {pi: IAdminForthPlugin, resource: AdminForthResource}[]): void;
657+
modifyResourceConfig?(adminforth: IAdminForth, resourceConfig: AdminForthResource, allPluginInstances?: {pi: IAdminForthPlugin, resource: AdminForthResource}[]): void;
658+
659+
660+
/**
661+
* This method is used for plugins, applied in global scope (pluginsScope = 'global')
662+
* @param adminforth Instance of IAdminForth
663+
*/
664+
modifyGlobalConfig?(adminforth: IAdminForth): void;
665+
657666
componentPath(componentFile: string): string;
658667

659668
/**
@@ -1831,6 +1840,10 @@ export interface AdminForthInputConfig {
18311840
*/
18321841
componentsToExplicitRegister?: AdminForthComponentDeclarationFull[]
18331842

1843+
/**
1844+
* List of plugins that should be applied in global scope.
1845+
*/
1846+
globalPlugins?: Array<IAdminForthPlugin>,
18341847
}
18351848

18361849

dev-demo/globalPlugins.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import CompletionAdapterOpenAIResponses from '../adapters/adminforth-completion-adapter-openai-responses/index.js';
2+
import AdminForthAgent from '../plugins/adminforth-agent/index.js';
3+
import AdminForthPlugin from '../adminforth/basePlugin.js';
4+
5+
const OVH_AI_ENDPOINTS_BASE_URL = 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1';
6+
const ovhAiEndpointsAccessToken = process.env.OVH_AI_ENDPOINTS_ACCESS_TOKEN;
7+
const openAiResponsesApiKey = ovhAiEndpointsAccessToken || process.env.OPENAI_API_KEY;
8+
const usesOvhAiEndpoints = Boolean(ovhAiEndpointsAccessToken);
9+
10+
function createAgentCompletionAdapter(
11+
model: string,
12+
effort: 'low' | 'medium' | 'xhigh',
13+
) {
14+
return new CompletionAdapterOpenAIResponses({
15+
openAiApiKey: openAiResponsesApiKey as string,
16+
baseUrl: usesOvhAiEndpoints ? OVH_AI_ENDPOINTS_BASE_URL : undefined,
17+
model: usesOvhAiEndpoints ? 'gpt-oss-120b' : model,
18+
extraRequestBodyParameters: {
19+
...(usesOvhAiEndpoints ? { store: false } : {}),
20+
reasoning: {
21+
effort,
22+
},
23+
},
24+
});
25+
}
26+
27+
export const globalPlugins = [
28+
new AdminForthAgent({
29+
placeholderMessages: async ({ adminUser, httpExtra }) => {
30+
return [
31+
"What is a cars count in SQLite",
32+
"Build average car price by days chart in SQLite",
33+
]
34+
},
35+
modes: [
36+
{
37+
name: 'Balanced',
38+
completionAdapter: createAgentCompletionAdapter('gpt-5.4-mini', 'medium'),
39+
},
40+
{
41+
name: 'Fast',
42+
completionAdapter: createAgentCompletionAdapter('gpt-5.4-mini', 'low'),
43+
},
44+
{
45+
name: 'Smart Thinking',
46+
completionAdapter: createAgentCompletionAdapter('gpt-5.4', 'xhigh'),
47+
},
48+
],
49+
maxTokens: 10000,
50+
reasoning: 'none',
51+
sessionResource: {
52+
resourceId: 'sessions',
53+
idField: 'id',
54+
titleField: 'title',
55+
turnsField: 'turns',
56+
askerIdField: 'asker_id',
57+
createdAtField: 'created_at',
58+
},
59+
turnResource: {
60+
resourceId: 'turns',
61+
idField: 'id',
62+
sessionIdField: 'session_id',
63+
createdAtField: 'created_at',
64+
promptField: 'prompt',
65+
responseField: 'response',
66+
debugField: 'dubbug',
67+
},
68+
}),
69+
];

dev-demo/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import adminExternalIdentitiesResource from './resources/adminUserExternalIdenti
3434

3535
import { logger } from '../adminforth/modules/logger.js';
3636

37+
import { globalPlugins } from './globalPlugins.js';
38+
3739
const ADMIN_BASE_URL = '';
3840

3941
export const admin = new AdminForth({
@@ -241,6 +243,7 @@ export const admin = new AdminForth({
241243
resourceId: 'turns',
242244
}
243245
],
246+
globalPlugins: globalPlugins,
244247
});
245248

246249
let lastJobId: string | null = null;

0 commit comments

Comments
 (0)