Skip to main content

Transformer Package (@schwab/transformer)

Overview

The @schwab/transformer package provides comprehensive data transformation utilities for converting between different data formats, API responses, and component structures. It serves as the data processing layer that standardizes and transforms external API responses into internal component-ready formats.

Architecture

Core Transformers

1. Drupal Asset Transformer

Drupal Asset Transformation
// transformDrupalAsset.ts
import { TFetchedDrupalAsset } from '@schwab/schema/fetched/cmap-api/FetchedDrupalAssetSchema';
import { TDrupalAsset } from '@schwab/schema/ui/DrupalAsset/DrupalAssetSchema';
import { ERole } from '@schwab/schema/native-enums/ERole';

export function transformDrupalAsset(
fetchedAsset: TFetchedDrupalAsset,
role: ERole
): TDrupalAsset {

// Transform basic asset properties
const baseAsset = {
id: fetchedAsset.id,
type: fetchedAsset.type,
title: fetchedAsset.attributes.title,
body: transformRichText(fetchedAsset.attributes.body?.processed),
url: fetchedAsset.attributes.path?.alias || fetchedAsset.attributes.drupal_internal__nid,
publishedAt: new Date(fetchedAsset.attributes.created),
updatedAt: new Date(fetchedAsset.attributes.changed),
};

// Transform metadata
const metadata = {
metatags: transformMetatags(fetchedAsset.attributes.metatag),
analytics: transformAnalytics(fetchedAsset.attributes.field_analytics),
seo: {
title: fetchedAsset.attributes.metatag?.title || baseAsset.title,
description: fetchedAsset.attributes.metatag?.description,
keywords: fetchedAsset.attributes.field_keywords?.map(k => k.name),
},
};

// Transform relationships
const relationships = transformRelationships(
fetchedAsset.relationships,
fetchedAsset.included || []
);

// Transform components
const components = transformComponents(
fetchedAsset.attributes.field_components,
relationships,
role
);

return {
...baseAsset,
...metadata,
components,
relationships,
role,
};
}

function transformComponents(
componentData: any[],
relationships: any,
role: ERole
): any[] {
return componentData.map((component, index) => {
const transformedComponent = {
id: component.id,
type: component.type,
index: index + 1,
};

switch (component.type) {
case 'paragraph--marquee':
return transformMarqueeComponent(component, relationships);

case 'paragraph--rich_text':
return transformRichTextComponent(component, relationships);

case 'paragraph--image':
return transformImageComponent(component, relationships);

case 'paragraph--video':
return transformVideoComponent(component, relationships);

default:
return transformGenericComponent(component, relationships);
}
});
}

2. Component-Specific Transformers

Component Transformers
// Component transformation utilities
export function transformMarqueeComponent(
component: any,
relationships: any
): TMarqueeComponent {
return {
role: ERole.Marquee,
heading: component.field_heading?.value,
subheading: component.field_subheading?.value,
body: transformRichText(component.field_body?.processed),
image: transformImageReference(
component.field_image,
relationships
),
cta: transformCtaReference(
component.field_cta,
relationships
),
styles: {
layout: component.field_layout?.value || '50%/50%',
alignment: component.field_alignment?.value || 'left',
background: component.field_background?.value,
},
dataLayer: transformDataLayer(component.field_analytics),
};
}

export function transformImageComponent(
component: any,
relationships: any
): TImageComponent {
const imageData = resolveRelationship(
component.field_media_image,
relationships,
'media--image'
);

return {
role: ERole.Image,
src: imageData?.field_media_image?.uri?.url,
alt: imageData?.field_media_image?.alt || '',
title: imageData?.name || component.field_title?.value,
caption: component.field_caption?.value,
styles: {
aspectRatio: component.field_aspect_ratio?.value || '16_9',
objectFit: component.field_object_fit?.value || 'cover',
loading: component.field_loading?.value || 'lazy',
},
responsive: transformResponsiveImageData(imageData),
};
}

