Skip to main content

Schema Builder Plugin System

Schema Builder is designed as a platform-agnostic, open plugin system. Plugins are self-contained ES modules that can be loaded at runtime — no rebuild required. The architecture is intentionally decoupled: Schema Builder exposes a stable interface, and plugins talk to it through a declared contract.

Schema Builder Plugin System User Interface Schema Builder Plugin System User Interface

Design Philosophy​

Schema Builder is not just a UI tool. It is a centralized, extensible schema services platform that can operate as:

  • A standalone JSON-LD schema workbench
  • An embedded service inside SureClinical's web client (iframe)
  • A headless translation service invoked programmatically
  • A plugin host for services written in Node.js, Python, Java Spring Boot, or any platform that produces an ES module

Plugins run either directly inside Schema Builder (in-browser ES module execution) or via a sandboxed runtime such as OpenSandbox for isolation, multi-tenancy, and cross-platform plugin hosting.


Plugin Types​

TypeHookPurpose
translatortranslate(rows, sourceUri)Converts Mapper View rows into an output format: SQL DDL, OWL, CSV, etc.
exporterexport()Exports the entire active schema in a custom format
panelpanelComponent (Vue)Adds a new full panel to the Schema Builder UI
importerimport()Loads and merges an external schema source
validatorvalidate(schema)Runs custom validation rules, returns annotated issues

Plugin Contract​

Every plugin implements SchemaBuilderPlugin:

interface SchemaBuilderPlugin {
// Required by all types
id: string; // Reverse-DNS: 'com.acme.my-plugin'
name: string;
description: string;
pluginType: 'translator' | 'exporter' | 'panel' | 'importer' | 'validator';
version: string; // semver
author: string;
icon: string; // FontAwesome class, e.g. 'fa-solid fa-database'

// TRANSLATOR
formatId?: string; // key shown in Mapper View format dropdown
translate?: (rows: MappingRow[], sourceUri: string) => string | Promise<string>;

// PANEL
panelComponent?: Component; // Vue 3 component
supportedModes?: SessionMode[];
panelIcon?: string;
panelLabel?: string;

// IMPORTER
import?: () => Promise<string | null>; // returns JSON-LD string or null

// VALIDATOR
validate?: (schema: object) => Promise<ValidationIssue[]>;

// Lifecycle (all types)
onActivate?: () => void;
onDeactivate?: () => void;
}

interface ValidationIssue {
path: string; // JSON Pointer path, e.g. '/entities/0/@id'
severity: 'error' | 'warning' | 'info';
message: string;
}

Plugin Loading​

Plugins are loaded at runtime via dynamic ES import(). Two loading paths are supported:

From URL​

await loadPluginFromUrl('https://cdn.example.com/my-plugin.js');
// or local dev server:
await loadPluginFromUrl('http://localhost:4000/dist/my-plugin.js');

The module must export a valid SchemaBuilderPlugin as its default export. Schema Builder validates the contract before registration — invalid plugins are rejected with a descriptive error.

From File​

// User selects a .js file via the Plugin Manager dialog
await loadPluginFromFile(file); // File object from <input type="file">

The file is wrapped in a Blob URL, imported dynamically, then the URL is immediately revoked to prevent memory leaks.

Plugin Manager UI​

The Plugin Manager is opened via the ⋮ overflow menu → View Plugins. It shows all registered plugins with type, version, description, and an active toggle. Built-in plugins cannot be removed. User-loaded plugins can be toggled or removed.


Plugin Registry​

The registry is a reactive Vue 3 ref wrapping a Map<id, plugin>:

// Get all registered plugins (reactive — use in Vue templates)
const all = pluginRegistry.getAll; // computed<SchemaBuilderPlugin[]>

// Find a translator for a given format
const plugin = pluginRegistry.getTranslator('sql-duckdb');

// Check if a translator exists (used to enable/disable Export button)
const canExport = pluginRegistry.hasTranslator('sql-postgresql');

// Manual registration (for programmatic use)
pluginRegistry.register(myPlugin);
pluginRegistry.unregister('com.acme.my-plugin');

Panel auto-wiring: When a panel plugin is registered, it is automatically added to the Schema Builder toolbar — no manual wiring required. The plugin's panelComponent, panelLabel, and panelIcon are wired into the panel registry on register().


