Files
gh-jamshu-jamshi-marketplac…/commands/architecture.md
2025-11-29 18:50:06 +08:00

635 lines
15 KiB
Markdown

Detailed explanation of the Odoo PWA architecture, patterns, and design decisions.
## What this command does:
- Explains the architectural patterns used in generated PWAs
- Details the data flow and state management
- Describes the caching strategy
- Explains offline-first design principles
- Provides insights into technical decisions
---
## Architecture Overview 🏗️
The Odoo PWA Generator creates **offline-first Progressive Web Apps** with a **three-layer architecture** that ensures data availability, performance, and seamless Odoo integration.
```
┌─────────────────────────────────────────┐
│ UI Layer (Components) │
│ - Forms, Lists, Navigation │
│ - Framework-specific (Svelte/React/Vue)│
└──────────────┬──────────────────────────┘
┌─────────────────────────────────────────┐
│ State Layer (Cache Stores) │
│ - Smart Caching Logic │
│ - Dual Storage (localStorage + IndexedDB) │
│ - Background Sync │
│ - Optimistic Updates │
└──────────────┬──────────────────────────┘
┌─────────────────────────────────────────┐
│ API Layer (Odoo Client) │
│ - JSON-RPC Communication │
│ - CRUD Operations │
│ - Field Formatting │
└──────────────┬──────────────────────────┘
┌─────────────────────────────────────────┐
│ Server Layer (API Routes) │
│ - Credential Management │
│ - UID Caching │
│ - Error Handling │
└──────────────┬──────────────────────────┘
┌─────────────────────────────────────────┐
│ Odoo Backend │
│ - Studio Models │
│ - Business Logic │
│ - Data Persistence │
└─────────────────────────────────────────┘
```
---
## Layer 1: UI Components 🎨
### Purpose:
Present data to users and capture user input.
### Responsibilities:
- Render data from cache stores
- Handle user interactions
- Validate form inputs
- Display loading and error states
- Provide responsive, mobile-friendly interface
### Framework-Specific Implementation:
#### SvelteKit
```javascript
<script>
import { expenseCache } from '$lib/stores/expenseCache';
// Reactive to cache updates
$effect(() => {
expenseCache.load();
});
</script>
{#each $expenseCache as expense}
<ExpenseCard {expense} />
{/each}
```
#### React
```javascript
import { useExpense } from './contexts/ExpenseContext';
function ExpenseList() {
const { records, isLoading } = useExpense();
useEffect(() => {
// Load on mount
}, []);
return records.map(expense => (
<ExpenseCard key={expense.id} expense={expense} />
));
}
```
#### Vue
```javascript
<script setup>
import { useExpenseStore } from '@/stores/expenseStore';
const expenseStore = useExpenseStore();
expenseStore.load();
</script>
<template>
<ExpenseCard
v-for="expense in expenseStore.records"
:key="expense.id"
:expense="expense"
/>
</template>
```
### Design Principles:
- **Reactive by default** - UI updates automatically when data changes
- **Loading states** - Show skeleton loaders while data fetches
- **Error boundaries** - Graceful error handling
- **Optimistic UI** - Show changes immediately, sync in background
---
## Layer 2: Cache Stores (State Management) 💾
### Purpose:
Manage application state with offline-first caching.
### The Smart Caching Pattern:
```javascript
// Two-phase load strategy
export async function load() {
// Phase 1: Load from cache (instant)
const cached = await loadFromCache();
records.set(cached); // UI shows data immediately
// Phase 2: Check if stale and sync
if (isCacheStale()) {
await syncInBackground(); // Update in background
}
}
```
### Dual Storage Strategy:
#### localStorage (Metadata)
Stores lightweight metadata:
- `lastSyncTime` - When was last successful sync
- `lastRecordId` - Highest ID fetched so far
- `version` - Cache schema version
```javascript
localStorage.setItem('expenseCache', JSON.stringify({
lastSyncTime: Date.now(),
lastRecordId: 123,
version: 1
}));
```
#### IndexedDB (Master Data)
Stores actual records:
- All record data
- Larger storage capacity
- Async API
- Structured data
```javascript
await db.expenses.bulkPut(records);
```
### Stale Detection:
```javascript
function isCacheStale() {
const cacheData = JSON.parse(localStorage.getItem('expenseCache'));
const CACHE_VALIDITY = 5 * 60 * 1000; // 5 minutes
return !cacheData ||
(Date.now() - cacheData.lastSyncTime) > CACHE_VALIDITY;
}
```
### Incremental Sync Pattern:
```javascript
// Only fetch new records, not everything
async function syncInBackground() {
const { lastRecordId } = getCacheMetadata();
// Fetch only records with id > lastRecordId
const newRecords = await odoo.searchRecords(
'x_expense',
[['id', '>', lastRecordId]],
fields
);
if (newRecords.length > 0) {
await appendToCache(newRecords);
updateLastRecordId(newRecords[newRecords.length - 1].id);
}
}
```
### Optimistic Updates:
```javascript
export async function create(data) {
// 1. Generate temporary ID
const tempId = `temp-${Date.now()}`;
const tempRecord = { id: tempId, ...data };
// 2. Update UI immediately
records.update(r => [...r, tempRecord]);
// 3. Create in Odoo (background)
try {
const realId = await odoo.createRecord('x_expense', data);
// 4. Replace temp ID with real ID
records.update(r =>
r.map(rec => rec.id === tempId ? { ...rec, id: realId } : rec)
);
} catch (error) {
// 5. Rollback on error
records.update(r => r.filter(rec => rec.id !== tempId));
throw error;
}
}
```
### Background Sync Timer:
```javascript
let syncInterval;
export function startAutoSync() {
syncInterval = setInterval(() => {
syncInBackground();
}, 3 * 60 * 1000); // Every 3 minutes
}
export function stopAutoSync() {
clearInterval(syncInterval);
}
```
### Partner Resolution Pattern:
```javascript
// Many2one fields return [id, name] or just id
// Resolve partner names and cache them
async function resolvePartnerNames(records) {
const partnerIds = new Set();
// Collect all unique partner IDs
records.forEach(record => {
if (record.x_studio_employee) {
if (Array.isArray(record.x_studio_employee)) {
partnerIds.add(record.x_studio_employee[0]);
} else {
partnerIds.add(record.x_studio_employee);
}
}
});
// Fetch partner names in batch
const partners = await odoo.fetchPartners(Array.from(partnerIds));
const partnerMap = new Map(partners.map(p => [p.id, p.name]));
// Cache for future use
localStorage.setItem('partnerCache', JSON.stringify(
Array.from(partnerMap.entries())
));
return partnerMap;
}
```
---
## Layer 3: API Client (Odoo Communication) 🔌
### Purpose:
Abstract Odoo API communication with clean, reusable methods.
### JSON-RPC Communication:
```javascript
async function jsonRpc(url, params) {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'call',
params: params,
id: Math.random()
})
});
const data = await response.json();
if (data.error) {
throw new Error(data.error.message);
}
return data.result;
}
```
### CRUD Methods:
#### Create
```javascript
export async function createRecord(model, fields) {
return await fetch('/api/odoo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'create',
model,
fields
})
});
}
```
#### Read
```javascript
export async function searchRecords(model, domain, fields) {
return await fetch('/api/odoo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'search',
model,
domain,
fields
})
});
}
```
#### Update
```javascript
export async function updateRecord(model, id, values) {
return await fetch('/api/odoo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'update',
model,
id,
values
})
});
}
```
#### Delete
```javascript
export async function deleteRecord(model, id) {
return await fetch('/api/odoo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'delete',
model,
id
})
});
}
```
### Field Formatting Helpers:
```javascript
// Many2one: Convert to Odoo format [id, false]
export function formatMany2one(id) {
return id ? [id, false] : false;
}
// Many2many: Convert to Odoo command [(6, 0, [ids])]
export function formatMany2many(ids) {
return ids && ids.length > 0 ? [[6, 0, ids]] : [[6, 0, []]];
}
```
---
## Layer 4: Server Routes (API Proxy) 🔐
### Purpose:
Securely handle Odoo authentication and proxy requests.
### Why Server-Side?
1. **Security** - API keys never exposed to client
2. **CORS** - Bypass cross-origin restrictions
3. **Caching** - Cache UIDs to reduce auth calls
4. **Error Handling** - Centralized error management
### UID Caching Pattern:
```javascript
let cachedUid = null;
async function authenticate() {
if (cachedUid) {
return cachedUid;
}
// Authenticate with Odoo
cachedUid = await odooClient.authenticate(
db, username, apiKey
);
return cachedUid;
}
```
### Action Routing:
```javascript
export async function POST({ request }) {
const { action, model, ...params } = await request.json();
const uid = await authenticate();
switch (action) {
case 'create':
return odooClient.create(model, params.fields);
case 'search':
return odooClient.searchRead(model, params.domain, params.fields);
case 'update':
return odooClient.write(model, params.id, params.values);
case 'delete':
return odooClient.unlink(model, params.id);
default:
throw new Error(`Unknown action: ${action}`);
}
}
```
---
## Data Flow Examples 🔄
### Example 1: Loading Data on Page Load
```
1. User navigates to page
2. Component calls expenseCache.load()
3. Cache store loads from localStorage/IndexedDB
4. UI updates with cached data (instant)
5. Cache checks if data is stale (> 5 min)
6. If stale, triggers background sync
7. API client calls /api/odoo
8. Server authenticates and calls Odoo
9. New records returned
10. Cache updated (localStorage + IndexedDB)
11. UI reactively updates with new data
```
### Example 2: Creating a Record
```
1. User submits form
2. Component calls expenseCache.create(data)
3. Cache creates temp record with temp-ID
4. UI updates immediately (optimistic)
5. API client calls /api/odoo (background)
6. Server creates record in Odoo
7. Real ID returned
8. Cache replaces temp-ID with real ID
9. localStorage and IndexedDB updated
10. UI shows success message
```
### Example 3: Offline Create → Online Sync
```
1. User creates record (offline)
2. Record saved to cache with temp-ID
3. API call fails (no network)
4. Record marked as "pending sync"
5. UI shows "Will sync when online"
[User goes online]
6. Background sync detects pending records
7. Retries API call
8. Success! Real ID received
9. Cache updated
10. UI shows "Synced" status
```
---
## Key Design Patterns 🎯
### 1. Offline-First
- Always load from cache first
- Sync in background
- Queue operations when offline
- Retry on reconnection
### 2. Optimistic UI
- Update UI immediately
- Sync with server in background
- Rollback on error
- Show pending states
### 3. Incremental Sync
- Don't re-fetch all data
- Only fetch new records (id > lastRecordId)
- Reduces bandwidth
- Faster sync times
### 4. Dual Storage
- localStorage for metadata (fast)
- IndexedDB for data (large)
- Best of both worlds
### 5. Partner Resolution
- Batch fetch related records
- Cache partner names
- Avoid N+1 queries
- Display human-readable names
### 6. Reactive State
- Framework-native reactivity
- UI updates automatically
- No manual DOM manipulation
- Cleaner code
---
## Performance Considerations ⚡
### Initial Load:
- Cache-first: Instant data display
- Background sync: Fetch updates without blocking
- IndexedDB: Fast access to large datasets
### Network Usage:
- Incremental sync: Only new data
- Batch operations: Combine requests
- UID caching: Reduce auth calls
### Memory Usage:
- Store large data in IndexedDB, not memory
- Clean up old data periodically
- Lazy load related data
### Bundle Size:
- Framework-specific optimizations
- Tree shaking
- Code splitting
- Lazy load routes
---
## Security Patterns 🔒
### Credential Management:
- API keys in environment variables
- Server-side authentication
- Never expose keys to client
- Rotate keys periodically
### Data Validation:
- Validate on client (UX)
- Validate on server (security)
- Sanitize inputs
- Check permissions
### Error Handling:
- Don't expose internal errors to user
- Log errors securely
- Graceful degradation
- User-friendly messages
---
## Example prompts to use this command:
- `/architecture` - Show architecture details
- User: "How does the caching work?"
- User: "Explain the data flow"
- User: "What design patterns are used?"
## Next Steps:
After understanding the architecture:
1. Review generated code in your project
2. Read CLAUDE.md in your project for specific details
3. Customize patterns for your use case
4. Optimize for your specific needs
For more information, see `/help` or `/examples`!