feat(api): sync réel — outbox POST /sync/ops idempotente + snapshot GET /sync/permissions (owner V1)
- operations.operation_id/ref_id BIGINT (= id client pending_operations), UNIQUE(device_id, operation_id) idempotente ; migration 000004 corrigée + test
- service/sync.go : ApplyBatch séquentiel (no-op si déjà traitée ; create déjà présent / update/move sur ressource absente / delete absent = no-op) ;
arrêt à la première erreur non-idempotente (NAME_CONFLICT, NOT_FOUND, INVALID_REQUEST) + trace après succès ; ops share/share_link accusées sans état (V1)
- handlers/sync.go : POST /sync/ops → {applied, failed|null} ; GET /sync/permissions?after= → snapshot delta (effectiveAccess owner, cachedAt ms)
- snapshot : repo.ListOwned (delta updated_at en ms, trié) ; index partiel racine (owner_id, name) WHERE parent_id IS NULL
- GET /files sans folderId = racine uniquement (convention useFiles(undefined)) ; tests files_test corrigés (codes d'erreur imbriqués, tailles)
- fix dbtest : ping la connexion maintenance (création de la DB cible possible) — les tests handlers/repo tournent enfin contre PG réel
- tests handlers end-to-end : batch appliqué + retry idempotent, arrêt sur NAME_CONFLICT, delete idempotent, snapshot + delta
This commit is contained in:
+8
-2
@@ -93,7 +93,13 @@ type OcrJob = { id: string; status: OcrJobStatus; text?: string | null; error?:
|
||||
- **Idempotence** : contrainte d'unicité serveur `(device_id, operation_id)`. Pour chaque op : si déjà traitée → **no-op** (comptée comme appliquée, les doublons arrivent à cause du backoff/retry). Sinon appliquée si valide.
|
||||
- **Ordre** : les opérations sont appliquées **séquentiellement**, dans l'ordre du batch. Le serveur **s'arrête à la première erreur non-idempotente** et renvoie l'index atteint — le client reprend à cet index.
|
||||
- Réponse : `2xx` avec `{ "applied": int, "failed": { "operation_id": int, "code": string, "message": string } | null }` (`applied` = index de la prochaine op à envoyer).
|
||||
- Côté client, le `pushStatus` (pending/synced/failed) des shares/share_links est **dérivé** de l'état des opérations de l'outbox ; dead-letter après `MAX_PENDING_ATTEMPTS` (= 5).
|
||||
- Côté client, le `pushStatus` (pending/synced/failed) des shares/share_links est **dérivé** de l'état des opérations de l'outbox ; dead-letter après `MAX_PENDING_ATTEMPTS` (= 5). **Côté serveur, les ops `share | revoke_share | update_share | create_link | revoke_link` sont accusées réception mais ne créent aucun état** (V1 single-owner, pas de table shares serveur) — la dérivation du pushStatus reste purement client.
|
||||
- Sémantique d'application (côté serveur) :
|
||||
- `create_resource` : crée la ressource ; **déjà présente → no-op** (rejeu idempotent). `payload.name` obligatoire.
|
||||
- `update_metadata` / `move_resource` : ressource absente → **no-op** (état terminal atteint) ; dossier cible de `move_resource` absent → `NOT_FOUND` ; déplacement dans soi-même → `INVALID_REQUEST`.
|
||||
- `delete_resource` : **idempotent** — suppression d'une ressource absente = succès.
|
||||
- Validation (deuxième champ `operation_id`, hex32 pour `resource_id`, enum `operation`) → échec `INVALID_REQUEST` avec arrêt du batch.
|
||||
- Nom déjà pris (même parent, ou à la racine) → échec `NAME_CONFLICT`.**
|
||||
|
||||
### 6.2 Snapshot — `GET /sync/permissions?after=<cached_at_ms>`
|
||||
|
||||
@@ -127,4 +133,4 @@ type ResourcePermission = {
|
||||
|
||||
## 7. Codes d'erreur courants
|
||||
|
||||
`NOT_FOUND`, `NOT_IMPLEMENTED` (501 temporaire sur les routes non construites — état actuel : files CRUD/upload/search, folders, devices, health sont réels ; `ocr/*`, `sync/*` en queue), `FILE_TOO_LARGE` (413), `NAME_CONFLICT` (409 — même nom dans le même parent, cf. `UNIQUE(parent_id, name)`), `NETWORK_ERROR` (côté client), `HTTP_<status>` (fallback). Le serveur doit répondre 501 `{ "error": { "code": "NOT_IMPLEMENTED", "message": "…" } }` sur toute route encore en queue. Statut `SERVICE_UNAVAILABLE` (503) si le backend n'est pas initialisé.
|
||||
`NOT_FOUND`, `NOT_IMPLEMENTED` (501 temporaire sur les routes non construites — état actuel : files CRUD/upload/search, folders, devices, health, **sync/ops + sync/permissions** sont réels ; `ocr/*` en queue), `FILE_TOO_LARGE` (413), `NAME_CONFLICT` (409 — même nom dans le même parent, cf. `UNIQUE(parent_id, name)`, **ou à la racine**, index partiel `(owner_id, name) WHERE parent_id IS NULL`), `NETWORK_ERROR` (côté client), `HTTP_<status>` (fallback). Le serveur doit répondre 501 `{ "error": { "code": "NOT_IMPLEMENTED", "message": "…" } }` sur toute route encore en queue. Statut `SERVICE_UNAVAILABLE` (503) si le backend n'est pas initialisé.
|
||||
Reference in New Issue
Block a user