You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

12 KiB

PlatformServiceMixin Best Practices Guide

Overview

This guide establishes best practices for using PlatformServiceMixin in TimeSafari components to ensure consistent, maintainable, and secure code.

Core Principles

1. Single Source of Truth

  • Always use PlatformServiceMixin for database operations
  • Never mix legacy patterns with mixin patterns in the same component
  • Use mixin caching to avoid redundant database queries

2. Component Context Awareness

  • Always include component name in error logging
  • Use this.$options.name for consistent component identification
  • Implement proper error boundaries with context

3. Progressive Enhancement

  • Start with basic mixin methods ($db, $exec, $one)
  • Use specialized methods when available ($getAllContacts, $settings)
  • Leverage caching for frequently accessed data

Implementation Patterns

Database Operations

Preferred Pattern: Use Specialized Methods

// Best: Use high-level specialized methods
const contacts = await this.$getAllContacts();
const settings = await this.$settings();
const userSettings = await this.$accountSettings(did);

Good Pattern: Use Mapped Query Methods

// Good: Use query methods with automatic mapping
const results = await this.$query<Contact>(
  "SELECT * FROM contacts WHERE registered = ?", 
  [true]
);

⚠️ Acceptable Pattern: Use Raw Database Methods

// Acceptable: Use raw methods when specialized methods don't exist
const result = await this.$db("SELECT COUNT(*) as count FROM logs");
const count = result?.values?.[0]?.[0] || 0;

Anti-Pattern: Direct Platform Service

// Anti-pattern: Avoid direct PlatformService usage
const platformService = PlatformServiceFactory.getInstance();
const result = await platformService.dbQuery(sql, params);

Settings Management

Best Practice: Use Mixin Methods

export default class MyComponent extends Vue {
  mixins: [PlatformServiceMixin],

  async loadSettings() {
    // ✅ Use cached settings retrieval
    const settings = await this.$settings();
    return settings;
  }

  async saveUserPreferences(changes: Partial<Settings>) {
    // ✅ Use specialized save method
    await this.$saveSettings(changes);
    await this.$log("User preferences saved");
  }

  async loadAccountSettings(did: string) {
    // ✅ Use account-specific settings
    const accountSettings = await this.$accountSettings(did);
    return accountSettings;
  }
}

Anti-Pattern: Legacy Settings Access

// Anti-pattern: Avoid legacy databaseUtil methods
import * as databaseUtil from "../db/databaseUtil";

async loadSettings() {
  const settings = await databaseUtil.retrieveSettingsForActiveAccount();
  return settings;
}

Error Handling

Best Practice: Component-Aware Error Handling

export default class MyComponent extends Vue {
  mixins: [PlatformServiceMixin],

  async performOperation() {
    try {
      const result = await this.$getAllContacts();
      await this.$log("Operation completed successfully");
      return result;
    } catch (error) {
      // ✅ Include component context in error logging
      await this.$logError(`[${this.$options.name}] Operation failed: ${error}`);
      
      // ✅ Provide user-friendly error handling
      this.$notify({
        group: "alert",
        type: "danger",
        title: "Operation Failed",
        text: "Unable to load contacts. Please try again.",
      });
      
      throw error; // Re-throw for upstream handling
    }
  }
}

Anti-Pattern: Generic Error Handling

// Anti-pattern: Generic error handling without context
try {
  // operation
} catch (error) {
  console.error("Error:", error);
  throw error;
}

Logging

Best Practice: Structured Logging

export default class MyComponent extends Vue {
  mixins: [PlatformServiceMixin],

  async performDatabaseOperation() {
    // ✅ Log operation start with context
    await this.$log(`[${this.$options.name}] Starting database operation`);

    try {
      const result = await this.$getAllContacts();
      
      // ✅ Log successful completion
      await this.$log(`[${this.$options.name}] Database operation completed, found ${result.length} contacts`);
      
      return result;
    } catch (error) {
      // ✅ Log errors with full context
      await this.$logError(`[${this.$options.name}] Database operation failed: ${error}`);
      throw error;
    }
  }

  // ✅ Use appropriate log levels
  async validateInput(input: string) {
    if (!input) {
      await this.$log(`[${this.$options.name}] Input validation failed: empty input`, 'warn');
      return false;
    }
    return true;
  }
}

Caching Strategies

Best Practice: Smart Caching Usage

export default class MyComponent extends Vue {
  mixins: [PlatformServiceMixin],

  async loadContactsWithCaching() {
    // ✅ Use cached contacts (automatically managed by mixin)
    const contacts = await this.$contacts();
    
    // ✅ Force refresh when needed
    if (this.needsFreshData) {
      const freshContacts = await this.$refreshContacts();
      return freshContacts;
    }
    
    return contacts;
  }

  async updateContactAndRefresh(did: string, changes: Partial<Contact>) {
    // ✅ Update contact and invalidate cache
    await this.$updateContact(did, changes);
    
    // ✅ Clear cache to ensure fresh data on next access
    this.$clearAllCaches();
    
    await this.$log(`[${this.$options.name}] Contact updated and cache cleared`);
  }
}

Security Best Practices

Input Validation

Always Validate Database Inputs

async saveContact(contact: Partial<Contact>) {
  // ✅ Validate required fields
  if (!contact.did || !contact.name) {
    await this.$logError(`[${this.$options.name}] Invalid contact data: missing required fields`);
    throw new Error('Contact must have DID and name');
  }

  // ✅ Sanitize inputs
  const sanitizedContact = {
    ...contact,
    name: contact.name.trim(),
    // Remove any potential XSS vectors
    notes: contact.notes?.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
  };

  return await this.$insertContact(sanitizedContact);
}

