Performance Standards

Performance Standards

Database Optimization

// Optimized database queries with TypeScript

interface Campaign {
  id: string;
  name: string;
  subject: string;
  userId: string;
  createdAt: Date;
  recipients?: EmailRecipient[];
  analytics?: CampaignAnalytics[];
  user?: User;
}

interface EmailRecipient {
  id: string;
  email: string;
  name?: string;
  campaignId: string;
}

interface CampaignAnalytics {
  id: string;
  campaignId: string;
  sentCount: number;
  deliveredCount: number;
  openedCount: number;
  clickedCount: number;
}

interface User {
  id: string;
  email: string;
  name: string;
}

interface CampaignQueryOptions {
  includeRecipients?: boolean;
  includeAnalytics?: boolean;
  includeUser?: boolean;
  limit?: number;
  offset?: number;
}

/**

 * Service with performance-optimized database operations
 */
class OptimizedCampaignService {
  constructor(private readonly db: DatabaseService) {}

  /**

   * Eager load related data to minimize N+1 queries
   */
  async getCampaignWithMetrics(
    campaignId: string,
    options: CampaignQueryOptions = {}
  ): Promise<Campaign | null> {
    const queryBuilder = this.db.createQueryBuilder()
      .select('campaign')
      .from(Campaign, 'campaign')
      .where('campaign.id = :campaignId', { campaignId });

    // Eager load recipients to avoid N+1 queries
    if (options.includeRecipients !== false) {
      queryBuilder.leftJoinAndSelect('campaign.recipients', 'recipients');
    }

    // Join with user table
    if (options.includeUser) {
      queryBuilder.leftJoinAndSelect('campaign.user', 'user');
    }

    // Load analytics in one query
    if (options.includeAnalytics) {
      queryBuilder.leftJoinAndSelect('campaign.analytics', 'analytics');
    }

    return queryBuilder.getOne();
  }

  /**

   * Efficiently load campaigns with pagination
   */
  async getCampaignsBatch(
    userId: string,
    options: CampaignQueryOptions = {}
  ): Promise<Campaign[]> {
    const { limit = 50, offset = 0, includeRecipients = true, includeAnalytics = true } = options;

    const queryBuilder = this.db.createQueryBuilder()
      .select('campaign')
      .from(Campaign, 'campaign')
      .where('campaign.userId = :userId', { userId })
      .orderBy('campaign.createdAt', 'DESC')
      .take(limit)
      .skip(offset);

    // Eager load related data
    if (includeRecipients) {
      queryBuilder.leftJoinAndSelect('campaign.recipients', 'recipients');
    }

    if (includeAnalytics) {
      queryBuilder.leftJoinAndSelect('campaign.analytics', 'analytics');
    }

    return queryBuilder.getMany();
  }

  /**

   * Use database-level search for better performance
   */
  async searchCampaigns(
    query: string,
    userId: string,
    options: CampaignQueryOptions = {}
  ): Promise<Campaign[]> {
    const queryBuilder = this.db.createQueryBuilder()
      .select('campaign')
      .from(Campaign, 'campaign')
      .where('campaign.userId = :userId', { userId })
      .andWhere(
        '(campaign.name ILIKE :query OR campaign.subject ILIKE :query)',
        { query: `%${query}%` }
      )
      .orderBy('campaign.createdAt', 'DESC');

    // Include recipients for search results
    if (options.includeRecipients !== false) {
      queryBuilder.leftJoinAndSelect('campaign.recipients', 'recipients');
    }

    return queryBuilder.getMany();
  }
}

/**

 * Enhanced query builder interface
 */
interface QueryBuilder<T> {
  select(alias: string): QueryBuilder<T>;
  from(entity: Function, alias: string): QueryBuilder<T>;
  where(condition: string, parameters?: Record<string, unknown>): QueryBuilder<T>;
  andWhere(condition: string, parameters?: Record<string, unknown>): QueryBuilder<T>;
  leftJoinAndSelect(propertyPath: string, alias: string): QueryBuilder<T>;
  orderBy(sort: string, order?: 'ASC' | 'DESC'): QueryBuilder<T>;
  take(limit: number): QueryBuilder<T>;
  skip(offset: number): QueryBuilder<T>;
  getOne(): Promise<T | null>;
  getMany(): Promise<T[]>;
}

