fix(entries): 404 on PUT when the row no longer exists
The UPDATE matched nothing but the handler still answered 200 Updated, so the editor reported Saved for an edit that went nowhere - the entry had been deleted by a sync or a purge after the editor opened it. Silent data loss; the client already surfaces a failed save as an error toast.
This commit is contained in:
+8
-5
@@ -80,13 +80,16 @@ Préparer : 3 entrées `A`, `B`, `C`, dont `A` dans le dossier `Work` avec le ta
|
|||||||
Aucun de ces enchaînements ne doit afficher « Entry changed elsewhere » :
|
Aucun de ces enchaînements ne doit afficher « Entry changed elsewhere » :
|
||||||
- **A12a** : `A` ouverte + dirty → Save → modifier encore → Save (le rechargement post-save re-cale la référence)✅
|
- **A12a** : `A` ouverte + dirty → Save → modifier encore → Save (le rechargement post-save re-cale la référence)✅
|
||||||
- **A12b** : `A` ouverte + dirty → batch *Add tag* / *Move to folder* sur `A` → Save (l'éditeur a absorbé le changement)✅
|
- **A12b** : `A` ouverte + dirty → batch *Add tag* / *Move to folder* sur `A` → Save (l'éditeur a absorbé le changement)✅
|
||||||
- **A12c** : `A` ouverte + dirty → supprimer son dossier → Save (cf. A7)
|
- **A12c** : `A` ouverte + dirty → supprimer son dossier → Save (cf. A7)✅
|
||||||
- **A12d** : `A` ouverte + dirty → *Fetch missing* favicons → Save (l'icône est hors payload éditeur)
|
- **A12d** : `A` ouverte + dirty → *Fetch missing* favicons → Save (l'icône est hors payload éditeur)✅
|
||||||
- **A12e** : `A` ouverte + dirty → sync **sans** changement distant sur `A` → Save
|
- **A12e** : `A` ouverte + dirty → sync **sans** changement distant sur `A` → Save✅
|
||||||
|
|
||||||
### A13 · entrée supprimée ailleurs pendant l'édition
|
### A13 · entrée supprimée ailleurs pendant l'édition ✅
|
||||||
- **ACTION** : `A` ouverte + dirty → un sync distant supprime `A` (tombstone) → Save
|
- **ACTION** : `A` ouverte + dirty → un sync distant supprime `A` (tombstone) → Save
|
||||||
- **ATTENDU** : pas de plantage. L'entrée n'est plus en mémoire → le contrôle A11 passe la main ; le PUT échoue proprement avec un toast d'erreur.
|
- **ATTENDU** : pas de plantage. L'entrée n'est plus en mémoire → le contrôle A11 passe la main ; le PUT échoue proprement avec un **toast d'erreur** — surtout pas « Saved ».
|
||||||
|
- **Recette locale, sans second appareil** : ouvrir `A` et la rendre dirty → dans DB Browser, app lancée, `DELETE FROM vault_entries WHERE id = <id>` puis **Write Changes** (sans ça le WAL ne publie rien) → dans l'app, déclencher un `loadEntries()` qui ne concerne pas `A` (mettre une **autre** entrée à la corbeille puis *Empty trash*) → Save.
|
||||||
|
- **Bug trouvé par ce test** : le serveur répondait **200 « Updated »** pour un `UPDATE` n'ayant touché **aucune** ligne — le client affichait « Saved » alors que rien n'était écrit. Perte de données silencieuse. `HandleUpdateEntry` teste maintenant `RowsAffected` et renvoie 404.
|
||||||
|
- **Même classe, non corrigé** : delete / restore / favorite / pin / set-icon répondent aussi OK sur un id inexistant. Assumé — aucun ne prétend avoir enregistré un travail de l'utilisateur, et le client recharge la liste juste après.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -514,6 +514,7 @@ var
|
|||||||
LSiteEnc, LSiteIv, LTitleEnc, LTitleIv, LTagsEnc, LTagsIv: string;
|
LSiteEnc, LSiteIv, LTitleEnc, LTitleIv, LTagsEnc, LTagsIv: string;
|
||||||
LHasTemplate: Boolean;
|
LHasTemplate: Boolean;
|
||||||
LQ: TFDQuery;
|
LQ: TFDQuery;
|
||||||
|
LRows: Integer;
|
||||||
begin
|
begin
|
||||||
try
|
try
|
||||||
LUserId := Authenticate(ARequest, AResponse);
|
LUserId := Authenticate(ARequest, AResponse);
|
||||||
@@ -664,6 +665,7 @@ begin
|
|||||||
LQ.ParamByName('id').AsInteger := LId;
|
LQ.ParamByName('id').AsInteger := LId;
|
||||||
LQ.ParamByName('uid').AsInteger := LUserId;
|
LQ.ParamByName('uid').AsInteger := LUserId;
|
||||||
LQ.ExecSQL;
|
LQ.ExecSQL;
|
||||||
|
LRows := LQ.RowsAffected;
|
||||||
finally
|
finally
|
||||||
LQ.Free;
|
LQ.Free;
|
||||||
end;
|
end;
|
||||||
@@ -671,6 +673,15 @@ begin
|
|||||||
DB.Unlock;
|
DB.Unlock;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// No row matched: the entry was deleted (another device's sync, a purge)
|
||||||
|
// between the editor opening it and this save. Reporting 200 here made the
|
||||||
|
// client show "Saved" for an edit that went nowhere — silent data loss.
|
||||||
|
if LRows <= 0 then
|
||||||
|
begin
|
||||||
|
TJSONHelper.SendError(AResponse, 404, 'Entry no longer exists');
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
LogAudit(LUserId, 'edit_entry', GetClientIP(ARequest));
|
LogAudit(LUserId, 'edit_entry', GetClientIP(ARequest));
|
||||||
TJSONHelper.SendOK(AResponse, 'Updated');
|
TJSONHelper.SendOK(AResponse, 'Updated');
|
||||||
end;
|
end;
|
||||||
|
|||||||
Reference in New Issue
Block a user