@graph-knowledge/plugin-sdk
v1.1.0
Published
Plugin SDK for Graph Knowledge - build custom shape plugins for the graph editor
Maintainers
Readme
@graph-knowledge/plugin-sdk
Build custom shape plugins for Graph Knowledge — a graph-based visual knowledge tool.
Plugins add new shape types to the editor. They run in a sandboxed Web Worker, render via SVG descriptors, and support property editing, hit-testing, and schema migration.
Quick Start
1. Install the SDK (for types only)
npm install @graph-knowledge/plugin-sdkThe SDK provides TypeScript interfaces, validators, and constants. Your plugin bundle itself is a plain JavaScript file — no build step required.
2. Create your plugin
Create a plugin.js file:
exports.createPlugin = function (context) {
return {
id: "my-shape",
name: "My Shape",
version: "1.0.0",
icon: "star", // Material icon name (optional)
category: "Shapes", // Toolbar category (optional)
createElement: function (x, y, props) {
return {
id: context.generateId(),
elementType: "plugin:my-shape",
x: x,
y: y,
width: 100,
height: 80,
rotation: 0,
properties: {
fillColor: (props && props.fillColor) || "#4A90D9",
label: (props && props.label) || "My Shape"
}
};
},
render: function (element) {
return [
{
tag: "rect",
attrs: {
x: 0,
y: 0,
width: element.width,
height: element.height,
rx: 8,
fill: element.properties.fillColor || "#4A90D9",
stroke: "#2C5F8A",
"stroke-width": 2
}
},
{
tag: "text",
attrs: {
x: element.width / 2,
y: element.height / 2,
"text-anchor": "middle",
"dominant-baseline": "middle",
fill: "#FFFFFF",
"font-size": 14
},
content: element.properties.label || ""
}
];
},
getBoundingBox: function (element) {
return {
x: element.x,
y: element.y,
width: element.width,
height: element.height,
rotation: element.rotation || 0,
centerX: element.x + element.width / 2,
centerY: element.y + element.height / 2
};
},
containsPoint: function (element, x, y) {
var localX = x - element.x;
var localY = y - element.y;
return localX >= 0 && localX <= element.width &&
localY >= 0 && localY <= element.height;
}
};
};3. Upload to Graph Knowledge
Open the editor, go to Store > Upload Plugin, select your .js file, fill in the metadata, and upload. Your shape will appear in the toolbar.
Plugin Interface
Every plugin must export a createPlugin(context) function that returns a RuntimePlugin object.
Required Properties
| Property | Type | Description |
|----------|------|-------------|
| id | string | Unique identifier (alphanumeric, hyphens, underscores) |
| name | string | Display name shown in the toolbar and store |
| version | string | Semver version (e.g., "1.0.0") |
Required Methods
createElement(x, y, props?)
Create a new element at the given canvas position.
- Parameters:
x: number,y: number,props?: Record<string, unknown>(optional initial properties) - Returns:
PluginElement
The returned element must have elementType set to "plugin:<your-id>".
render(element)
Produce SVG descriptors for the given element.
- Parameters:
element: PluginElement - Returns:
SvgNode[]
Coordinates in the SVG output are relative to the element's local space (top-left is 0, 0). The host applies translation and rotation transforms.
getBoundingBox(element)
Return the axis-aligned bounding box for the element.
- Parameters:
element: PluginElement - Returns:
BoundingBox
containsPoint(element, x, y)
Hit-test whether a point (in canvas coordinates) falls inside the element.
- Parameters:
element: PluginElement,x: number,y: number - Returns:
boolean
Optional Properties
| Property | Type | Description |
|----------|------|-------------|
| icon | string | Material icon name for the toolbar button |
| category | string | Toolbar group (e.g., "Diagrams", "Shapes") |
| schemaVersion | number | Version of the element data schema (for migrations) |
Optional Methods
getPropertyEditors(element)
Return property editor descriptors for the properties panel.
- Returns:
PropertyEditor[]
updateProperty(element, propId, value)
Apply a property change and return the updated element. Must return a new object (immutable update).
- Returns:
PluginElement
normalizeElement(element)
Migrate or normalize an element's data (e.g., when schemaVersion changes). Called when loading elements from storage.
- Returns:
PluginElement
PluginContext
The context object passed to createPlugin() provides safe utilities:
| Method | Description |
|--------|-------------|
| generateId() | Generate a unique ID (crypto.randomUUID) |
PluginElement
The element data model visible to plugins:
interface PluginElement {
readonly id: string;
readonly elementType: string; // "plugin:<plugin-id>"
x: number;
y: number;
width: number;
height: number;
rotation: number; // degrees [0, 360)
properties: Record<string, unknown>;
}Store all custom data in properties. The host manages x, y, width, height, and rotation (drag, resize, rotate transforms are handled automatically).
SVG Rendering
Plugins render shapes by returning SvgNode[] descriptors. The host builds real SVG DOM from these — no innerHTML or raw HTML is used.
SvgNode
interface SvgNode {
tag: SvgTag;
attrs?: Record<string, string | number>;
content?: string; // text content (for text/tspan)
children?: SvgNode[];
}Allowed Tags
rect, circle, ellipse, path, line, polyline, polygon,
text, tspan, g, defs, use, linearGradient, radialGradient,
stop, clipPathTags outside this whitelist are stripped during validation.
Blocked Attributes
Event handler attributes (onclick, onload, onerror, onmouseover, etc.) are stripped. javascript: URLs in href/xlink:href are also blocked.
Limits
| Constraint | Limit | |------------|-------| | Maximum node count | 500 | | Maximum nesting depth | 10 |
Exceeding these limits produces validation errors and the shape won't render.
Coordinate System
All coordinates in render() output are in local element space:
- Top-left corner is
(0, 0) - Bottom-right corner is
(element.width, element.height)
The host applies the element's position and rotation transforms.
Property Editors
Define editable properties that appear in the properties panel when your shape is selected.
PropertyEditor
interface PropertyEditor {
propertyId: string; // key in element.properties
label: string; // display label
type: PropertyEditorType; // editor widget type
value: PropertyValue; // current value
options?: SelectOption[]; // for "select" type only
}Editor Types
| Type | Widget | Value Type |
|------|--------|------------|
| "text" | Text input | string |
| "number" | Number input | number |
| "color" | Color picker | string (hex) |
| "boolean" | Toggle | boolean |
| "textarea" | Multi-line text | string |
| "select" | Dropdown | string \| number |
For "select", provide options:
interface SelectOption {
label: string;
value: string | number;
}Round-trip
getPropertyEditors() reads from element.properties and returns editor descriptors. When the user changes a value, updateProperty(element, propId, value) is called. Return a new PluginElement with the updated property:
updateProperty: function (element, propId, value) {
var newProps = {};
for (var key in element.properties) {
newProps[key] = element.properties[key];
}
newProps[propId] = value;
return {
id: element.id,
elementType: element.elementType,
x: element.x,
y: element.y,
width: element.width,
height: element.height,
rotation: element.rotation,
properties: newProps
};
}Sandbox Restrictions
Plugins run in a Web Worker with a locked-down environment. The following APIs are not available:
fetch,XMLHttpRequest— no network accessdocument,window— no DOM accessindexedDB,localStorage,sessionStorage— no storageWebSocket,EventSource— no real-time connectionseval,Function()constructor — no dynamic code executionimportScripts— no additional script loading
Timeouts
| Operation | Timeout |
|-----------|---------|
| render() | 1 second |
| All other methods | 5 seconds |
If a method exceeds its timeout, the call is terminated and the shape displays an error state.
What IS available
Math,JSON,Date,RegExp, and other built-in JavaScript objectscrypto.randomUUID()(viacontext.generateId())console.log/warn/error(forwarded to host for debugging)setTimeout,clearTimeout(within the worker)
BoundingBox
interface BoundingBox {
x: number; // top-left x (canvas coordinates)
y: number; // top-left y (canvas coordinates)
width: number;
height: number;
rotation: number; // degrees
centerX: number; // x + width / 2
centerY: number; // y + height / 2
}Plugin Manifest
When publishing a plugin to the store, a manifest describes the plugin metadata:
interface PluginManifest {
id: string; // unique plugin ID
name: string; // display name (max 100 chars)
version: string; // semver (e.g., "1.0.0")
description: string; // short description (max 500 chars)
author: {
name: string;
url?: string;
};
icon?: string; // Material icon name
category?: string; // e.g., "Diagrams"
bundleSource: string; // inline JS source code of the plugin
bundleHash: string; // SHA-256 hex digest of the bundle
}ID format
Plugin IDs must contain only alphanumeric characters, hyphens, and underscores (/^[a-zA-Z0-9_-]+$/).
Bundle hash
Compute the SHA-256 hash of your bundle file:
shasum -a 256 plugin.js | awk '{print $1}'Or in Node.js:
const crypto = require("crypto");
const fs = require("fs");
const hash = crypto.createHash("sha256")
.update(fs.readFileSync("plugin.js", "utf-8"))
.digest("hex");Validators
The SDK exports validators you can use to check your plugin before publishing:
validatePlugin(plugin)
Duck-type validation of a RuntimePlugin instance. Checks that all required properties and methods exist with correct types.
const { validatePlugin } = require("@graph-knowledge/plugin-sdk");
const result = validatePlugin(myPlugin);
if (!result.valid) {
console.error("Plugin errors:", result.errors);
}validateSvgNodes(nodes)
Validate and sanitize an array of SvgNode descriptors. Checks tag whitelist, blocked attributes, depth limits, and node count limits. Returns sanitized nodes with invalid content stripped.
const { validateSvgNodes } = require("@graph-knowledge/plugin-sdk");
const result = validateSvgNodes(myPlugin.render(element));
if (!result.valid) {
console.error("SVG errors:", result.errors);
}
// result.sanitized contains the cleaned nodesvalidateManifest(manifest)
Validate a plugin manifest for required fields and format constraints.
const { validateManifest } = require("@graph-knowledge/plugin-sdk");
const result = validateManifest(manifest);
if (!result.valid) {
console.error("Manifest errors:", result.errors);
}Type Guards
isRuntimePlugin(value)— returnstrueif value passesvalidatePluginisPluginManifest(value)— returnstrueif value passesvalidateManifest
Bundle Constraints
| Constraint | Limit |
|------------|-------|
| Maximum bundle size | 500 KB |
| Format | Plain JavaScript (CommonJS exports.createPlugin = ...) |
| Dependencies | None allowed — everything must be self-contained |
| Frameworks | None — no React, Angular, etc. |
The bundle is loaded into a Web Worker. It must be a single .js file that assigns exports.createPlugin.
Complete Example
See the examples/database-cylinder/ directory for a full working plugin that renders a 3D cylinder shape for database diagrams. It includes:
plugin.js— the plugin source (~190 lines)
The database cylinder plugin demonstrates:
- Creating elements with custom properties (
fillColor,strokeColor,strokeWidth,label) - Rendering complex SVG with paths, ellipses, lines, and text
- Property editors for all four editor types (
text,color,number) - Immutable property updates
- Bounding box and hit-testing
Testing Locally
1. Upload via the UI
The simplest way: open Graph Knowledge, go to Store > Upload Plugin, and upload your .js file.
2. Upload via script
For automated setup, use the upload script pattern from the example:
# From the repo root
node examples/plugins/database-cylinder/upload.js --user <your-firebase-uid>This reads the plugin source, computes the hash, creates the Firestore documents (with inline bundleSource), and adds it to your inventory. Then run nx serve graph-knowledge to test.
Constants
The SDK exports these constants for reference:
const {
ALLOWED_SVG_TAGS, // Set of allowed SVG tag names
BLOCKED_SVG_ATTRIBUTES, // Set of blocked attribute names
MAX_SVG_NODE_COUNT, // 500
MAX_SVG_NODE_DEPTH // 10
} = require("@graph-knowledge/plugin-sdk");License
MIT
