update
This commit is contained in:
@ -1,577 +1,138 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as einvoice from '../../../ts/index.js';
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { EInvoice } from '../../../ts/index.js';
|
||||
import { PerformanceTracker } from '../../helpers/performance.tracker.js';
|
||||
import { CorpusLoader } from '../../helpers/corpus.loader.js';
|
||||
|
||||
tap.test('ERR-09: Transformation Errors - Handle XSLT and data transformation failures', async (t) => {
|
||||
const performanceTracker = new PerformanceTracker('ERR-09');
|
||||
tap.test('ERR-09: Transformation Errors - should handle transformation errors', async () => {
|
||||
// ERR-09: Test error handling for transformation errors
|
||||
|
||||
await t.test('XSLT transformation errors', async () => {
|
||||
performanceTracker.startOperation('xslt-errors');
|
||||
|
||||
const xsltErrors = [
|
||||
{
|
||||
name: 'Invalid XSLT syntax',
|
||||
xslt: `<?xml version="1.0"?>
|
||||
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:template match="/">
|
||||
<xsl:value-of select="$undefined-variable"/>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>`,
|
||||
xml: '<invoice><id>TEST-001</id></invoice>',
|
||||
expectedError: /undefined.*variable|xslt.*error/i
|
||||
},
|
||||
{
|
||||
name: 'Circular reference',
|
||||
xslt: `<?xml version="1.0"?>
|
||||
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:template match="/" name="recursive">
|
||||
<xsl:call-template name="recursive"/>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>`,
|
||||
xml: '<invoice><id>TEST-001</id></invoice>',
|
||||
expectedError: /circular|recursive|stack overflow/i
|
||||
},
|
||||
{
|
||||
name: 'Missing required template',
|
||||
xslt: `<?xml version="1.0"?>
|
||||
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:template match="/">
|
||||
<xsl:apply-templates select="missing-element"/>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>`,
|
||||
xml: '<invoice><id>TEST-001</id></invoice>',
|
||||
expectedError: /no matching.*template|element not found/i
|
||||
}
|
||||
];
|
||||
|
||||
for (const test of xsltErrors) {
|
||||
const startTime = performance.now();
|
||||
// Test 1: Basic error handling
|
||||
console.log('\nTest 1: Basic transformation errors handling');
|
||||
const { result: basicResult, metric: basicMetric } = await PerformanceTracker.track(
|
||||
'err09-basic',
|
||||
async () => {
|
||||
let errorCaught = false;
|
||||
let errorMessage = '';
|
||||
|
||||
try {
|
||||
// Simulate XSLT transformation
|
||||
const transformationError = new Error(`XSLT Error: ${test.name}`);
|
||||
throw transformationError;
|
||||
// Simulate error scenario
|
||||
const einvoice = new EInvoice();
|
||||
|
||||
// Try to load invalid content based on test type
|
||||
// Invalid format transformation
|
||||
await einvoice.toXmlString('invalid-format' as any);
|
||||
|
||||
} catch (error) {
|
||||
expect(error).toBeTruthy();
|
||||
console.log(`✓ ${test.name}: ${error.message}`);
|
||||
errorCaught = true;
|
||||
errorMessage = error.message || 'Unknown error';
|
||||
console.log(` Error caught: ${errorMessage}`);
|
||||
}
|
||||
|
||||
performanceTracker.recordMetric('xslt-error', performance.now() - startTime);
|
||||
return {
|
||||
success: errorCaught,
|
||||
errorMessage,
|
||||
gracefulHandling: errorCaught && !errorMessage.includes('FATAL')
|
||||
};
|
||||
}
|
||||
|
||||
performanceTracker.endOperation('xslt-errors');
|
||||
});
|
||||
);
|
||||
|
||||
await t.test('Data mapping errors', async () => {
|
||||
performanceTracker.startOperation('mapping-errors');
|
||||
|
||||
class DataMapper {
|
||||
private mappingRules = new Map<string, (value: any) => any>();
|
||||
|
||||
addRule(sourcePath: string, transform: (value: any) => any): void {
|
||||
this.mappingRules.set(sourcePath, transform);
|
||||
}
|
||||
|
||||
async map(sourceData: any, targetSchema: any): Promise<any> {
|
||||
const errors: string[] = [];
|
||||
const result: any = {};
|
||||
|
||||
for (const [path, transform] of this.mappingRules) {
|
||||
try {
|
||||
const sourceValue = this.getValueByPath(sourceData, path);
|
||||
if (sourceValue === undefined) {
|
||||
errors.push(`Missing source field: ${path}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetValue = transform(sourceValue);
|
||||
this.setValueByPath(result, path, targetValue);
|
||||
} catch (error) {
|
||||
errors.push(`Mapping error for ${path}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Data mapping failed:\n${errors.join('\n')}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getValueByPath(obj: any, path: string): any {
|
||||
return path.split('.').reduce((curr, prop) => curr?.[prop], obj);
|
||||
}
|
||||
|
||||
private setValueByPath(obj: any, path: string, value: any): void {
|
||||
const parts = path.split('.');
|
||||
const last = parts.pop()!;
|
||||
const target = parts.reduce((curr, prop) => {
|
||||
if (!curr[prop]) curr[prop] = {};
|
||||
return curr[prop];
|
||||
}, obj);
|
||||
target[last] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const mapper = new DataMapper();
|
||||
|
||||
// Add mapping rules
|
||||
mapper.addRule('invoice.id', (v) => v.toUpperCase());
|
||||
mapper.addRule('invoice.date', (v) => {
|
||||
const date = new Date(v);
|
||||
if (isNaN(date.getTime())) {
|
||||
throw new Error('Invalid date format');
|
||||
}
|
||||
return date.toISOString();
|
||||
});
|
||||
mapper.addRule('invoice.amount', (v) => {
|
||||
const amount = parseFloat(v);
|
||||
if (isNaN(amount)) {
|
||||
throw new Error('Invalid amount');
|
||||
}
|
||||
return amount.toFixed(2);
|
||||
});
|
||||
|
||||
const testData = [
|
||||
{
|
||||
name: 'Valid data',
|
||||
source: { invoice: { id: 'test-001', date: '2024-01-01', amount: '100.50' } },
|
||||
shouldSucceed: true
|
||||
},
|
||||
{
|
||||
name: 'Missing required field',
|
||||
source: { invoice: { id: 'test-002', amount: '100' } },
|
||||
shouldSucceed: false
|
||||
},
|
||||
{
|
||||
name: 'Invalid data type',
|
||||
source: { invoice: { id: 'test-003', date: 'invalid-date', amount: '100' } },
|
||||
shouldSucceed: false
|
||||
},
|
||||
{
|
||||
name: 'Nested missing field',
|
||||
source: { wrongStructure: { id: 'test-004' } },
|
||||
shouldSucceed: false
|
||||
}
|
||||
];
|
||||
|
||||
for (const test of testData) {
|
||||
const startTime = performance.now();
|
||||
console.log(` Basic error handling completed in ${basicMetric.duration}ms`);
|
||||
console.log(` Error was caught: ${basicResult.success}`);
|
||||
console.log(` Graceful handling: ${basicResult.gracefulHandling}`);
|
||||
|
||||
// Test 2: Recovery mechanism
|
||||
console.log('\nTest 2: Recovery after error');
|
||||
const { result: recoveryResult, metric: recoveryMetric } = await PerformanceTracker.track(
|
||||
'err09-recovery',
|
||||
async () => {
|
||||
const einvoice = new EInvoice();
|
||||
|
||||
// First cause an error
|
||||
try {
|
||||
const result = await mapper.map(test.source, {});
|
||||
|
||||
if (test.shouldSucceed) {
|
||||
console.log(`✓ ${test.name}: Mapping successful`);
|
||||
} else {
|
||||
console.log(`✗ ${test.name}: Should have failed but succeeded`);
|
||||
}
|
||||
// Invalid format transformation
|
||||
await einvoice.toXmlString('invalid-format' as any);
|
||||
} catch (error) {
|
||||
if (!test.shouldSucceed) {
|
||||
console.log(`✓ ${test.name}: Correctly failed - ${error.message.split('\n')[0]}`);
|
||||
} else {
|
||||
console.log(`✗ ${test.name}: Unexpected failure - ${error.message}`);
|
||||
// Expected error
|
||||
}
|
||||
|
||||
// Now try normal operation
|
||||
einvoice.id = 'RECOVERY-TEST';
|
||||
einvoice.issueDate = new Date(2025, 0, 25);
|
||||
einvoice.invoiceId = 'RECOVERY-TEST';
|
||||
einvoice.accountingDocId = 'RECOVERY-TEST';
|
||||
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Test Company',
|
||||
description: 'Testing error recovery',
|
||||
address: {
|
||||
streetName: 'Test Street',
|
||||
houseNumber: '1',
|
||||
postalCode: '12345',
|
||||
city: 'Test City',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB 12345',
|
||||
registrationName: 'Commercial Register'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
performanceTracker.recordMetric('mapping-test', performance.now() - startTime);
|
||||
}
|
||||
|
||||
performanceTracker.endOperation('mapping-errors');
|
||||
});
|
||||
|
||||
await t.test('Schema transformation conflicts', async () => {
|
||||
performanceTracker.startOperation('schema-conflicts');
|
||||
|
||||
const schemaConflicts = [
|
||||
{
|
||||
name: 'Incompatible data types',
|
||||
source: { type: 'string', value: '123' },
|
||||
target: { type: 'number' },
|
||||
transform: (v: string) => parseInt(v),
|
||||
expectedIssue: 'Type coercion required'
|
||||
},
|
||||
{
|
||||
name: 'Missing mandatory field',
|
||||
source: { optional: 'value' },
|
||||
target: { required: ['mandatory'] },
|
||||
transform: (v: any) => v,
|
||||
expectedIssue: 'Required field missing'
|
||||
},
|
||||
{
|
||||
name: 'Enumeration mismatch',
|
||||
source: { status: 'ACTIVE' },
|
||||
target: { status: { enum: ['active', 'inactive'] } },
|
||||
transform: (v: string) => v.toLowerCase(),
|
||||
expectedIssue: 'Enum value transformation'
|
||||
},
|
||||
{
|
||||
name: 'Array to single value',
|
||||
source: { items: ['a', 'b', 'c'] },
|
||||
target: { item: 'string' },
|
||||
transform: (v: string[]) => v[0],
|
||||
expectedIssue: 'Data loss warning'
|
||||
}
|
||||
];
|
||||
|
||||
for (const conflict of schemaConflicts) {
|
||||
const startTime = performance.now();
|
||||
einvoice.to = {
|
||||
type: 'person',
|
||||
name: 'Test',
|
||||
surname: 'Customer',
|
||||
salutation: 'Mr' as const,
|
||||
sex: 'male' as const,
|
||||
title: 'Doctor' as const,
|
||||
description: 'Test customer',
|
||||
address: {
|
||||
streetName: 'Customer Street',
|
||||
houseNumber: '2',
|
||||
postalCode: '54321',
|
||||
city: 'Customer City',
|
||||
country: 'DE'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Test Product',
|
||||
articleNumber: 'TEST-001',
|
||||
unitType: 'EA',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
// Try to export after error
|
||||
let canRecover = false;
|
||||
try {
|
||||
const result = conflict.transform(conflict.source);
|
||||
console.log(`⚠️ ${conflict.name}: ${conflict.expectedIssue}`);
|
||||
console.log(` Transformed: ${JSON.stringify(conflict.source)} → ${JSON.stringify(result)}`);
|
||||
const xml = await einvoice.toXmlString('ubl');
|
||||
canRecover = xml.includes('RECOVERY-TEST');
|
||||
} catch (error) {
|
||||
console.log(`✗ ${conflict.name}: Transformation failed - ${error.message}`);
|
||||
canRecover = false;
|
||||
}
|
||||
|
||||
performanceTracker.recordMetric('schema-conflict', performance.now() - startTime);
|
||||
return { success: canRecover };
|
||||
}
|
||||
|
||||
performanceTracker.endOperation('schema-conflicts');
|
||||
});
|
||||
);
|
||||
|
||||
await t.test('XPath evaluation errors', async () => {
|
||||
performanceTracker.startOperation('xpath-errors');
|
||||
|
||||
class XPathEvaluator {
|
||||
evaluate(xpath: string, xml: string): any {
|
||||
// Simulate XPath evaluation errors
|
||||
const errors = {
|
||||
'//invalid[': 'Unclosed bracket in XPath expression',
|
||||
'//invoice/amount/text() + 1': 'Type error: Cannot perform arithmetic on node set',
|
||||
'//namespace:element': 'Undefined namespace prefix: namespace',
|
||||
'//invoice[position() = $var]': 'Undefined variable: var',
|
||||
'//invoice/substring(id)': 'Invalid function syntax'
|
||||
};
|
||||
|
||||
if (errors[xpath]) {
|
||||
throw new Error(errors[xpath]);
|
||||
}
|
||||
|
||||
// Simple valid paths
|
||||
if (xpath === '//invoice/id') {
|
||||
return 'TEST-001';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const evaluator = new XPathEvaluator();
|
||||
|
||||
const xpathTests = [
|
||||
{ path: '//invoice/id', shouldSucceed: true },
|
||||
{ path: '//invalid[', shouldSucceed: false },
|
||||
{ path: '//invoice/amount/text() + 1', shouldSucceed: false },
|
||||
{ path: '//namespace:element', shouldSucceed: false },
|
||||
{ path: '//invoice[position() = $var]', shouldSucceed: false },
|
||||
{ path: '//invoice/substring(id)', shouldSucceed: false }
|
||||
];
|
||||
|
||||
for (const test of xpathTests) {
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
const result = evaluator.evaluate(test.path, '<invoice><id>TEST-001</id></invoice>');
|
||||
|
||||
if (test.shouldSucceed) {
|
||||
console.log(`✓ XPath "${test.path}": Result = ${result}`);
|
||||
} else {
|
||||
console.log(`✗ XPath "${test.path}": Should have failed`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!test.shouldSucceed) {
|
||||
console.log(`✓ XPath "${test.path}": ${error.message}`);
|
||||
} else {
|
||||
console.log(`✗ XPath "${test.path}": Unexpected error - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
performanceTracker.recordMetric('xpath-evaluation', performance.now() - startTime);
|
||||
}
|
||||
|
||||
performanceTracker.endOperation('xpath-errors');
|
||||
});
|
||||
console.log(` Recovery test completed in ${recoveryMetric.duration}ms`);
|
||||
console.log(` Can recover after error: ${recoveryResult.success}`);
|
||||
|
||||
await t.test('Format conversion pipeline errors', async () => {
|
||||
performanceTracker.startOperation('pipeline-errors');
|
||||
|
||||
class ConversionPipeline {
|
||||
private steps: Array<{ name: string; transform: (data: any) => any }> = [];
|
||||
|
||||
addStep(name: string, transform: (data: any) => any): void {
|
||||
this.steps.push({ name, transform });
|
||||
}
|
||||
|
||||
async execute(input: any): Promise<any> {
|
||||
let current = input;
|
||||
const executionLog: string[] = [];
|
||||
|
||||
for (const step of this.steps) {
|
||||
try {
|
||||
executionLog.push(`Executing: ${step.name}`);
|
||||
current = await step.transform(current);
|
||||
executionLog.push(`✓ ${step.name} completed`);
|
||||
} catch (error) {
|
||||
executionLog.push(`✗ ${step.name} failed: ${error.message}`);
|
||||
throw new Error(
|
||||
`Pipeline failed at step "${step.name}": ${error.message}\n` +
|
||||
`Execution log:\n${executionLog.join('\n')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
const pipeline = new ConversionPipeline();
|
||||
|
||||
// Add pipeline steps
|
||||
pipeline.addStep('Validate Input', (data) => {
|
||||
if (!data.invoice) {
|
||||
throw new Error('Missing invoice element');
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
pipeline.addStep('Normalize Dates', (data) => {
|
||||
if (data.invoice.date) {
|
||||
data.invoice.date = new Date(data.invoice.date).toISOString();
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
pipeline.addStep('Convert Currency', (data) => {
|
||||
if (data.invoice.amount && data.invoice.currency !== 'EUR') {
|
||||
throw new Error('Currency conversion not implemented');
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
pipeline.addStep('Apply Business Rules', (data) => {
|
||||
if (data.invoice.amount < 0) {
|
||||
throw new Error('Negative amounts not allowed');
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
name: 'Valid pipeline execution',
|
||||
input: { invoice: { id: 'TEST-001', date: '2024-01-01', amount: 100, currency: 'EUR' } },
|
||||
shouldSucceed: true
|
||||
},
|
||||
{
|
||||
name: 'Missing invoice element',
|
||||
input: { order: { id: 'ORDER-001' } },
|
||||
shouldSucceed: false,
|
||||
failureStep: 'Validate Input'
|
||||
},
|
||||
{
|
||||
name: 'Unsupported currency',
|
||||
input: { invoice: { id: 'TEST-002', amount: 100, currency: 'USD' } },
|
||||
shouldSucceed: false,
|
||||
failureStep: 'Convert Currency'
|
||||
},
|
||||
{
|
||||
name: 'Business rule violation',
|
||||
input: { invoice: { id: 'TEST-003', amount: -50, currency: 'EUR' } },
|
||||
shouldSucceed: false,
|
||||
failureStep: 'Apply Business Rules'
|
||||
}
|
||||
];
|
||||
|
||||
for (const test of testCases) {
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
const result = await pipeline.execute(test.input);
|
||||
|
||||
if (test.shouldSucceed) {
|
||||
console.log(`✓ ${test.name}: Pipeline completed successfully`);
|
||||
} else {
|
||||
console.log(`✗ ${test.name}: Should have failed at ${test.failureStep}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!test.shouldSucceed) {
|
||||
const failedStep = error.message.match(/step "([^"]+)"/)?.[1];
|
||||
if (failedStep === test.failureStep) {
|
||||
console.log(`✓ ${test.name}: Failed at expected step (${failedStep})`);
|
||||
} else {
|
||||
console.log(`✗ ${test.name}: Failed at wrong step (expected ${test.failureStep}, got ${failedStep})`);
|
||||
}
|
||||
} else {
|
||||
console.log(`✗ ${test.name}: Unexpected failure`);
|
||||
}
|
||||
}
|
||||
|
||||
performanceTracker.recordMetric('pipeline-execution', performance.now() - startTime);
|
||||
}
|
||||
|
||||
performanceTracker.endOperation('pipeline-errors');
|
||||
});
|
||||
// Summary
|
||||
console.log('\n=== Transformation Errors Error Handling Summary ===');
|
||||
console.log(`Error Detection: ${basicResult.success ? 'Working' : 'Failed'}`);
|
||||
console.log(`Graceful Handling: ${basicResult.gracefulHandling ? 'Yes' : 'No'}`);
|
||||
console.log(`Recovery: ${recoveryResult.success ? 'Successful' : 'Failed'}`);
|
||||
|
||||
await t.test('Corpus transformation analysis', async () => {
|
||||
performanceTracker.startOperation('corpus-transformation');
|
||||
|
||||
const corpusLoader = new CorpusLoader();
|
||||
const xmlFiles = await corpusLoader.getFiles(/\.xml$/);
|
||||
|
||||
console.log(`\nAnalyzing transformation scenarios with ${xmlFiles.length} files...`);
|
||||
|
||||
const transformationStats = {
|
||||
total: 0,
|
||||
ublToCii: 0,
|
||||
ciiToUbl: 0,
|
||||
zugferdToXrechnung: 0,
|
||||
errors: 0,
|
||||
unsupported: 0
|
||||
};
|
||||
|
||||
const sampleSize = Math.min(20, xmlFiles.length);
|
||||
const sampledFiles = xmlFiles.slice(0, sampleSize);
|
||||
|
||||
for (const file of sampledFiles) {
|
||||
transformationStats.total++;
|
||||
|
||||
try {
|
||||
// Detect source format
|
||||
if (file.path.includes('UBL') || file.path.includes('.ubl.')) {
|
||||
transformationStats.ublToCii++;
|
||||
} else if (file.path.includes('CII') || file.path.includes('.cii.')) {
|
||||
transformationStats.ciiToUbl++;
|
||||
} else if (file.path.includes('ZUGFeRD') || file.path.includes('XRECHNUNG')) {
|
||||
transformationStats.zugferdToXrechnung++;
|
||||
} else {
|
||||
transformationStats.unsupported++;
|
||||
}
|
||||
} catch (error) {
|
||||
transformationStats.errors++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\nTransformation Scenarios:');
|
||||
console.log(`Total files analyzed: ${transformationStats.total}`);
|
||||
console.log(`UBL → CII candidates: ${transformationStats.ublToCii}`);
|
||||
console.log(`CII → UBL candidates: ${transformationStats.ciiToUbl}`);
|
||||
console.log(`ZUGFeRD → XRechnung candidates: ${transformationStats.zugferdToXrechnung}`);
|
||||
console.log(`Unsupported formats: ${transformationStats.unsupported}`);
|
||||
console.log(`Analysis errors: ${transformationStats.errors}`);
|
||||
|
||||
performanceTracker.endOperation('corpus-transformation');
|
||||
});
|
||||
|
||||
await t.test('Transformation rollback mechanisms', async () => {
|
||||
performanceTracker.startOperation('rollback');
|
||||
|
||||
class TransformationContext {
|
||||
private snapshots: Array<{ stage: string; data: any }> = [];
|
||||
private currentData: any;
|
||||
|
||||
constructor(initialData: any) {
|
||||
this.currentData = JSON.parse(JSON.stringify(initialData));
|
||||
this.snapshots.push({ stage: 'initial', data: this.currentData });
|
||||
}
|
||||
|
||||
async transform(stage: string, transformer: (data: any) => any): Promise<void> {
|
||||
try {
|
||||
const transformed = await transformer(this.currentData);
|
||||
this.currentData = transformed;
|
||||
this.snapshots.push({
|
||||
stage,
|
||||
data: JSON.parse(JSON.stringify(transformed))
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Transformation failed at stage "${stage}": ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
rollbackTo(stage: string): void {
|
||||
const snapshot = this.snapshots.find(s => s.stage === stage);
|
||||
if (!snapshot) {
|
||||
throw new Error(`No snapshot found for stage: ${stage}`);
|
||||
}
|
||||
|
||||
this.currentData = JSON.parse(JSON.stringify(snapshot.data));
|
||||
|
||||
// Remove all snapshots after this stage
|
||||
const index = this.snapshots.indexOf(snapshot);
|
||||
this.snapshots = this.snapshots.slice(0, index + 1);
|
||||
}
|
||||
|
||||
getData(): any {
|
||||
return this.currentData;
|
||||
}
|
||||
|
||||
getHistory(): string[] {
|
||||
return this.snapshots.map(s => s.stage);
|
||||
}
|
||||
}
|
||||
|
||||
const initialData = {
|
||||
invoice: {
|
||||
id: 'TEST-001',
|
||||
amount: 100,
|
||||
items: ['item1', 'item2']
|
||||
}
|
||||
};
|
||||
|
||||
const context = new TransformationContext(initialData);
|
||||
|
||||
try {
|
||||
// Successful transformations
|
||||
await context.transform('add-date', (data) => {
|
||||
data.invoice.date = '2024-01-01';
|
||||
return data;
|
||||
});
|
||||
|
||||
await context.transform('calculate-tax', (data) => {
|
||||
data.invoice.tax = data.invoice.amount * 0.19;
|
||||
return data;
|
||||
});
|
||||
|
||||
console.log('✓ Transformations applied:', context.getHistory());
|
||||
|
||||
// Failed transformation
|
||||
await context.transform('invalid-operation', (data) => {
|
||||
throw new Error('Invalid operation');
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log(`✓ Error caught: ${error.message}`);
|
||||
|
||||
// Rollback to last successful state
|
||||
context.rollbackTo('calculate-tax');
|
||||
console.log('✓ Rolled back to:', context.getHistory());
|
||||
|
||||
// Try rollback to initial state
|
||||
context.rollbackTo('initial');
|
||||
console.log('✓ Rolled back to initial state');
|
||||
|
||||
const finalData = context.getData();
|
||||
expect(JSON.stringify(finalData)).toEqual(JSON.stringify(initialData));
|
||||
}
|
||||
|
||||
performanceTracker.endOperation('rollback');
|
||||
});
|
||||
|
||||
// Performance summary
|
||||
console.log('\n' + performanceTracker.getSummary());
|
||||
|
||||
// Transformation error handling best practices
|
||||
console.log('\nTransformation Error Handling Best Practices:');
|
||||
console.log('1. Validate transformation rules before execution');
|
||||
console.log('2. Implement checkpoints for complex transformation pipelines');
|
||||
console.log('3. Provide detailed error context including failed step and data state');
|
||||
console.log('4. Support rollback mechanisms for failed transformations');
|
||||
console.log('5. Log all transformation steps for debugging');
|
||||
console.log('6. Handle type mismatches and data loss gracefully');
|
||||
console.log('7. Validate output against target schema');
|
||||
console.log('8. Implement transformation preview/dry-run capability');
|
||||
// Test passes if errors are caught gracefully
|
||||
expect(basicResult.success).toBeTrue();
|
||||
expect(recoveryResult.success).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
||||
// Run the test
|
||||
tap.start();
|
||||
|
Reference in New Issue
Block a user