Error Information Disclosure

Safe Error Handling

async performSensitiveOperation(did: string) {
  try {
    // Sensitive operation
    const result = await this.$accountSettings(did);
    return result;
  } catch (error) {
    // ✅ Log full error for debugging
    await this.$logError(`[${this.$options.name}] Sensitive operation failed: ${error}`);
    
    // ✅ Return generic error to user
    throw new Error('Unable to complete operation. Please try again.');
  }
}

SQL Injection Prevention

Always Use Parameterized Queries

// ✅ Safe: Parameterized query
async findContactsByName(searchTerm: string) {
  return await this.$query<Contact>(
    "SELECT * FROM contacts WHERE name LIKE ?",
    [`%${searchTerm}%`]
  );
}

// ❌ Dangerous: String concatenation
async findContactsByNameUnsafe(searchTerm: string) {
  return await this.$query<Contact>(
    `SELECT * FROM contacts WHERE name LIKE '%${searchTerm}%'`
  );
}

Performance Optimization

Database Query Optimization

Efficient Query Patterns

export default class MyComponent extends Vue {
  mixins: [PlatformServiceMixin],

  async loadOptimizedData() {
    // ✅ Use transactions for multiple operations
    return await this.$withTransaction(async () => {
      const contacts = await this.$getAllContacts();
      const settings = await this.$settings();
      return { contacts, settings };
    });
  }

  async loadDataWithPagination(offset: number, limit: number) {
    // ✅ Use LIMIT and OFFSET for large datasets
    return await this.$query<Contact>(
      "SELECT * FROM contacts ORDER BY name LIMIT ? OFFSET ?",
      [limit, offset]
    );
  }
}

Memory Management

Proper Cache Management

export default class MyComponent extends Vue {
  mixins: [PlatformServiceMixin],

  beforeDestroy() {
    // ✅ Clear component caches on destroy
    this.$clearAllCaches();
  }

  async handleLargeDataset() {
    try {
      // Process large dataset
      const largeResult = await this.$query("SELECT * FROM large_table");
      
      // ✅ Process in chunks to avoid memory issues
      const chunkSize = 100;
      for (let i = 0; i < largeResult.length; i += chunkSize) {
        const chunk = largeResult.slice(i, i + chunkSize);
        await this.processChunk(chunk);
      }
    } finally {
      // ✅ Clear caches after processing large datasets
      this.$clearAllCaches();
    }
  }
}

Testing Strategies

Unit Testing

Mock Mixin Methods

// test/MyComponent.spec.ts
import { mount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';
import { PlatformServiceMixin } from '@/utils/PlatformServiceMixin';

describe('MyComponent', () => {
  let wrapper;
  
  beforeEach(() => {
    // ✅ Mock mixin methods
    const mockMixin = {
      ...PlatformServiceMixin,
      methods: {
        ...PlatformServiceMixin.methods,
        $getAllContacts: jest.fn().mockResolvedValue([]),
        $settings: jest.fn().mockResolvedValue({}),
        $log: jest.fn().mockResolvedValue(undefined),
        $logError: jest.fn().mockResolvedValue(undefined),
      }
    };
    
    wrapper = mount(MyComponent, {
      mixins: [mockMixin]
    });
  });
  
  it('should load contacts on mount', async () => {
    await wrapper.vm.loadContacts();
    expect(wrapper.vm.$getAllContacts).toHaveBeenCalled();
  });
});

Integration Testing

Test Real Database Operations

// test/integration/ContactsView.spec.ts
import { createLocalVue, mount } from '@vue/test-utils';
import ContactsView from '@/views/ContactsView.vue';
import { PlatformServiceMixin } from '@/utils/PlatformServiceMixin';

describe('ContactsView Integration', () => {
  it('should perform real database operations', async () => {
    const wrapper = mount(ContactsView, {
      mixins: [PlatformServiceMixin]
    });
    
    // ✅ Test real mixin functionality
    const contacts = await wrapper.vm.$getAllContacts();
    expect(Array.isArray(contacts)).toBe(true);
  });
});

Migration Checklist

When migrating components to PlatformServiceMixin:

Pre-Migration

  • Identify all database operations in the component
  • List all logging operations
  • Check for error handling patterns
  • Note any specialized database queries

During Migration

  • Add PlatformServiceMixin to mixins array
  • Replace all database operations with mixin methods
  • Update logging to use mixin logging methods
  • Add component context to error messages
  • Replace settings operations with mixin methods
  • Update error handling to use structured patterns

Post-Migration

  • Remove all legacy imports (databaseUtil, logConsoleAndDb)
  • Test all component functionality
  • Verify TypeScript compilation
  • Check for any remaining anti-patterns
  • Update component tests if needed
  • Run migration validation script

Troubleshooting Common Issues

Issue: TypeScript errors after migration

Solution: Ensure proper type definitions and mixin interface implementation

Issue: Methods not available on this

Solution: Verify PlatformServiceMixin is properly included in mixins array

Issue: Caching not working as expected

Solution: Check cache TTL settings and clear cache when needed

Issue: Database operations failing

Solution: Verify PlatformService is properly initialized and check error logs

Issue: Performance degradation

Solution: Review query efficiency and cache usage patterns

Version History

  • v1.0 - Initial best practices documentation
  • v1.1 - Added security and performance sections
  • v1.2 - Enhanced testing strategies and troubleshooting