Initial commit
This commit is contained in:
634
commands/architecture.md
Normal file
634
commands/architecture.md
Normal file
@@ -0,0 +1,634 @@
|
||||
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`!
|
||||
Reference in New Issue
Block a user