npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@graph-knowledge/api

v0.4.0

Published

Headless Document API for Graph Knowledge - programmatic access to documents, nodes, and elements

Downloads

1,463

Readme

@graph-knowledge/api

Headless Document API for Graph Knowledge - provides programmatic access to documents, nodes, and elements without requiring the Angular UI.

Installation

npm (for external use)

npm install @graph-knowledge/api firebase

Monorepo (internal use)

import { GraphKnowledgeAPI } from "@graph-knowledge/api";

Quick Start

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("[email protected]", "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();

API Reference

GraphKnowledgeAPI

Main entry point for the API.

Constructor

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 |

Document Operations

// 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"]);

Node Operations

// 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);

Element Operations

// 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 text
// 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
// `width` 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);

Template Operations

Templates are pre-made documents that can be cloned by premium users:

// 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

Batch Operations

For efficient bulk operations (ideal for AI agents and automation):

// 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"
]);

Supported Element Types

| 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) |

Link Navigation

Elements can act as links to other nodes:

// 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
});

Text Measurement

Measure text dimensions before creating elements. Useful for calculating layouts and positioning:

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

MeasureTextOptions

| 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 |

TextMetrics Result

| 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) |

Node.js Support

For accurate text measurement in Node.js, install the optional canvas package:

npm install canvas

Without it, the API uses character-based estimation (less accurate). The canvas package requires native compilation - see node-canvas requirements.

Text Width Behavior

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:

// 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 });

Layout Helpers

Fit text into shapes with automatic wrapping, shrinking, or both. Returns a ready-to-use textInput for element creation.

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 });

Strategies

| 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. |

// 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"
});

FitTextOptions

| 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 |

Creating Documents with AI

Combine shapes and fitted text for programmatic document creation:

// 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 });

Error Handling

The API throws typed errors for different failure cases:

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)");
    }
}

Testing

The library exports mock implementations for testing:

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...
    });
});

Architecture

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...

Building

# Build
nx build api

# Test
nx test api

# Lint
nx lint api

# Publish to npm (after build)
nx publish api

License

MIT