export function transformVideoComponent(
component: any,
relationships: any
): TVideoComponent {
return {
role: ERole.Video,
src: component.field_video_url?.uri,
poster: transformImageReference(component.field_poster, relationships),
title: component.field_title?.value,
description: component.field_description?.value,
duration: component.field_duration?.value,
transcript: component.field_transcript?.value,
captions: transformCaptionData(component.field_captions),
analytics: transformVideoAnalytics(component.field_analytics),
};
}

3. Data Normalization

Data Normalization Utilities
// Data normalization and standardization
export class DataNormalizer {

/**
* Normalize phone numbers to consistent format
*/
static normalizePhoneNumber(phone: string): string {
const cleaned = phone.replace(/\D/g, '');

if (cleaned.length === 10) {
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6)}`;
}

if (cleaned.length === 11 && cleaned.startsWith('1')) {
return `(${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7)}`;
}

return phone; // Return original if can't normalize
}

/**
* Normalize currency values
*/
static normalizeCurrency(amount: string | number): number {
if (typeof amount === 'number') return amount;

const cleaned = amount.replace(/[$,\s]/g, '');
return parseFloat(cleaned) || 0;
}

/**
* Normalize date formats
*/
static normalizeDate(dateInput: string | Date): Date {
if (dateInput instanceof Date) return dateInput;

// Handle various date formats
const date = new Date(dateInput);

if (isNaN(date.getTime())) {
throw new Error(`Invalid date format: ${dateInput}`);
}

return date;
}

/**
* Normalize text content
*/
static normalizeText(text: string): string {
return text
.trim()
.replace(/\s+/g, ' ') // Replace multiple spaces with single space
.replace(/\n\s*\n/g, '\n\n'); // Normalize paragraph breaks
}

/**
* Normalize URLs
*/
static normalizeUrl(url: string, baseUrl?: string): string {
try {
return new URL(url, baseUrl).toString();
} catch {
return url.startsWith('/') ? url : `/${url}`;
}
}
}

4. Rich Text Transformation

Rich Text Processing
// Rich text content transformation
export class RichTextTransformer {

/**
* Transform HTML content for component consumption
*/
static transformHtmlContent(html: string): {
content: string;
extractedLinks: Array<{ href: string; text: string; }>;
extractedImages: Array<{ src: string; alt: string; }>;
} {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');

// Extract and process links
const extractedLinks = Array.from(doc.querySelectorAll('a')).map(link => ({
href: link.href,
text: link.textContent || '',
target: link.target,
rel: link.rel,
}));

// Extract and process images
const extractedImages = Array.from(doc.querySelectorAll('img')).map(img => ({
src: img.src,
alt: img.alt,
width: img.width,
height: img.height,
}));

// Clean and normalize HTML
const cleanedHtml = this.sanitizeHtml(doc.body.innerHTML);

return {
content: cleanedHtml,
extractedLinks,
extractedImages,
};
}

/**
* Convert Drupal field formats to component props
*/
static transformFieldFormat(field: {
value: string;
format: string;
processed?: string;
}): string {
// Use processed version if available (Drupal text filters applied)
if (field.processed) {
return this.sanitizeHtml(field.processed);
}

// Otherwise process based on format
switch (field.format) {
case 'full_html':
return this.sanitizeHtml(field.value);

case 'basic_html':
return this.sanitizeBasicHtml(field.value);

case 'plain_text':
return this.escapeHtml(field.value);

default:
return field.value;
}
}

private static sanitizeHtml(html: string): string {
// Implement HTML sanitization
// Remove script tags, dangerous attributes, etc.
return html
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/on\w+="[^"]*"/g, '')
.replace(/javascript:/gi, '');
}

private static sanitizeBasicHtml(html: string): string {
// Allow only basic HTML tags
const allowedTags = ['p', 'br', 'strong', 'em', 'a', 'ul', 'ol', 'li'];
// Implementation would filter to allowed tags only
return html;
}

private static escapeHtml(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
}

Analytics Data Transformation

1. Analytics Event Transformation

Analytics Data Processing
// Analytics data transformation utilities
export class AnalyticsTransformer {

/**
* Transform component analytics data
*/
static transformComponentAnalytics(
componentData: any,
pageContext: any
): TAnalyticsData {
return {
component: {
type: componentData.type,
name: componentData.name || componentData.type,
position: componentData.position || 0,
variant: componentData.variant,
},
page: {
title: pageContext.title,
path: pageContext.path,
section: pageContext.section,
category: pageContext.category,
},
user: {
segment: pageContext.userSegment,
authenticated: pageContext.isAuthenticated,
},
interaction: {
type: 'view',
timestamp: new Date().toISOString(),
},
};
}

/**
* Transform form analytics
*/
static transformFormAnalytics(
formId: string,
fieldData: Record<string, any>,
action: 'start' | 'complete' | 'abandon'
): TFormAnalytics {
return {
form: {
id: formId,
name: formId.replace(/([A-Z])/g, ' $1').trim(),
action,
completionRate: this.calculateCompletionRate(fieldData),
},
fields: Object.keys(fieldData).map(fieldName => ({
name: fieldName,
filled: !!fieldData[fieldName],
errors: this.getFieldErrors(fieldName, fieldData[fieldName]),
})),
timing: {
startTime: new Date().toISOString(),
// Additional timing data would be tracked
},
};
}

private static calculateCompletionRate(fieldData: Record<string, any>): number {
const totalFields = Object.keys(fieldData).length;
const filledFields = Object.values(fieldData).filter(Boolean).length;
return totalFields > 0 ? (filledFields / totalFields) * 100 : 0;
}

private static getFieldErrors(fieldName: string, value: any): string[] {
// Implement field validation and return errors
const errors: string[] = [];

if (!value) {
errors.push('Field is required');
}

// Add more validation logic as needed
return errors;
}
}

2. Search Data Transformation

Search Index Transformation
// Search data transformation for indexing
export class SearchTransformer {

/**
* Transform content for search indexing
*/
static transformForSearch(content: any): TSearchDocument {
return {
id: content.id,
title: content.title,
body: this.extractSearchableText(content.body),
url: content.url,
type: content.type,
categories: content.categories || [],
tags: content.tags || [],
publishedAt: content.publishedAt,
updatedAt: content.updatedAt,
searchableContent: this.createSearchableContent(content),
boost: this.calculateSearchBoost(content),
};
}

private static extractSearchableText(html: string): string {
// Remove HTML tags and extract plain text
return html
.replace(/<[^>]*>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}

private static createSearchableContent(content: any): string {
const searchParts = [
content.title,
this.extractSearchableText(content.body),
content.categories?.join(' '),
content.tags?.join(' '),
].filter(Boolean);

return searchParts.join(' ').toLowerCase();
}

private static calculateSearchBoost(content: any): number {
let boost = 1.0;

// Boost based on content freshness
const daysSincePublished = (Date.now() - new Date(content.publishedAt).getTime()) / (1000 * 60 * 60 * 24);
if (daysSincePublished < 30) boost += 0.5;
if (daysSincePublished < 7) boost += 0.3;

// Boost based on content type
if (content.type === 'featured') boost += 0.8;
if (content.type === 'popular') boost += 0.5;

// Boost based on engagement metrics
if (content.viewCount > 1000) boost += 0.3;
if (content.shareCount > 100) boost += 0.2;

return Math.min(boost, 3.0); // Cap at 3x boost
}
}

Performance Optimization

1. Lazy Transformation

Performance-Optimized Transformers
// Lazy and cached transformation utilities
export class PerformanceTransformer {

private static transformCache = new Map<string, any>();

/**
* Cached transformation with TTL
*/
static cachedTransform<T>(
key: string,
transformer: () => T,
ttlMs: number = 5 * 60 * 1000 // 5 minutes
): T {
const cached = this.transformCache.get(key);

if (cached && Date.now() - cached.timestamp < ttlMs) {
return cached.data;
}

const result = transformer();
this.transformCache.set(key, {
data: result,
timestamp: Date.now(),
});

return result;
}

/**
* Batch transformation for large datasets
*/
static async batchTransform<T, U>(
items: T[],
transformer: (item: T) => U,
batchSize: number = 100
): Promise<U[]> {
const results: U[] = [];

for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = batch.map(transformer);
results.push(...batchResults);

// Allow other tasks to run between batches
await new Promise(resolve => setTimeout(resolve, 0));
}

return results;
}

/**
* Stream transformation for real-time data
*/
static createTransformStream<T, U>(
transformer: (item: T) => U
): TransformStream<T, U> {
return new TransformStream({
transform(chunk: T, controller) {
try {
const transformed = transformer(chunk);
controller.enqueue(transformed);
} catch (error) {
controller.error(error);
}
}
});
}
}

2. Memory-Efficient Processing

Memory-Efficient Transformers
// Memory-optimized transformation utilities
export class MemoryEfficientTransformer {

/**
* Process large datasets with memory management
*/
static async processLargeDataset<T, U>(
items: T[],
transformer: (item: T, index: number) => Promise<U>,
options: {
maxConcurrent?: number;
memoryThreshold?: number;
onProgress?: (processed: number, total: number) => void;
} = {}
): Promise<U[]> {
const {
maxConcurrent = 5,
memoryThreshold = 100 * 1024 * 1024, // 100MB
onProgress,
} = options;

const results: U[] = [];
const semaphore = new Semaphore(maxConcurrent);

for (let i = 0; i < items.length; i++) {
await semaphore.acquire();

try {
const result = await transformer(items[i], i);
results.push(result);

// Check memory usage
const memoryUsage = process.memoryUsage();
if (memoryUsage.heapUsed > memoryThreshold) {
// Force garbage collection if available
if (global.gc) {
global.gc();
}
}

onProgress?.(i + 1, items.length);

} finally {
semaphore.release();
}
}

return results;
}
}

class Semaphore {
private permits: number;
private waitQueue: Array<() => void> = [];

constructor(permits: number) {
this.permits = permits;
}

async acquire(): Promise<void> {
return new Promise((resolve) => {
if (this.permits > 0) {
this.permits--;
resolve();
} else {
this.waitQueue.push(resolve);
}
});
}

release(): void {
this.permits++;
const next = this.waitQueue.shift();
if (next) {
this.permits--;
next();
}
}
}

Error Handling and Validation

1. Transformation Validation

Validation and Error Handling
// Validation utilities for transformers
export class TransformationValidator {

/**
* Validate transformation result
*/
static validateTransformation<T>(
result: unknown,
schema: z.ZodSchema<T>,
context?: string
): T {
try {
return schema.parse(result);
} catch (error) {
if (error instanceof z.ZodError) {
throw new TransformationError(
`Transformation validation failed${context ? ` for ${context}` : ''}: ${error.message}`,
'VALIDATION_ERROR',
{ originalError: error, context }
);
}
throw error;
}
}

/**
* Safe transformation with fallback
*/
static safeTransform<T, U>(
input: T,
transformer: (input: T) => U,
fallback: U,
context?: string
): U {
try {
return transformer(input);
} catch (error) {
console.warn(`Transformation failed${context ? ` for ${context}` : ''}:`, error);
return fallback;
}
}
}

export class TransformationError extends Error {
constructor(
message: string,
public code: string,
public metadata?: Record<string, any>
) {
super(message);
this.name = 'TransformationError';
}
}

The Transformer package provides essential data processing capabilities that ensure consistent, validated, and optimized data transformation across the Charles Schwab monorepo, enabling seamless integration between external APIs and internal component systems.