forked from jsnbuchanan/crowd-funder-for-time-pwa
- Create logical sub-folder classification for all documentation - Organize 91 migration files into component-specific folders - Separate user guides, build system, migration, and development docs - Maintain maximum 7 items per folder for easy navigation - Add comprehensive README and reorganization summary - Ensure all changes tracked in git with proper versioning Structure: - user-guides/ (3 items): user-facing documentation - build-system/ (3 items): core, platforms, automation - migration/ (6 items): assessments, testing, templates - development/ (4 items): tools and standards - architecture/, testing/, examples/ (ready for future docs) Total: 24 folders created, all within 7-item limits
436 lines
12 KiB
Markdown
436 lines
12 KiB
Markdown
# 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**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
// Anti-pattern: Avoid direct PlatformService usage
|
|
const platformService = PlatformServiceFactory.getInstance();
|
|
const result = await platformService.dbQuery(sql, params);
|
|
```
|
|
|
|
### Settings Management
|
|
|
|
#### ✅ **Best Practice: Use Mixin Methods**
|
|
```typescript
|
|
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**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
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**
|
|
```typescript
|
|
// Anti-pattern: Generic error handling without context
|
|
try {
|
|
// operation
|
|
} catch (error) {
|
|
console.error("Error:", error);
|
|
throw error;
|
|
}
|
|
```
|
|
|
|
### Logging
|
|
|
|
#### ✅ **Best Practice: Structured Logging**
|
|
```typescript
|
|
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**
|
|
```typescript
|
|
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**
|
|
```typescript
|
|
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**
|
|
```typescript
|
|
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**
|
|
```typescript
|
|
// ✅ 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**
|
|
```typescript
|
|
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**
|
|
```typescript
|
|
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**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
// 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 |