interface DatabaseService {
  createQueryBuilder<T>(): QueryBuilder<T>;
}

Caching Strategy

// Efficient caching implementation with TypeScript

interface CacheValue {
  data: unknown;
  timestamp: number;
  ttl: number;
}

interface CacheOptions {
  ttl?: number; // Time to live in seconds
  tags?: string[];
}

interface CacheResult<T> {
  success: boolean;
  data?: T;
  error?: string;
}

/**

 * High-performance caching service
 */
class CacheService {
  private readonly defaultTTL = 3600; // 1 hour
  private readonly redisClient: RedisClient;

  constructor(redisClient: RedisClient) {
    this.redisClient = redisClient;
  }

  /**

   * Get value from cache with error handling
   */
  async get<T>(key: string): Promise<T | null> {
    try {
      const value = await this.redisClient.get(key);
      if (!value) return null;

      const parsed = JSON.parse(value) as CacheValue;

      // Check if cache entry has expired
      if (Date.now() - parsed.timestamp > parsed.ttl * 1000) {
        await this.redisClient.del(key);
        return null;
      }

      return parsed.data as T;
    } catch (error) {
      console.warn(`Cache get failed for key ${key}:`, error);
      return null;
    }
  }

  /**

   * Set value in cache with expiration
   */
  async set<T>(key: string, value: T, options: CacheOptions = {}): Promise<boolean> {
    const ttl = options.ttl || this.defaultTTL;

    try {
      const cacheValue: CacheValue = {
        data: value,
        timestamp: Date.now(),
        ttl
      };

      await this.redisClient.setex(key, ttl, JSON.stringify(cacheValue));
      return true;
    } catch (error) {
      console.warn(`Cache set failed for key ${key}:`, error);
      return false;
    }
  }

  /**

   * Invalidate multiple keys matching a pattern
   */
  async invalidatePattern(pattern: string): Promise<number> {
    try {
      const keys = await this.redisClient.keys(pattern);
      if (keys.length === 0) return 0;

      return await this.redisClient.del(...keys);
    } catch (error) {
      console.warn(`Cache invalidate failed for pattern ${pattern}:`, error);
      return 0;
    }
  }

  /**

   * Get multiple cache values in batch
   */
  async mget<T>(keys: string[]): Promise<(T | null)[]> {
    try {
      const values = await this.redisClient.mget(keys);
      return values.map(value => {
        if (!value) return null;

        try {
          const parsed = JSON.parse(value) as CacheValue;
          if (Date.now() - parsed.timestamp > parsed.ttl * 1000) {
            return null;
          }
          return parsed.data as T;
        } catch {
          return null;
        }
      });
    } catch (error) {
      console.warn(`Cache mget failed for keys ${keys.join(', ')}:`, error);
      return keys.map(() => null);
    }
  }
}

/**

 * Decorator for caching function results
 */
class CampaignCacheDecorator {
  constructor(
    private readonly cacheService: CacheService,
    private readonly defaultTTL: number = 300 // 5 minutes
  ) {}

  /**

   * Decorator for caching campaign data
   */
  cachedCampaign<T extends (...args: unknown[]) => Promise<unknown>>(
    ttl?: number
  ) {
    return (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => {
      const originalMethod = descriptor.value;

      descriptor.value = async function (...args: unknown[]) {
        const cacheKey = `campaign:${JSON.stringify(args)}`;

        // Try cache first
        const cached = await this.cacheService.get<T>(cacheKey);
        if (cached !== null) {
          return cached;
        }

        // Get from original method
        const result = await originalMethod.apply(this, args);

        // Cache result if not null/undefined
        if (result !== null && result !== undefined) {
          await this.cacheService.set(cacheKey, result, { ttl: ttl || this.defaultTTL });
        }

        return result;
      };

      return descriptor;
    };
  }
}

/**

 * Redis client interface
 */
interface RedisClient {
  get(key: string): Promise<string | null>;
  setex(key: string, ttl: number, value: string): Promise<void>;
  del(...keys: string[]): Promise<number>;
  mget(...keys: string[]): Promise<(string | null)[]>;
  keys(pattern: string): Promise<string[]>;
}