UNPKG

@zhanghongping/json-sage-workflow-cli

Version:

An intelligent JSON processing workflow system with improved error handling and configuration

260 lines (214 loc) 7.38 kB
# Creating Custom Nodes in JSON Sage Workflow ## Overview Custom nodes allow you to extend JSON Sage Workflow with your own processing logic. This guide provides a complete example of creating a custom node and explains each component in detail. ## Complete Example Here's a complete example of a custom node that processes JSON data: ```typescript import { BaseNode, WorkflowContext, NodeError } from 'json-sage-workflow'; interface DataTransformerConfig { fieldMapping: Record<string, string>; defaultValues?: Record<string, any>; validation?: { required?: string[]; types?: Record<string, string>; }; } class DataTransformerNode extends BaseNode { private config: DataTransformerConfig; constructor(config: DataTransformerConfig) { super(); this.config = this.validateConfig(config); } private validateConfig(config: DataTransformerConfig): DataTransformerConfig { if (!config.fieldMapping || typeof config.fieldMapping !== 'object') { throw new NodeError('Invalid configuration: fieldMapping is required and must be an object'); } return config; } async validate(context: WorkflowContext): Promise<boolean> { const input = context.getData('input'); // Validate input exists if (!input || typeof input !== 'object') { throw new NodeError('Invalid input: expected an object'); } // Validate required fields if (this.config.validation?.required) { for (const field of this.config.validation.required) { if (!(field in input)) { throw new NodeError(`Missing required field: ${field}`); } } } // Validate field types if (this.config.validation?.types) { for (const [field, expectedType] of Object.entries(this.config.validation.types)) { if (field in input && typeof input[field] !== expectedType) { throw new NodeError( `Invalid type for field ${field}: expected ${expectedType}, got ${typeof input[field]}` ); } } } return true; } async execute(context: WorkflowContext): Promise<void> { try { const input = context.getData('input'); const output: Record<string, any> = {}; // Apply field mapping for (const [targetField, sourceField] of Object.entries(this.config.fieldMapping)) { output[targetField] = input[sourceField]; } // Apply default values if (this.config.defaultValues) { for (const [field, defaultValue] of Object.entries(this.config.defaultValues)) { if (output[field] === undefined) { output[field] = defaultValue; } } } // Store the transformed data context.setData('output', output); // Log success context.log.info('Data transformation completed successfully'); } catch (error) { throw new NodeError('Error during data transformation', { cause: error }); } } async cleanup(context: WorkflowContext): Promise<void> { // Clean up any temporary data context.removeData('temp'); } async handleError(error: Error, context: WorkflowContext): Promise<void> { // Log the error context.log.error('Error in DataTransformerNode', error); // Store error details in context context.setData('error', { message: error.message, timestamp: new Date().toISOString(), nodeId: this.getId() }); // Attempt recovery if possible if (error instanceof NodeError) { await this.attemptRecovery(context); } else { throw error; // Rethrow unhandled errors } } private async attemptRecovery(context: WorkflowContext): Promise<void> { // Implement recovery logic const input = context.getData('input'); if (input) { try { // Attempt basic transformation without validation const output = {}; for (const [targetField, sourceField] of Object.entries(this.config.fieldMapping)) { if (input[sourceField] !== undefined) { output[targetField] = input[sourceField]; } } context.setData('output', output); context.log.info('Recovery successful'); } catch (error) { context.log.error('Recovery failed', error); throw error; } } } } // Usage Example const transformerNode = new DataTransformerNode({ fieldMapping: { 'firstName': 'name.first', 'lastName': 'name.last', 'email': 'contact.email' }, defaultValues: { 'status': 'active', 'role': 'user' }, validation: { required: ['name.first', 'contact.email'], types: { 'name.first': 'string', 'contact.email': 'string' } } }); // Using the node in a workflow const workflow = new JSONWorkflow() .addNode('transform', transformerNode) .addEdge('input', 'transform') .addEdge('transform', 'output'); // Execute the workflow const result = await workflow.execute({ input: { name: { first: 'John', last: 'Doe' }, contact: { email: 'john@example.com' } } }); ``` ## Method Explanations ### Constructor The constructor initializes the node with its configuration and validates it: - Takes configuration parameters - Validates configuration format - Stores configuration for use during execution ### validate() The validate method checks input data before processing: - Verifies input data exists and has correct format - Checks required fields are present - Validates field types - Returns true if validation passes, throws error if not ### execute() The execute method contains the main processing logic: - Retrieves input data from context - Performs data transformation - Handles errors during processing - Stores results in context - Logs operation status ### cleanup() The cleanup method handles resource management: - Removes temporary data - Closes connections if any - Frees up resources - Called after execution completes ### handleError() The error handler provides error recovery: - Logs error details - Stores error information in context - Attempts recovery if possible - Throws unrecoverable errors ## Best Practices 1. **Configuration Validation** - Always validate configuration in constructor - Use TypeScript interfaces for type safety - Provide clear error messages for invalid config 2. **Input Validation** - Validate all input data before processing - Check data types and required fields - Use custom validation rules when needed 3. **Error Handling** - Implement comprehensive error handling - Provide detailed error messages - Include recovery mechanisms - Log errors appropriately 4. **Resource Management** - Clean up resources in cleanup method - Handle memory usage carefully - Implement proper disposal patterns 5. **Testing** - Write unit tests for all methods - Test error scenarios - Test with various input types - Test recovery mechanisms 6. **Documentation** - Document configuration options - Provide usage examples - Explain error scenarios - Include performance considerations