594 lines
14 KiB
Markdown
594 lines
14 KiB
Markdown
|
|
# 📜 Message Search, History Pagination, and Offline Sync
|
||
|
|
|
||
|
|
**Date**: 2025-12-05
|
||
|
|
**Version**: 1.0.0
|
||
|
|
**Statut**: ✅ Implémenté
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📋 Table des matières
|
||
|
|
|
||
|
|
1. [Vue d'ensemble](#vue-densemble)
|
||
|
|
2. [History Pagination](#history-pagination)
|
||
|
|
3. [Message Search](#message-search)
|
||
|
|
4. [Offline Sync](#offline-sync)
|
||
|
|
5. [Spécifications techniques](#spécifications-techniques)
|
||
|
|
6. [Exemples d'utilisation](#exemples-dutilisation)
|
||
|
|
7. [Limites et bonnes pratiques](#limites-et-bonnes-pratiques)
|
||
|
|
8. [Impact sur l'UI](#impact-sur-lui)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🎯 Vue d'ensemble
|
||
|
|
|
||
|
|
Ce document décrit trois fonctionnalités majeures ajoutées au `veza-chat-server` :
|
||
|
|
|
||
|
|
1. **History Pagination** : Pagination efficace de l'historique avec cursors `before`/`after`
|
||
|
|
2. **Message Search** : Recherche textuelle de messages dans une conversation
|
||
|
|
3. **Offline Sync** : Synchronisation des messages manquants depuis la dernière connexion
|
||
|
|
|
||
|
|
Toutes ces fonctionnalités sont :
|
||
|
|
- ✅ Sécurisées (permissions strictes via `PermissionService`)
|
||
|
|
- ✅ Performantes (index SQL optimisés)
|
||
|
|
- ✅ Compatibles avec les statuts (edited, deleted, delivered, read)
|
||
|
|
- ✅ Disponibles via WebSocket
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📜 History Pagination
|
||
|
|
|
||
|
|
### Description
|
||
|
|
|
||
|
|
Permet de récupérer l'historique d'une conversation avec pagination par cursors basés sur `created_at`. Plus efficace que l'offset/limit classique car :
|
||
|
|
- Supporte les insertions concurrentes
|
||
|
|
- Meilleure performance avec les index
|
||
|
|
- Pas de problèmes de doublons lors de nouvelles insertions
|
||
|
|
|
||
|
|
### Inbound WebSocket Message
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"type": "FetchHistory",
|
||
|
|
"conversation_id": "550e8400-e29b-41d4-a716-446655440000",
|
||
|
|
"before": "2025-12-05T10:30:00Z",
|
||
|
|
"after": null,
|
||
|
|
"limit": 50
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Paramètres**:
|
||
|
|
- `conversation_id` (UUID, requis) : ID de la conversation
|
||
|
|
- `before` (DateTime ISO8601, optionnel) : Récupère les messages avant ce timestamp
|
||
|
|
- `after` (DateTime ISO8601, optionnel) : Récupère les messages après ce timestamp
|
||
|
|
- `limit` (usize, optionnel, défaut: 50, max: 100) : Nombre de messages à récupérer
|
||
|
|
|
||
|
|
**Règles**:
|
||
|
|
- Si `before` est fourni : tri DESC (messages plus anciens)
|
||
|
|
- Si `after` est fourni : tri ASC (messages plus récents)
|
||
|
|
- Si les deux sont fournis : messages entre `after` et `before` (tri ASC)
|
||
|
|
- Si aucun n'est fourni : messages les plus récents (tri DESC)
|
||
|
|
- Les résultats sont **toujours retournés en ordre ASC** (du plus ancien au plus récent)
|
||
|
|
|
||
|
|
### Outbound WebSocket Message
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"type": "HistoryChunk",
|
||
|
|
"conversation_id": "550e8400-e29b-41d4-a716-446655440000",
|
||
|
|
"messages": [
|
||
|
|
{
|
||
|
|
"id": "...",
|
||
|
|
"conversation_id": "...",
|
||
|
|
"sender_id": "...",
|
||
|
|
"content": "Hello world",
|
||
|
|
"created_at": "2025-12-05T10:00:00Z",
|
||
|
|
"is_edited": false,
|
||
|
|
"is_deleted": false,
|
||
|
|
...
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"has_more_before": true,
|
||
|
|
"has_more_after": false
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Champs**:
|
||
|
|
- `messages` : Liste des messages (toujours triés ASC)
|
||
|
|
- `has_more_before` : Indique s'il y a des messages plus anciens
|
||
|
|
- `has_more_after` : Indique s'il y a des messages plus récents
|
||
|
|
|
||
|
|
### Exemples d'utilisation
|
||
|
|
|
||
|
|
#### Charger les messages les plus récents
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"type": "FetchHistory",
|
||
|
|
"conversation_id": "...",
|
||
|
|
"before": null,
|
||
|
|
"after": null,
|
||
|
|
"limit": 50
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Charger les messages plus anciens (scroll up)
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"type": "FetchHistory",
|
||
|
|
"conversation_id": "...",
|
||
|
|
"before": "2025-12-05T10:00:00Z",
|
||
|
|
"after": null,
|
||
|
|
"limit": 50
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Charger les nouveaux messages (scroll down)
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"type": "FetchHistory",
|
||
|
|
"conversation_id": "...",
|
||
|
|
"before": null,
|
||
|
|
"after": "2025-12-05T10:00:00Z",
|
||
|
|
"limit": 50
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Index SQL
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE INDEX idx_messages_conv_created_at
|
||
|
|
ON messages(conversation_id, created_at DESC);
|
||
|
|
|
||
|
|
CREATE INDEX idx_messages_conv_created_not_deleted
|
||
|
|
ON messages(conversation_id, created_at DESC)
|
||
|
|
WHERE is_deleted = false;
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔍 Message Search
|
||
|
|
|
||
|
|
### Description
|
||
|
|
|
||
|
|
Recherche textuelle de messages dans une conversation. Utilise `ILIKE` avec index trigram pour une recherche performante et insensible à la casse.
|
||
|
|
|
||
|
|
### Inbound WebSocket Message
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"type": "SearchMessages",
|
||
|
|
"conversation_id": "550e8400-e29b-41d4-a716-446655440000",
|
||
|
|
"query": "hello world",
|
||
|
|
"limit": 50,
|
||
|
|
"offset": 0
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Paramètres**:
|
||
|
|
- `conversation_id` (UUID, requis) : ID de la conversation
|
||
|
|
- `query` (String, requis) : Terme de recherche (ne peut pas être vide)
|
||
|
|
- `limit` (usize, optionnel, défaut: 50, max: 100) : Nombre de résultats par page
|
||
|
|
- `offset` (usize, optionnel, défaut: 0) : Offset pour pagination
|
||
|
|
|
||
|
|
### Outbound WebSocket Message
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"type": "SearchResults",
|
||
|
|
"conversation_id": "550e8400-e29b-41d4-a716-446655440000",
|
||
|
|
"messages": [
|
||
|
|
{
|
||
|
|
"id": "...",
|
||
|
|
"content": "Hello world!",
|
||
|
|
"created_at": "2025-12-05T10:00:00Z",
|
||
|
|
...
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"query": "hello world",
|
||
|
|
"total": 123
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Champs**:
|
||
|
|
- `messages` : Liste des messages correspondants (triés par `created_at DESC`)
|
||
|
|
- `query` : La requête de recherche originale
|
||
|
|
- `total` : Nombre total de résultats (pour pagination)
|
||
|
|
|
||
|
|
### Exemples d'utilisation
|
||
|
|
|
||
|
|
#### Recherche simple
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"type": "SearchMessages",
|
||
|
|
"conversation_id": "...",
|
||
|
|
"query": "meeting",
|
||
|
|
"limit": 20,
|
||
|
|
"offset": 0
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Pagination des résultats
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"type": "SearchMessages",
|
||
|
|
"conversation_id": "...",
|
||
|
|
"query": "meeting",
|
||
|
|
"limit": 20,
|
||
|
|
"offset": 20
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Index SQL
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||
|
|
|
||
|
|
CREATE INDEX idx_messages_content_trgm
|
||
|
|
ON messages USING GIN(content gin_trgm_ops);
|
||
|
|
|
||
|
|
CREATE INDEX idx_messages_conv_content_trgm
|
||
|
|
ON messages USING GIN(conversation_id, content gin_trgm_ops);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Comportement
|
||
|
|
|
||
|
|
- ✅ Recherche insensible à la casse (`ILIKE`)
|
||
|
|
- ✅ Recherche partielle (contient le terme)
|
||
|
|
- ✅ Exclut les messages supprimés par défaut
|
||
|
|
- ✅ Tri par `created_at DESC` (plus récents en premier)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔄 Offline Sync
|
||
|
|
|
||
|
|
### Description
|
||
|
|
|
||
|
|
Synchronise tous les messages manquants depuis la dernière connexion. Inclut :
|
||
|
|
- Messages créés depuis `since`
|
||
|
|
- Messages édités depuis `since` (même si créés avant)
|
||
|
|
- Messages supprimés depuis `since` (même si créés avant)
|
||
|
|
|
||
|
|
Permet aux clients mobiles d'avoir une synchronisation fiable après une déconnexion.
|
||
|
|
|
||
|
|
### Inbound WebSocket Message
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"type": "SyncMessages",
|
||
|
|
"conversation_id": "550e8400-e29b-41d4-a716-446655440000",
|
||
|
|
"since": "2025-12-05T09:00:00Z"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Paramètres**:
|
||
|
|
- `conversation_id` (UUID, requis) : ID de la conversation
|
||
|
|
- `since` (DateTime ISO8601, requis) : Timestamp de la dernière synchronisation
|
||
|
|
|
||
|
|
### Outbound WebSocket Message
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"type": "SyncChunk",
|
||
|
|
"conversation_id": "550e8400-e29b-41d4-a716-446655440000",
|
||
|
|
"messages": [
|
||
|
|
{
|
||
|
|
"id": "...",
|
||
|
|
"content": "New message",
|
||
|
|
"created_at": "2025-12-05T10:00:00Z",
|
||
|
|
"is_edited": false,
|
||
|
|
"is_deleted": false,
|
||
|
|
...
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"id": "...",
|
||
|
|
"content": "Edited content",
|
||
|
|
"created_at": "2025-12-05T08:00:00Z",
|
||
|
|
"is_edited": true,
|
||
|
|
"edited_at": "2025-12-05T10:30:00Z",
|
||
|
|
...
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"id": "...",
|
||
|
|
"content": "Deleted message",
|
||
|
|
"created_at": "2025-12-05T08:30:00Z",
|
||
|
|
"is_deleted": true,
|
||
|
|
"deleted_at": "2025-12-05T10:45:00Z",
|
||
|
|
...
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"last_sync": "2025-12-05T11:00:00Z"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Champs**:
|
||
|
|
- `messages` : Tous les messages créés ou modifiés depuis `since` (triés par `created_at ASC`)
|
||
|
|
- `last_sync` : Timestamp actuel (à utiliser pour la prochaine sync)
|
||
|
|
|
||
|
|
### Exemples d'utilisation
|
||
|
|
|
||
|
|
#### Synchronisation initiale
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"type": "SyncMessages",
|
||
|
|
"conversation_id": "...",
|
||
|
|
"since": "2025-12-05T00:00:00Z"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Synchronisation après déconnexion
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"type": "SyncMessages",
|
||
|
|
"conversation_id": "...",
|
||
|
|
"since": "2025-12-05T09:30:00Z"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Index SQL
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE INDEX idx_messages_conv_created_sync
|
||
|
|
ON messages(conversation_id, created_at ASC)
|
||
|
|
WHERE is_deleted = false;
|
||
|
|
|
||
|
|
CREATE INDEX idx_messages_conv_updated_sync
|
||
|
|
ON messages(conversation_id, updated_at ASC)
|
||
|
|
WHERE is_deleted = false;
|
||
|
|
```
|
||
|
|
|
||
|
|
### Comportement
|
||
|
|
|
||
|
|
- ✅ Inclut tous les messages créés depuis `since`
|
||
|
|
- ✅ Inclut tous les messages édités depuis `since` (même créés avant)
|
||
|
|
- ✅ Inclut tous les messages supprimés depuis `since` (même créés avant)
|
||
|
|
- ✅ Tri par `created_at ASC` (du plus ancien au plus récent)
|
||
|
|
- ✅ Le client doit gérer les updates (édits) et deletes (suppressions)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔧 Spécifications techniques
|
||
|
|
|
||
|
|
### Repository Methods
|
||
|
|
|
||
|
|
#### `fetch_history`
|
||
|
|
```rust
|
||
|
|
pub async fn fetch_history(
|
||
|
|
&self,
|
||
|
|
conversation_id: Uuid,
|
||
|
|
before: Option<DateTime<Utc>>,
|
||
|
|
after: Option<DateTime<Utc>>,
|
||
|
|
limit: usize,
|
||
|
|
include_deleted: bool,
|
||
|
|
) -> Result<(Vec<Message>, bool, bool)>
|
||
|
|
```
|
||
|
|
|
||
|
|
Retourne : `(messages, has_more_before, has_more_after)`
|
||
|
|
|
||
|
|
#### `search_messages`
|
||
|
|
```rust
|
||
|
|
pub async fn search_messages(
|
||
|
|
&self,
|
||
|
|
conversation_id: Uuid,
|
||
|
|
query: &str,
|
||
|
|
limit: usize,
|
||
|
|
offset: usize,
|
||
|
|
include_deleted: bool,
|
||
|
|
) -> Result<(Vec<Message>, i64)>
|
||
|
|
```
|
||
|
|
|
||
|
|
Retourne : `(messages, total_count)`
|
||
|
|
|
||
|
|
#### `fetch_since`
|
||
|
|
```rust
|
||
|
|
pub async fn fetch_since(
|
||
|
|
&self,
|
||
|
|
conversation_id: Uuid,
|
||
|
|
since: DateTime<Utc>,
|
||
|
|
) -> Result<Vec<Message>>
|
||
|
|
```
|
||
|
|
|
||
|
|
### Permissions
|
||
|
|
|
||
|
|
Toutes les fonctionnalités nécessitent :
|
||
|
|
- `can_read_conversation(user_id, conversation_id)` : L'utilisateur doit avoir accès à la conversation
|
||
|
|
|
||
|
|
### Erreurs possibles
|
||
|
|
|
||
|
|
- `ChatError::Unauthorized` : Pas de permission pour lire la conversation
|
||
|
|
- `ChatError::ValidationError` : Query de recherche vide
|
||
|
|
- `ChatError::InternalError` : Erreur de base de données
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📱 Exemples d'utilisation
|
||
|
|
|
||
|
|
### Client Web (React)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// History Pagination
|
||
|
|
const fetchHistory = async (conversationId: string, before?: Date) => {
|
||
|
|
ws.send(JSON.stringify({
|
||
|
|
type: "FetchHistory",
|
||
|
|
conversation_id: conversationId,
|
||
|
|
before: before?.toISOString(),
|
||
|
|
after: null,
|
||
|
|
limit: 50
|
||
|
|
}));
|
||
|
|
};
|
||
|
|
|
||
|
|
// Message Search
|
||
|
|
const searchMessages = async (conversationId: string, query: string) => {
|
||
|
|
ws.send(JSON.stringify({
|
||
|
|
type: "SearchMessages",
|
||
|
|
conversation_id: conversationId,
|
||
|
|
query: query,
|
||
|
|
limit: 50,
|
||
|
|
offset: 0
|
||
|
|
}));
|
||
|
|
};
|
||
|
|
|
||
|
|
// Offline Sync
|
||
|
|
const syncMessages = async (conversationId: string, lastSync: Date) => {
|
||
|
|
ws.send(JSON.stringify({
|
||
|
|
type: "SyncMessages",
|
||
|
|
conversation_id: conversationId,
|
||
|
|
since: lastSync.toISOString()
|
||
|
|
}));
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### Client Mobile (React Native)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Sync après reconnexion
|
||
|
|
const syncAfterReconnect = async (conversationId: string) => {
|
||
|
|
const lastSync = await AsyncStorage.getItem(`last_sync_${conversationId}`);
|
||
|
|
const since = lastSync ? new Date(lastSync) : new Date(0);
|
||
|
|
|
||
|
|
ws.send(JSON.stringify({
|
||
|
|
type: "SyncMessages",
|
||
|
|
conversation_id: conversationId,
|
||
|
|
since: since.toISOString()
|
||
|
|
}));
|
||
|
|
|
||
|
|
// Écouter SyncChunk et mettre à jour last_sync
|
||
|
|
ws.on('message', (msg) => {
|
||
|
|
if (msg.type === 'SyncChunk') {
|
||
|
|
AsyncStorage.setItem(`last_sync_${conversationId}`, msg.last_sync);
|
||
|
|
// Mettre à jour l'UI avec les messages
|
||
|
|
}
|
||
|
|
});
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ⚠️ Limites et bonnes pratiques
|
||
|
|
|
||
|
|
### Limites
|
||
|
|
|
||
|
|
1. **History Pagination** :
|
||
|
|
- `limit` max : 100 messages
|
||
|
|
- Utiliser `before`/`after` plutôt que offset pour de meilleures performances
|
||
|
|
|
||
|
|
2. **Message Search** :
|
||
|
|
- `limit` max : 100 résultats
|
||
|
|
- `query` minimum : 1 caractère
|
||
|
|
- Recherche partielle (contient), pas de recherche exacte
|
||
|
|
|
||
|
|
3. **Offline Sync** :
|
||
|
|
- Pas de limite sur le nombre de messages (peut être volumineux)
|
||
|
|
- Le client doit gérer les updates et deletes
|
||
|
|
|
||
|
|
### Bonnes pratiques
|
||
|
|
|
||
|
|
1. **History Pagination** :
|
||
|
|
- Toujours utiliser `before` pour charger plus d'anciens messages
|
||
|
|
- Utiliser `after` pour charger les nouveaux messages
|
||
|
|
- Stocker le `created_at` du premier/dernier message pour la pagination
|
||
|
|
|
||
|
|
2. **Message Search** :
|
||
|
|
- Implémenter un debounce sur la recherche (300-500ms)
|
||
|
|
- Limiter la longueur minimale de la query (3 caractères recommandé)
|
||
|
|
- Afficher un indicateur de chargement pendant la recherche
|
||
|
|
|
||
|
|
3. **Offline Sync** :
|
||
|
|
- Stocker `last_sync` localement (AsyncStorage, localStorage)
|
||
|
|
- Sync automatique après reconnexion
|
||
|
|
- Gérer les conflits si un message est édité localement et sur le serveur
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🎨 Impact sur l'UI
|
||
|
|
|
||
|
|
### History Pagination
|
||
|
|
|
||
|
|
**Scroll infini vers le haut** :
|
||
|
|
```typescript
|
||
|
|
const [messages, setMessages] = useState<Message[]>([]);
|
||
|
|
const [hasMore, setHasMore] = useState(true);
|
||
|
|
|
||
|
|
const loadMore = async () => {
|
||
|
|
if (!hasMore) return;
|
||
|
|
|
||
|
|
const oldestMessage = messages[0];
|
||
|
|
const before = oldestMessage?.created_at;
|
||
|
|
|
||
|
|
fetchHistory(conversationId, before).then((chunk) => {
|
||
|
|
setMessages([...chunk.messages, ...messages]);
|
||
|
|
setHasMore(chunk.has_more_before);
|
||
|
|
});
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### Message Search
|
||
|
|
|
||
|
|
**Barre de recherche avec résultats** :
|
||
|
|
```typescript
|
||
|
|
const [searchQuery, setSearchQuery] = useState("");
|
||
|
|
const [searchResults, setSearchResults] = useState<Message[]>([]);
|
||
|
|
|
||
|
|
const handleSearch = debounce((query: string) => {
|
||
|
|
if (query.length < 3) return;
|
||
|
|
|
||
|
|
searchMessages(conversationId, query).then((results) => {
|
||
|
|
setSearchResults(results.messages);
|
||
|
|
});
|
||
|
|
}, 300);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Offline Sync
|
||
|
|
|
||
|
|
**Indicateur de synchronisation** :
|
||
|
|
```typescript
|
||
|
|
const [isSyncing, setIsSyncing] = useState(false);
|
||
|
|
|
||
|
|
const sync = async () => {
|
||
|
|
setIsSyncing(true);
|
||
|
|
const lastSync = await getLastSync(conversationId);
|
||
|
|
syncMessages(conversationId, lastSync);
|
||
|
|
// setIsSyncing(false) dans le handler SyncChunk
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📊 Performance
|
||
|
|
|
||
|
|
### Index utilisés
|
||
|
|
|
||
|
|
- `idx_messages_conv_created_at` : Pagination efficace
|
||
|
|
- `idx_messages_content_trgm` : Recherche textuelle rapide
|
||
|
|
- `idx_messages_conv_created_sync` : Sync optimisée
|
||
|
|
|
||
|
|
### Métriques attendues
|
||
|
|
|
||
|
|
- **History Pagination** : < 50ms pour 50 messages
|
||
|
|
- **Message Search** : < 100ms pour 1000 messages
|
||
|
|
- **Offline Sync** : < 200ms pour 100 messages
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔐 Sécurité
|
||
|
|
|
||
|
|
- ✅ Toutes les fonctionnalités vérifient les permissions via `PermissionService`
|
||
|
|
- ✅ Les messages supprimés sont exclus par défaut (sauf si `include_deleted = true`)
|
||
|
|
- ✅ Validation des paramètres (query non vide, limit max, etc.)
|
||
|
|
- ✅ Pas d'injection SQL (utilisation de paramètres liés)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📝 Migration
|
||
|
|
|
||
|
|
Pour activer ces fonctionnalités, exécuter :
|
||
|
|
|
||
|
|
```bash
|
||
|
|
psql -d veza_db -f migrations/006_history_search_sync.sql
|
||
|
|
```
|
||
|
|
|
||
|
|
Cette migration crée tous les index nécessaires.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**Fin du document**
|
||
|
|
|