CrystalFlow supports class inheritance, allowing you to create base node classes with shared functionality and decorators.Documentation Index
Fetch the complete documentation index at: https://crystalflow.dev/docs/llms.txt
Use this file to discover all available pages before exploring further.
Why Use Inheritance?
Code Reuse
Share common logic across multiple nodes
Consistency
Ensure consistent behavior and properties
Maintainability
Update common code in one place
Organization
Create logical node hierarchies
Basic Inheritance
import { Node, defineNode, Property, Input, Output } from '@crystalflow/core';
// Base class with common functionality
class BaseApiNode extends Node {
@Property({
type: 'string',
label: 'API Key',
required: true
})
apiKey: string = '';
@Property({
type: 'number',
label: 'Timeout (ms)',
defaultValue: 30000
})
timeout: number = 30000;
protected async makeRequest(url: string, options?: RequestInit) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(url, {
...options,
headers: {
'Authorization': `Bearer ${this.apiKey}`,
...options?.headers
},
signal: controller.signal
});
return await response.json();
} finally {
clearTimeout(timeoutId);
}
}
}
// Child class inherits properties and methods
@defineNode({
type: 'api.users',
label: 'Fetch Users',
category: 'API'
})
class FetchUsersNode extends BaseApiNode {
// Inherits apiKey and timeout properties
@Output({ type: 'any[]', label: 'Users' })
users: any[];
async execute() {
// Use inherited method
this.users = await this.makeRequest('https://api.example.com/users');
}
}
@defineNode({
type: 'api.posts',
label: 'Fetch Posts',
category: 'API'
})
class FetchPostsNode extends BaseApiNode {
// Also inherits apiKey and timeout properties
@Output({ type: 'any[]', label: 'Posts' })
posts: any[];
async execute() {
// Use inherited method
this.posts = await this.makeRequest('https://api.example.com/posts');
}
}
Property Inheritance
All properties defined with@Property are inherited:
class BaseNode extends Node {
@Property({ type: 'boolean', label: 'Enable Logging', defaultValue: false })
enableLogging: boolean = false;
@Property({ type: 'string', label: 'Log Level', defaultValue: 'info' })
logLevel: string = 'info';
protected log(message: string) {
if (this.enableLogging) {
console.log(`[${this.logLevel.toUpperCase()}] ${message}`);
}
}
}
@defineNode({ type: 'custom.processor', label: 'Processor', category: 'Custom' })
class ProcessorNode extends BaseNode {
// Inherits enableLogging and logLevel
@Input({ type: 'any', label: 'Data' })
data: any;
@Output({ type: 'any', label: 'Result' })
result: any;
execute() {
this.log('Processing data...'); // Use inherited method
this.result = this.processData(this.data);
this.log('Processing complete');
}
}
Multi-Level Inheritance
Create deep inheritance hierarchies:// Level 1: Base for all nodes
class BaseNode extends Node {
@Property({ type: 'boolean', label: 'Enable Logging', defaultValue: false })
enableLogging: boolean = false;
protected log(message: string) {
if (this.enableLogging) {
console.log(message);
}
}
}
// Level 2: Base for HTTP nodes
class BaseHttpNode extends BaseNode {
@Property({ type: 'number', label: 'Timeout', defaultValue: 30000 })
timeout: number = 30000;
@Property({ type: 'number', label: 'Retries', defaultValue: 3 })
retries: number = 3;
protected async fetchWithRetry(url: string, options?: RequestInit) {
let lastError: Error;
for (let i = 0; i <= this.retries; i++) {
try {
this.log(`Attempt ${i + 1}/${this.retries + 1}`);
return await this.makeRequest(url, options);
} catch (error) {
lastError = error;
if (i < this.retries) {
await this.delay(1000 * Math.pow(2, i)); // Exponential backoff
}
}
}
throw lastError;
}
private async makeRequest(url: string, options?: RequestInit) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
return await fetch(url, { ...options, signal: controller.signal });
} finally {
clearTimeout(timeoutId);
}
}
private delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Level 3: Base for REST API nodes
class BaseRestApiNode extends BaseHttpNode {
@Property({ type: 'string', label: 'Base URL', required: true })
baseUrl: string = '';
@Property({ type: 'string', label: 'API Key' })
apiKey: string = '';
protected async get(path: string) {
return this.fetchWithRetry(`${this.baseUrl}${path}`, {
method: 'GET',
headers: this.getHeaders()
});
}
protected async post(path: string, body: any) {
return this.fetchWithRetry(`${this.baseUrl}${path}`, {
method: 'POST',
headers: this.getHeaders(),
body: JSON.stringify(body)
});
}
private getHeaders() {
const headers: Record<string, string> = {
'Content-Type': 'application/json'
};
if (this.apiKey) {
headers['Authorization'] = `Bearer ${this.apiKey}`;
}
return headers;
}
}
// Level 4: Concrete implementation
@defineNode({
type: 'api.github.repos',
label: 'GitHub Repositories',
category: 'GitHub'
})
class GitHubReposNode extends BaseRestApiNode {
// Inherits: enableLogging, timeout, retries, baseUrl, apiKey
// Inherits methods: log, get, post, fetchWithRetry
@Input({ type: 'string', label: 'Username', required: true })
username: string = '';
@Output({ type: 'any[]', label: 'Repositories' })
repositories: any[];
async execute() {
this.log(`Fetching repos for ${this.username}`);
const response = await this.get(`/users/${this.username}/repos`);
this.repositories = await response.json();
this.log(`Found ${this.repositories.length} repositories`);
}
}
Abstract Methods Pattern
Use abstract patterns for enforceability:abstract class BaseProcessorNode extends Node {
@Property({ type: 'boolean', label: 'Enable Logging', defaultValue: false })
enableLogging: boolean = false;
@Input({ type: 'any', label: 'Input' })
input: any;
@Output({ type: 'any', label: 'Output' })
output: any;
execute() {
this.log('Starting processing...');
this.output = this.process(this.input);
this.log('Processing complete');
}
// Child classes must implement this
protected abstract process(input: any): any;
protected log(message: string) {
if (this.enableLogging) {
console.log(message);
}
}
}
@defineNode({ type: 'text.uppercase', label: 'Uppercase', category: 'Text' })
class UppercaseNode extends BaseProcessorNode {
protected process(input: any): any {
return String(input).toUpperCase();
}
}
@defineNode({ type: 'text.lowercase', label: 'Lowercase', category: 'Text' })
class LowercaseNode extends BaseProcessorNode {
protected process(input: any): any {
return String(input).toLowerCase();
}
}
Mixin Pattern
Combine multiple behaviors:// Mixin for caching behavior
function CacheableMixin<T extends new (...args: any[]) => Node>(Base: T) {
return class extends Base {
private cache = new Map<string, any>();
@Property({ type: 'boolean', label: 'Enable Cache', defaultValue: true })
enableCache: boolean = true;
protected getCached(key: string): any | undefined {
if (this.enableCache) {
return this.cache.get(key);
}
}
protected setCache(key: string, value: any) {
if (this.enableCache) {
this.cache.set(key, value);
}
}
protected clearCache() {
this.cache.clear();
}
};
}
// Mixin for retry behavior
function RetryableMixin<T extends new (...args: any[]) => Node>(Base: T) {
return class extends Base {
@Property({ type: 'number', label: 'Max Retries', defaultValue: 3 })
maxRetries: number = 3;
protected async retry<T>(fn: () => Promise<T>): Promise<T> {
let lastError: Error;
for (let i = 0; i <= this.maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (i < this.maxRetries) {
await this.delay(1000 * Math.pow(2, i));
}
}
}
throw lastError;
}
private delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
};
}
// Use multiple mixins
@defineNode({ type: 'api.data', label: 'Fetch Data', category: 'API' })
class FetchDataNode extends RetryableMixin(CacheableMixin(Node)) {
// Has both caching and retry capabilities
@Input({ type: 'string', label: 'URL' })
url: string = '';
@Output({ type: 'any', label: 'Data' })
data: any;
async execute() {
// Check cache first
const cached = this.getCached(this.url);
if (cached) {
this.data = cached;
return;
}
// Fetch with retry
const response = await this.retry(async () => {
const res = await fetch(this.url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
});
this.data = response;
this.setCache(this.url, response);
}
}
Override Parent Methods
Override methods while calling parent implementation:class BaseNode extends Node {
execute() {
console.log('Base execute');
this.doWork();
}
protected doWork() {
// Base implementation
}
}
@defineNode({ type: 'custom.child', label: 'Child', category: 'Custom' })
class ChildNode extends BaseNode {
execute() {
console.log('Child execute - before');
super.execute(); // Call parent
console.log('Child execute - after');
}
protected doWork() {
super.doWork(); // Call parent implementation
// Add child-specific work
}
}
Best Practices
Start with Base Classes
Start with Base Classes
Identify common patterns and create base classes early.
Keep Hierarchies Shallow
Keep Hierarchies Shallow
Avoid deep inheritance trees (3-4 levels max).
Use Protected Methods
Use Protected Methods
Make shared methods
protected to encapsulate implementation.Document Base Classes
Document Base Classes
Clearly document what child classes should override.
Consider Composition
Consider Composition
Sometimes composition (mixins) is better than inheritance.
Common Base Classes
API Client Base
class BaseApiClientNode extends Node {
@Property({ type: 'string', label: 'API Key', required: true })
apiKey: string = '';
@Property({ type: 'number', label: 'Timeout', defaultValue: 30000 })
timeout: number = 30000;
protected async request(url: string, options?: RequestInit) {
// Common API request logic
}
}
Data Processor Base
abstract class BaseDataProcessorNode extends Node {
@Input({ type: 'any[]', label: 'Data' })
data: any[] = [];
@Output({ type: 'any[]', label: 'Processed' })
processed: any[];
execute() {
this.processed = this.data.map(item => this.processItem(item));
}
protected abstract processItem(item: any): any;
}
File Operation Base
class BaseFileNode extends Node {
@Property({ type: 'string', label: 'Encoding', defaultValue: 'utf-8' })
encoding: string = 'utf-8';
protected readFile(path: string) {
// Common file reading logic
}
protected writeFile(path: string, content: string) {
// Common file writing logic
}
}
Next Steps
Property System
Understanding property inheritance
Creating Custom Nodes
Build nodes with inheritance
Node API
Complete Node class reference
Examples
See inheritance in action