UNPKG

@flowlab/all

Version:

A cool library focusing on handling various flows

121 lines (110 loc) 6.67 kB
// src/transformers/validationTransformer.ts import { z, ZodSchema, ZodError } from 'zod'; import { ITransformer, PipelineContext, PipelineStepConfig } from '../core/interfaces'; import { ComponentError, ConfigurationError } from '../core/errors'; // Define how schema is provided in config interface ValidationConfig extends PipelineStepConfig { transformer: 'validator'; // Registered name schema: object | string; // Can be inline Zod schema object (as JSON) or path to a .js/.ts file exporting a schema // What to do on validation failure: 'error', 'filter', 'dlq' (dlq needs pipeline context) onFailure?: 'error' | 'filter' | 'log'; // Default to 'filter'? 'error'? Let's default to 'error' } export class ValidationTransformer<TInput, TOutput = TInput> implements ITransformer<TInput, TOutput> { private schema: ZodSchema<TOutput> | null = null; private config: ValidationConfig; private schemaSource: object | string; private onFailure: 'error' | 'filter' | 'log'; constructor(config: ValidationConfig) { if (!config.schema) { throw new ConfigurationError(`ValidationTransformer requires 'schema' in step config.`); } this.config = config; this.schemaSource = config.schema; this.onFailure = config.onFailure || 'error'; // Default to throwing error } // Lazy load/compile the schema private async loadSchema(context: PipelineContext): Promise<ZodSchema<TOutput>> { if (this.schema) return this.schema; context.logger.debug({ schemaSource: this.schemaSource }, `Loading/Compiling Zod schema for ValidationTransformer.`); try { let schemaDefinition: any; if (typeof this.schemaSource === 'string') { // Load from file path (relative to CWD or absolute) // SECURITY: Be very careful loading code dynamically. Ensure paths are trusted. const schemaPath = path.resolve(process.cwd(), this.schemaSource); context.logger.info(`Loading Zod schema from file: ${schemaPath}`); const module = await import(schemaPath); // Assume schema is default export or named export 'schema' schemaDefinition = module.default || module.schema; if (!schemaDefinition) { throw new ConfigurationError(`No Zod schema found in export from ${schemaPath}`); } } else { // Assume inline object definition needs to be parsed by Zod // This is less common/useful - Zod schemas are usually defined in code. // A better approach might be to expect a registered schema by ID. // For simplicity, let's assume if it's an object, it *is* the Zod schema instance // This means config needs to pass the actual Zod object, which is hard via JSON/YAML. // Recommendation: Use file path method or register schemas. // Let's stick to file path or throw error for inline object for now. throw new ConfigurationError(`Inline object schema definitions are not directly supported. Please provide a file path.`); // schemaDefinition = this.schemaSource; // If we assumed it's already a Zod object } // Check if it's a Zod schema instance if (schemaDefinition instanceof ZodSchema) { this.schema = schemaDefinition as ZodSchema<TOutput>; context.logger.debug(`Zod schema loaded successfully.`); return this.schema; } else { throw new ConfigurationError(`Loaded schema source is not a valid Zod schema instance.`); } } catch (error: any) { context.logger.error({ err: error, schemaSource: this.schemaSource }, `Failed to load or compile Zod schema.`); if (error instanceof ConfigurationError || error instanceof ComponentError) throw error; throw new ComponentError('Failed to load Zod schema', 'ValidationTransformer', error); } } async transform(data: TInput, context: PipelineContext): Promise<TOutput> { const zodSchema = await this.loadSchema(context); try { // Parse (validates and returns typed output, potentially transforming) const validatedData = await zodSchema.parseAsync(data); return validatedData; } catch (error: any) { if (error instanceof ZodError) { context.logger.warn({ zodErrors: error.errors, item: data }, `Validation failed for item.`); switch (this.onFailure) { case 'filter': // Return null to signal filtering (pipeline run logic needs to handle null) // Note: ITransformer interface expects TOutput, not null. // This requires adjusting the pipeline logic or the interface. // Alternative: Throw specific error caught by pipeline's error handler throw new ValidationError('Item failed validation (strategy: filter)', error.errors, data); case 'log': // Logged above, just return original data (or null/undefined?) // Returning original data might violate TOutput type. Risky. // Let's throw specific error like 'filter'. throw new ValidationError('Item failed validation (strategy: log)', error.errors, data); case 'error': default: throw new ValidationError('Item failed validation (strategy: error)', error.errors, data); // Rethrow validation error } } else { // Unexpected error during parsing context.logger.error({ err: error, item: data }, `Unexpected error during validation.`); throw new ComponentError('Unexpected validation error', 'ValidationTransformer', error); } } } } // Custom error class for validation failures export class ValidationError extends ComponentError { public readonly validationErrors: z.ZodIssue[]; public readonly originalItem: any; constructor(message: string, errors: z.ZodIssue[], item: any) { super(message, 'ValidationTransformer'); this.validationErrors = errors; this.originalItem = item; // Include item for DLQ/logging this.name = 'ValidationError'; } }