Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
GDB_VERSION=11.1.0-TR11
GDB_VERSION=11.1.0-RC8
GRAPHDB_URL=http://graphdb:7200
WORKBENCH_PORT=9000
UID=1000
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ coverage/
# Test environment
graphdb.license
/translation-report.json
/plugins/
4 changes: 2 additions & 2 deletions e2e-tests/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion e2e-tests/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "graphdb-workbench-tests",
"version": "3.1.0-RC1",
"version": "3.1.0-plugins1",
"description": "Cypress tests for GraphDB workbench",
"scripts": {
"prepack": "npm shrinkwrap",
Expand Down
11 changes: 9 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "graphdb-workbench",
"version": "3.1.0-RC1",
"version": "3.1.0-plugins1",
"description": "The web application for GraphDB APIs",
"engines": {
"node": ">=22.17"
Expand Down Expand Up @@ -33,6 +33,7 @@
"validate": "sh scripts/validate.sh",
"test": "sh scripts/test.sh",
"test:coverage": "sh scripts/test-coverage.sh",
"copy-plugins": "sh scripts/copy-plugins.sh",
"cy:run": "sh scripts/cy.sh",
"sonar": "sh scripts/sonar.sh",
"hotdeploy": "node scripts/hotdeploy.js",
Expand Down Expand Up @@ -98,6 +99,7 @@
"resolutions": {},
"dependencies": {
"@single-spa/import-map-injector": "^2.0.2",
"graphdb-workbench-plugins": "^0.0.1-TR14",
"import-map-overrides": "^6.0.1"
}
}
2 changes: 1 addition & 1 deletion packages/api/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {SingleSpa} from './src/models/single-spa/single-spa';
import {PluginRegistry} from './src/models/plugin-registry/plugin-registry';
import {PluginRegistry} from './src/models/plugins/plugin-registry';

declare global {
interface Window {
Expand Down
6 changes: 0 additions & 6 deletions packages/api/src/models/plugin-registry/plugin-registry.ts

This file was deleted.

7 changes: 7 additions & 0 deletions packages/api/src/models/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export * from './plugin-registry';
export * from './plugin-definition';
export * from './plugin-module';
export * from './plugins-manifest';
export * from './extension-point';
export * from './plugins-manifest-response';
export * from './plugin-definition-list';
12 changes: 12 additions & 0 deletions packages/api/src/models/plugins/plugin-definition-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {ModelList} from '../common';
import {PluginDefinition} from './plugin-definition';

/**
* PluginDefinitionList is a class that extends ModelList to manage a list of PluginDefinition objects.
*/
export class PluginDefinitionList extends ModelList<PluginDefinition> {

constructor(data?: PluginDefinition[]) {
super(data);
}
}
13 changes: 13 additions & 0 deletions packages/api/src/models/plugins/plugin-definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* PluginDefinition interface represents the structure of a plugin definition as used in the plugin manifest.
*/
export interface PluginDefinition {
/**
* The entry point of the plugin, typically a path to the main JavaScript file.
*/
entry: string;
/**
* The name of the plugin.
*/
name: string;
}
12 changes: 12 additions & 0 deletions packages/api/src/models/plugins/plugin-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {PluginRegistry} from './plugin-registry';

/**
* PluginModule interface represents a plugin module that can be registered with the PluginRegistry.
*/
export interface PluginModule {
/**
* Registers the plugin module with the provided PluginRegistry.
* @param registry - The PluginRegistry instance to register the plugin module with.
*/
register: (registry: PluginRegistry) => void;
}
21 changes: 21 additions & 0 deletions packages/api/src/models/plugins/plugin-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {ExtensionPoint} from './extension-point';
import {PluginDefinition} from './plugin-definition';

/**
* PluginRegistry interface defines the methods for managing plugins in the application.
*/
export interface PluginRegistry {
/**
* Retrieves the plugin definition for a given extension point.
* @param extensionPoint - The extension point for which to retrieve the plugin definition.
* @return The plugin definition associated with the specified extension point.
*/
get<T>(extensionPoint: ExtensionPoint): T;

/**
* Adds a plugin definition to the registry for a specific extension point.
* @param extensionPoint - The extension point to which the plugin definition should be added.
* @param pluginDefinition - The plugin definition to add to the registry.
*/
add(extensionPoint: ExtensionPoint, pluginDefinition: PluginDefinition): void;
}
6 changes: 6 additions & 0 deletions packages/api/src/models/plugins/plugins-manifest-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface PluginsManifestResponse {
plugins: {
name: string;
entry: string;
}[];
}
23 changes: 23 additions & 0 deletions packages/api/src/models/plugins/plugins-manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {Model} from '../common';
import {PluginDefinitionList} from './plugin-definition-list';

/**
* PluginsManifest is a class that represents the manifest of plugins in the application.
* It contains a list of plugin definitions and provides methods to access them.
*/
export class PluginsManifest extends Model<PluginsManifest> {
private readonly _pluginDefinitions: PluginDefinitionList;

constructor(plugins: PluginDefinitionList) {
super();
this._pluginDefinitions = plugins;
}

/**
* Retrieves the list of plugin definitions in the manifest.
* @return The PluginDefinitionList containing all plugin definitions.
*/
getPluginDefinitions(): PluginDefinitionList {
return this._pluginDefinitions;
}
}
2 changes: 2 additions & 0 deletions packages/api/src/ontotext-workbench-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export * from './models/toastr';
export * from './models/rdf-search';
export * from './models/single-spa';
export * from './models/app-lifecycle';
export * from './models/plugins';

// Export providers for external usages.
export * from './providers';
Expand All @@ -40,6 +41,7 @@ export * from './services/rdf-search';
export * from './services/navigation';
export * from './services/app-lifecycle';
export * from './services/window';
export * from './services/plugins';

// Export utils for external usages.
export * from './services/utils';
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/services/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './plugins-rest.service';
export * from './plugins.service';
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {Mapper} from '../../../providers/mapper/mapper';
import {PluginsManifest} from '../../../models/plugins';
import {PluginsManifestResponse} from '../../../models/plugins/plugins-manifest-response';
import {PluginDefinitionList} from '../../../models/plugins/plugin-definition-list';

export class PluginsManifestMapper extends Mapper<PluginsManifest> {

/**
* Maps the raw data to an instance of the {@link PluginsManifest} model.
*
* @param {PluginsManifestResponse} data - The raw data to be transformed into a model.
* @returns {PluginsManifest} - A new instance of the {@link PluginsManifest} model
*/
mapToModel(data: PluginsManifestResponse): PluginsManifest {
const pluginDefinitions = new PluginDefinitionList(data.plugins);
return new PluginsManifest(pluginDefinitions);
}
}
46 changes: 46 additions & 0 deletions packages/api/src/services/plugins/plugins-rest.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {HttpService} from '../http/http.service';
import {PluginModule, PluginsManifest, PluginsManifestResponse} from '../../models/plugins';
import {getOrigin} from '../utils';

// TODO: move this to a configuration file or environment variable
const MANIFEST_URL = 'plugins/plugins-manifest.json';

/**
* Service responsible for handling REST operations related to plugins.
*/
export class PluginsRestService extends HttpService {

/**
* Fetches the plugins manifest from the server.
*
* @returns A Promise that resolves to the plugins manifest object containing
* information about available plugins.
*/
getPluginsManifest(): Promise<PluginsManifestResponse> {
return this.get(MANIFEST_URL);
}

/**
* Dynamically loads all plugins defined in the plugins manifest.
*
* @param pluginsManifest - The manifest object containing the list of plugins to load.
* Each plugin definition includes entry points.
*
* @returns A Promise that resolves to an array of loaded plugin modules.
*/
async loadPlugins(pluginsManifest: PluginsManifest) {
return await Promise.all(
pluginsManifest.getPluginDefinitions().getItems().map(async (pluginDef) => {
try {
const entryUrl = pluginDef.entry.startsWith('http')
? pluginDef.entry
: `${getOrigin()}${pluginDef.entry}`;

return await import(/* webpackIgnore: true */ entryUrl) as PluginModule;
} catch (err) {
console.error(`Failed to load plugin ${pluginDef.name}:`, err);
}
})
);
}
}
61 changes: 61 additions & 0 deletions packages/api/src/services/plugins/plugins.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {Service} from '../../providers/service/service';
import {ServiceProvider} from '../../providers';
import {PluginsRestService} from './plugins-rest.service';
import {PluginsManifest, PluginModule} from '../../models/plugins';
import {WindowService} from '../window';
import {PluginsManifestMapper} from './mapper/plugins-manifest.mapper';

/**
* Service responsible for managing plugins in the application.
* Handles retrieving plugin manifests and loading plugins into the application.
*/
export class PluginsService implements Service {
private readonly pluginsRestService: PluginsRestService;
private readonly pluginsManifestMapper: PluginsManifestMapper;

constructor() {
this.pluginsRestService = ServiceProvider.get(PluginsRestService);
this.pluginsManifestMapper = new PluginsManifestMapper();
}

/**
* Retrieves the plugins manifest from the server.
*
* @returns A promise that resolves to the plugins manifest containing information about available plugins.
*/
async getPluginsManifest(): Promise<PluginsManifest> {
return await this.pluginsRestService.getPluginsManifest()
.then((response) => this.pluginsManifestMapper.mapToModel(response));
}

/**
* Loads all available plugins into the application.
*
* This method retrieves the plugins manifest, loads the plugin modules,
* filters out any undefined modules, and registers each valid plugin
* with the application's plugin registry.
*
* If the plugins manifest cannot be loaded, the application will continue to function with built-in plugins only.
*
* @returns A promise that resolves when all plugins have been loaded and registered.
*/
async loadPlugins(): Promise<void> {
let pluginsManifest: PluginsManifest | undefined = undefined;
try {
pluginsManifest = await this.getPluginsManifest();
} catch (error) {
console.warn('Failed to load plugins manifest. Continue with the built-in plugins only.', error);
// If the manifest cannot be loaded, we will continue with built-in plugins only.
// This allows the application to function without external plugins.
}

if (pluginsManifest) {
const pluginModules = await this.pluginsRestService.loadPlugins(pluginsManifest);
pluginModules
.filter((pluginModule): pluginModule is PluginModule => pluginModule !== undefined)
.forEach((pluginModule: PluginModule) => {
pluginModule.register(WindowService.getPluginRegistry());
});
}
}
}
42 changes: 42 additions & 0 deletions packages/api/src/services/plugins/test/plugins.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { PluginsService } from '../plugins.service';
import {TestUtil} from '../../utils/test/test-util';
import {ResponseMock} from '../../http/test/response-mock';
import {PluginsManifestResponse} from '../../../models/plugins';

describe('PluginsService', () => {
let pluginsService: PluginsService;

beforeEach(() => {
pluginsService = new PluginsService();
});

it('should retrieve plugins manifest', async () => {
const manifest: PluginsManifestResponse = {
plugins: [{
name: 'test-plugin',
entry: 'test-plugin.js'
}]
};
TestUtil.mockResponse(new ResponseMock('plugins/plugins-manifest.json').setResponse(manifest));

const manifestModel = await pluginsService.getPluginsManifest();

expect(manifestModel.getPluginDefinitions().getItems()).toEqual(manifest.plugins);
expect(manifestModel.getPluginDefinitions().getItems().length).toBe(1);
});

// TODO: Find a way to test dynamic imports in Jest
// it('should load and register plugins when manifest is available', async () => {
// const manifest: PluginsManifestResponse = {
// plugins: [{ name: 'test', entry: 'plugins/test.js' }]
// };
// TestUtil.mockResponse(new ResponseMock('plugins/plugins-manifest.json').setResponse(manifest));
// // TestUtil.mockResponse(new ResponseMock('http://localhost/plugins/test.js').setResponse(manifest));
// await pluginsService.loadPlugins();
// });
//
// it('should continue without plugins if manifest fails to load', async () => {
//
// });
});

Loading