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
Copy
Ask AI
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:
Copy
Ask AI
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:Copy
Ask AI
// 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:Copy
Ask AI
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:Copy
Ask AI
// 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:Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
}
}