Built-in Plugins​

Schema Builder ships three built-in plugins (registered at app bootstrap):

Plugin IDTypeDescription
com.sureclinical.sql-postgresqltranslatorGenerates PostgreSQL CREATE TABLE DDL with FK inference
com.sureclinical.sql-duckdbtranslatorGenerates DuckDB-compatible DDL (VARCHAR, DOUBLE, TIMESTAMP)
com.sureclinical.seed-data-generatortranslatorGenerates realistic INSERT INTO SQL using faker.js with a seeded RNG

DDL Generation​

Both SQL plugins follow the same two-phase algorithm:

  1. Group phase — Mapper View rows with targetType === 'TABLE' or nodeType === 'owl:Class' start a new table. Subsequent non-TABLE rows become columns.
  2. FK inference phase — Columns with targetType === 'URI/IRI' or nodeType === 'owl:ObjectProperty' are checked against known table names. If a match is found, a REFERENCES constraint is emitted automatically.

Each table always receives an iri TEXT NOT NULL PRIMARY KEY column derived from @id.

Seed Dataset Generator​

The Seed Dataset Generator uses @faker-js/faker with faker.seed(42) for reproducible output — the same schema always produces identical INSERT statements.

  • Two-pass generation: Pass 1 pre-generates all IRIs for every table; Pass 2 emits INSERTs with round-robin FK references.
  • Row count control: Set a TABLE row's notes field to a number to override the default of 50 rows (max: 10,000).
  • Value selection: the plugin reads dataType, nodeType, and column name to pick the most semantically appropriate faker.js method.

Sandboxed Execution — OpenSandbox​

For multi-tenant deployments, plugin isolation, or hosting plugins authored on non-JavaScript platforms, Schema Builder supports sandboxed plugin execution via OpenSandbox.

OpenSandbox provides a lightweight, browser-compatible sandbox runtime that:

  • Isolates plugin code from the host application namespace
  • Enforces resource limits and capability restrictions
  • Supports loading plugins from CDN, private registries, or local file servers
  • Enables future plugin execution on server-side runtimes (Node.js, Deno, edge workers)

Sandboxed plugins communicate with Schema Builder through the same SchemaBuilderPlugin interface — the sandbox is transparent to the plugin author.


Writing a Plugin​

A minimal translator plugin:

// my-csv-exporter.js
export default {
id: 'com.acme.csv-exporter',
name: 'CSV Exporter',
description: 'Exports Mapper View rows as CSV',
pluginType: 'translator',
version: '1.0.0',
author: 'Acme Corp',
icon: 'fa-solid fa-file-csv',
formatId: 'csv',

translate(rows, sourceUri) {
const selected = rows.filter(r => r.selected);
const header = 'name,type,nullable\n';
const body = selected
.map(r => `${r.name},${r.targetType},${r.nullable ?? true}`)
.join('\n');
return header + body;
},

onActivate() {
console.log('CSV Exporter activated');
},
};

Load it in the Plugin Manager or programmatically:

await loadPluginFromUrl('https://my-cdn.example.com/csv-exporter.js');
// or via file upload in Plugin Manager UI

Once registered, csv appears in the Mapper View format dropdown and the Export button becomes active.

Platform Notes​

PlatformApproach
Node.jsBuild as an ES module ("type": "module" in package.json), serve via local dev server or CDN
PythonUse py2js or a Python-to-ES-module bridge; alternatively expose a REST endpoint and write a thin JS adapter plugin that calls it
Java Spring BootExpose a REST endpoint; write a JS importer or translator plugin that calls it via fetch()
SandboxedAny platform — wrap in OpenSandbox runtime for isolation and cross-origin safety

Plugin Lifecycle​

loadPluginFromUrl / loadPluginFromFile
│
▼
validatePlugin() ←── throws if id/name/pluginType/version/author missing
│
▼
pluginRegistry.register(plugin)
│
├── onActivate() called
├── panel plugins → auto-wired into panelTypeRegistry
└── translator plugins → available in Mapper View dropdown

··· (plugin is active) ···

pluginRegistry.unregister(id)
│
├── onDeactivate() called
└── panel plugins → removed from panelTypeRegistry