@flowlab/all
Version:
A cool library focusing on handling various flows
121 lines (110 loc) • 6.67 kB
text/typescript
// 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';
}
}