Headless Document API for Graph Knowledge - programmatic access to documents, nodes, and elements
npm install @graph-knowledge/apiHeadless Document API for Graph Knowledge - provides programmatic access to documents, nodes, and elements without requiring the Angular UI.
``bash`
npm install @graph-knowledge/api firebase
`typescript`
import { GraphKnowledgeAPI } from "@graph-knowledge/api";
`typescript
import { GraphKnowledgeAPI } from "@graph-knowledge/api";
// Initialize with Graph Knowledge production config
const api = new GraphKnowledgeAPI({
firebaseConfig: {
apiKey: "AIzaSyDucPTxS82x-rnChqCnfVAlG-RcBK0sXEE",
authDomain: "knowledgegraph-72939.firebaseapp.com",
projectId: "knowledgegraph-72939",
storageBucket: "knowledgegraph-72939.firebasestorage.app",
messagingSenderId: "51304440744",
appId: "1:51304440744:web:7859f2b285bb33afd0339d"
}
});
// Sign in
await api.signIn("user@example.com", "password");
// Create a document
const doc = await api.documents.create({
title: "My Document",
content: "Description"
});
// Create a node in the document
const node = await api.nodes.create(doc.id, {
title: "My Node",
canvasWidth: 1920,
canvasHeight: 1080
});
// Add elements to the node
await api.elements.create(doc.id, node.id, {
type: "rectangle",
x: 100,
y: 100,
width: 200,
height: 150,
fillColor: "#FF5733",
strokeColor: "#000000"
});
await api.elements.create(doc.id, node.id, {
type: "text",
x: 150,
y: 160,
text: "Hello World",
fontSize: 24
});
// Sign out
await api.signOut();
`
Main entry point for the API.
#### Constructor
`typescript`
new GraphKnowledgeAPI(config: ApiConfig)
#### Methods
| Method | Description |
|--------|-------------|
| signIn(email, password) | Signs in with email and password |signOut()
| | Signs out the current user |waitForAuthInit()
| | Waits for Firebase Auth to initialize |measureText(text, options?)
| | Measures text dimensions (static method, no auth required) |fitTextToShape(text, bounds, options?)
| | Fits text into a shape's bounds (static method, no auth required) |
#### Properties
| Property | Type | Description |
|----------|------|-------------|
| currentUserId | string \| null | Current user's ID |documents
| | IDocumentOperations | Document CRUD operations |nodes
| | INodeOperations | Node CRUD operations |elements
| | IElementOperations | Element CRUD operations |batch
| | IBatchOperations | Batch element operations |templates
| | ITemplateOperations | Template operations (list, get, clone) |authClient
| | IAuthClient | Authentication client |
`typescript
// Create a document
const doc = await api.documents.create({
title: "My Document",
content: "Optional description",
canvasWidth: 1920, // Optional, default: 1920
canvasHeight: 1080 // Optional, default: 1080
});
// Get a document by ID
const doc = await api.documents.get(documentId);
// List all documents
const docs = await api.documents.list();
// Update a document
await api.documents.update(documentId, {
title: "New Title",
content: "New description"
});
// Delete a document
await api.documents.delete(documentId);
// Share a document
await api.documents.share(documentId, ["user-id-1", "user-id-2"]);
`
`typescript
// Create a node
const node = await api.nodes.create(documentId, {
title: "My Node",
content: "Optional description",
parentNodeId: "parent-node-id", // Optional
canvasWidth: 1920,
canvasHeight: 1080
});
// Get a node
const node = await api.nodes.get(documentId, nodeId);
// List all nodes in a document
const nodes = await api.nodes.list(documentId);
// Update a node
await api.nodes.update(documentId, nodeId, {
title: "New Title"
});
// Delete a node
await api.nodes.delete(documentId, nodeId);
`
`typescript
// Get an element by ID
const element = await api.elements.get(documentId, nodeId, elementId);
// List all elements in a node
const elements = await api.elements.list(documentId, nodeId);
// Create a rectangle
const rect = await api.elements.create(documentId, nodeId, {
type: "rectangle",
x: 100,
y: 100,
width: 200,
height: 150,
fillColor: "#FF5733",
strokeColor: "#000000",
strokeWidth: 2,
cornerRadius: 10
});
// Create text
// If you omit both width and maxWidth, the API will auto-measure the textwidth
// and use a tight single-line bounding box. This is convenient for simple labels.
// For predictable wrapping or alignment in more complex layouts, explicitly set
// or maxWidth. You can use measureText() to choose a width, or
// fitTextToShape() for text inside shapes.
await api.elements.create(documentId, nodeId, {
type: "text",
x: 100,
y: 100,
text: "Hello World",
fontSize: 24,
fontFamily: "Arial",
fillColor: "#000000"
});
// Create centered text within a container width
// When you specify a width larger than the text, textAlign controls
// positioning within that container (like CSS text-align)
await api.elements.create(documentId, nodeId, {
type: "text",
x: 100,
y: 150,
width: 400, // Container width
text: "Centered within 400px",
fontSize: 20,
textAlign: "center" // "left" | "center" | "right"
});
// Create multi-line text (use \n for line breaks)
await api.elements.create(documentId, nodeId, {
type: "text",
x: 100,
y: 200,
text: "Line 1\nLine 2\nLine 3",
fontSize: 18,
lineHeight: 1.4 // Optional: adjust line spacing (default: 1.2)
});
// Note: height is auto-calculated based on the number of lines and lineHeight
// Create text with auto-wrapping
// maxWidth sets the wrap boundary — text will wrap at word boundaries
await api.elements.create(documentId, nodeId, {
type: "text",
x: 100,
y: 300,
text: "This is a long paragraph that will automatically wrap within the specified width boundary.",
maxWidth: 400, // Text wraps at 400px
fontSize: 18
});
// Note: height is auto-calculated based on wrapped lines
// Create a connector
await api.elements.create(documentId, nodeId, {
type: "connector",
x: 0,
y: 0,
startElementId: "element-1",
endElementId: "element-2",
startAnchor: "right",
endAnchor: "left",
lineStyle: "solid",
endMarker: "arrow"
});
// Create a UML class
await api.elements.create(documentId, nodeId, {
type: "uml-class",
x: 100,
y: 100,
width: 200,
height: 150,
name: "MyClass",
attributes: "+ name: string\n- id: number",
methods: "+ getName(): string\n+ setName(name: string): void"
});
// Create a line
await api.elements.create(documentId, nodeId, {
type: "line",
x: 100,
y: 100,
width: 200,
height: 100,
strokeColor: "#000000",
strokeWidth: 2,
lineStyle: "solid" // "solid" | "dashed" | "dotted"
});
// Create a block arrow
await api.elements.create(documentId, nodeId, {
type: "block-arrow",
x: 100,
y: 100,
width: 150,
height: 80,
fillColor: "#4a9eff",
strokeColor: "#000000",
strokeWidth: 2,
direction: "right" // "right" | "left" | "up" | "down"
});
// Create basic shapes (triangle, diamond, hexagon, ellipse)
// All basic shapes share the same properties
await api.elements.create(documentId, nodeId, {
type: "triangle", // or "diamond", "hexagon", "ellipse"
x: 100,
y: 100,
width: 150,
height: 130,
fillColor: "#4CAF50",
strokeColor: "#2E7D32",
strokeWidth: 2
});
// Update an element
await api.elements.update(documentId, nodeId, elementId, {
x: 200,
y: 200,
properties: {
fillColor: "#00FF00"
}
});
// Delete an element
await api.elements.delete(documentId, nodeId, elementId);
`
Templates are pre-made documents that can be cloned by premium users:
`typescript
// List all available templates
const templates = await api.templates.list();
console.log(templates);
// [{ id: "template-1", title: "UML Class Diagram", isTemplate: true, ... }]
// Get a specific template with all its nodes and elements
const template = await api.templates.get("template-1");
// Clone a template (creates a new document) - requires premium
const myDoc = await api.templates.clone("template-1");
// Creates "Copy of UML Class Diagram" document
// Clone with custom title
const myDoc2 = await api.templates.clone("template-1", "My Project Diagram");
// Creates "My Project Diagram" document
// The cloned document:
// - Has a new unique ID
// - Is owned by the current user
// - Has isTemplate: false
// - Has clonedFromTemplateId pointing to the source template
// - Contains copies of all nodes and elements with new IDs
`
For efficient bulk operations (ideal for AI agents and automation):
`typescript
// Create multiple elements atomically
const elements = await api.batch.createElements(documentId, nodeId, [
{ type: "rectangle", x: 100, y: 100 },
{ type: "rectangle", x: 300, y: 100 },
{ type: "text", x: 200, y: 200, text: "Connected" }
]);
// Update multiple elements atomically
await api.batch.updateElements(documentId, nodeId, [
{ elementId: "elem-1", x: 150, y: 150 },
{ elementId: "elem-2", width: 300, height: 200 }
]);
// Delete multiple elements atomically
await api.batch.deleteElements(documentId, nodeId, [
"elem-1",
"elem-2",
"elem-3"
]);
`
| Type | Description |
|------|-------------|
| rectangle | Rectangle shape with fill, stroke, corner radius |triangle
| | Triangle shape with fill and stroke |diamond
| | Diamond/rhombus shape with fill and stroke |hexagon
| | Hexagon shape with fill and stroke |ellipse
| | Ellipse/oval shape with fill and stroke |text
| | Text element with font customization (supports multi-line with \n) |line
| | Freeform line with stroke styling |block-arrow
| | Block arrow shape with directional pointer |connector
| | Line connecting two elements |uml-class
| | UML class diagram element |uml-interface
| | UML interface element |uml-component
| | UML component element |uml-package
| | UML package element |uml-artifact
| | UML artifact element |uml-note
| | UML note element |custom:{shapeId}
| | Custom SVG shape (requires premium) |
Elements can act as links to other nodes:
`typescript`
// Create an element that links to another node
await api.elements.create(documentId, nodeId, {
type: "rectangle",
x: 100,
y: 100,
isLink: true,
linkTarget: "other-node-id" // Node ID to navigate to
});
Measure text dimensions before creating elements. Useful for calculating layouts and positioning:
`typescript
import { GraphKnowledgeAPI } from "@graph-knowledge/api";
// Static method - no API instance or authentication needed
const metrics = GraphKnowledgeAPI.measureText("Hello World", {
fontSize: 24,
fontFamily: "Arial",
fontWeight: 400
});
console.log(metrics);
// { width: 132.5, height: 24, lines: 1, anchorDx: 0, anchorDy: 0 }
// Measure multi-line text
const multiLine = GraphKnowledgeAPI.measureText("Line 1\nLine 2", {
fontSize: 16,
lineHeight: 1.4
});
console.log(multiLine.lines); // 2
// Measure with word wrapping
const wrapped = GraphKnowledgeAPI.measureText(
"This is a long sentence that will be wrapped",
{ fontSize: 16, maxWidth: 150 }
);
console.log(wrapped.lines); // > 1
console.log(wrapped.width); // <= 150
`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| fontSize | number | 16 | Font size in pixels |fontFamily
| | string | "Arial" | Font family |fontWeight
| | number | 400 | Font weight (100-900) |textAlign
| | "left" \| "center" \| "right" | "left" | Affects anchorDx |lineHeight
| | number | 1.2 | Line height multiplier |maxWidth
| | number | - | Max width for word wrapping |
| Property | Description |
|----------|-------------|
| width | Maximum line width in pixels |height
| | Total text height in pixels |lines
| | Number of lines |anchorDx
| | X offset for text alignment positioning |anchorDy
| | Y offset (0 for top baseline) |
For accurate text measurement in Node.js, install the optional canvas package:
`bash`
npm install canvas
Without it, the API uses character-based estimation (less accurate).
The canvas package requires native compilation - see node-canvas requirements.
When you create a text element via the API without width or maxWidth, the API auto-measures the text and sets the width to fit the content. This prevents the legacy 100px default that caused unexpected wrapping.
For predictable wrapping or text inside shapes, explicitly set width or maxWidth:
`typescript
// Auto-measured — convenient for simple labels
await api.elements.create(docId, nodeId, {
type: "text", x: 100, y: 100,
text: "My Long Title",
fontSize: 24
});
// Explicit width — for controlled wrapping
const m = GraphKnowledgeAPI.measureText("My Long Title", { fontSize: 24 });
await api.elements.create(docId, nodeId, {
type: "text", x: 100, y: 100, width: m.width,
text: "My Long Title",
fontSize: 24
});
// fitTextToShape — for text inside shapes (sets maxWidth automatically)
const fit = GraphKnowledgeAPI.fitTextToShape("My Long Title", shapeBounds);
await api.elements.create(docId, nodeId, { type: "text", ...fit.textInput });
`
Fit text into shapes with automatic wrapping, shrinking, or both. Returns a ready-to-use textInput for element creation.
`typescript
import { GraphKnowledgeAPI } from "@graph-knowledge/api";
// Fit text into a rectangle (default: wrap strategy)
const result = GraphKnowledgeAPI.fitTextToShape("Hello World", {
x: 100, y: 100, width: 200, height: 100
});
console.log(result.fits); // true if text fits within bounds
console.log(result.fontSize); // final font size used
// Create the text element directly
await api.elements.create(docId, nodeId, { type: "text", ...result.textInput });
`
| Strategy | Behavior |
|----------|----------|
| "wrap" (default) | Wraps text at word boundaries. Reports fits: false if height overflows. |"shrink"
| | Reduces font size (down to minFontSize) until text fits in one line. |"auto"
| | Tries wrapping first. If height overflows, shrinks font size with wrapping. |
`typescript
// Shrink: reduce font size to fit in one line
const shrunk = GraphKnowledgeAPI.fitTextToShape("A very long title", bounds, {
strategy: "shrink",
fontSize: 24,
minFontSize: 8
});
// Auto: wrap first, shrink if needed
const auto = GraphKnowledgeAPI.fitTextToShape("Long paragraph text...", bounds, {
strategy: "auto"
});
`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| fontSize | number | 16 | Starting font size in pixels |fontFamily
| | string | "Arial, sans-serif" | Font family |fontWeight
| | number | 400 | Font weight (100-900) |fillColor
| | string | "#000000" | Text fill color |textAlign
| | "left" \| "center" \| "right" | "center" | Text alignment |lineHeight
| | number | 1.2 | Line height multiplier |padding
| | number | 8 | Padding between shape edge and text |strategy
| | "wrap" \| "shrink" \| "auto" | "wrap" | Fitting strategy |minFontSize
| | number | 8 | Minimum font size for shrink/auto |
Combine shapes and fitted text for programmatic document creation:
`typescript
// Create a labeled rectangle
const rect = await api.elements.create(docId, nodeId, {
type: "rectangle",
x: 100, y: 100, width: 200, height: 80,
fillColor: "#E3F2FD"
});
const label = GraphKnowledgeAPI.fitTextToShape("User Service", {
x: 100, y: 100, width: 200, height: 80
}, { fontSize: 18, strategy: "auto" });
await api.elements.create(docId, nodeId, { type: "text", ...label.textInput });
`
The API throws typed errors for different failure cases:
`typescript
import {
GraphKnowledgeAPI,
AuthenticationError,
NotFoundError,
ValidationError,
PermissionError
} from "@graph-knowledge/api";
try {
await api.documents.get("non-existent");
} catch (error) {
if (error instanceof NotFoundError) {
console.log("Document not found");
} else if (error instanceof AuthenticationError) {
console.log("Not authenticated");
} else if (error instanceof ValidationError) {
console.log("Invalid input:", error.field);
} else if (error instanceof PermissionError) {
console.log("Permission denied (e.g., premium required)");
}
}
`
The library exports mock implementations for testing:
`typescript
import {
MockAuthClient,
MockFirestoreClient
} from "@graph-knowledge/api";
import { ElementOperations } from "@graph-knowledge/api";
import { ElementValidatorRegistry } from "@graph-knowledge/api";
describe("MyTest", () => {
let mockAuth: MockAuthClient;
let mockFirestore: MockFirestoreClient;
beforeEach(() => {
mockAuth = new MockAuthClient({ userId: "test-user" });
mockFirestore = new MockFirestoreClient();
});
it("should work with mocks", async () => {
// Seed test data
mockFirestore.seed("documents/doc-1/nodes/node-1", {
id: "node-1",
title: "Test Node",
elements: []
});
// Test your code...
});
});
`
The library follows SOLID principles:
- Single Responsibility: Each class has one job
- Open/Closed: Element validators use registry pattern for extensibility
- Liskov Substitution: All implementations can be swapped via interfaces
- Interface Segregation: Small, focused interfaces
- Dependency Inversion: Operations depend on interfaces, not implementations
``
GraphKnowledgeAPI (Composition Root)
├── FirebaseAuthClient : IAuthClient
├── FirebaseFirestoreClient : IFirestoreClient
├── DocumentOperations : IDocumentOperations
├── NodeOperations : INodeOperations
├── ElementOperations : IElementOperations
├── BatchOperations : IBatchOperations
├── TemplateOperations : ITemplateOperations
└── ElementValidatorRegistry : IElementValidatorRegistry
├── RectangleValidator
├── TextValidator
├── ConnectorValidator
└── UmlValidators...
`bashBuild
nx build api
MIT