mirror of
https://github.com/napnap75/multiarch-docker-images.git
synced 2026-11-11 00:22:12 +01:00
feat(ui): US-22 becomes a side-effect-free draft correction; add US-24 S3 deletion
US-22: the PocketBase UI edits one field at a time with no preview of the final name, so a field edit now persists the values with no template evaluation, no re-filing and no status change; the corrected event carries the field diff alone. The full correction (all fields, dry-run, re-filing, validation) moves to the custom dashboard (US-3x). US-24: deleting a documents record deletes the S3 object at file_key first; the cascade delete removes the history. Spec, hooks, E2E updated together.
This commit is contained in:
+8
-6
@@ -115,7 +115,7 @@ On classification failure, the document is routed to `pending/` and the payload
|
||||
}
|
||||
```
|
||||
|
||||
`corrected` — created after a dashboard correction is saved and the file re-filed; a correction always moves the document to `validated` whatever its previous status, so the status change is part of the snapshot diff:
|
||||
`corrected` — created whenever declared fields are modified, whatever the interface. A field edited from the standard PocketBase UI is a draft correction (US-22): the values persist as they are, no template evaluation, no re-filing, no status change, and the snapshots carry the field diff alone. A dashboard correction (US-3x) adds the side effects: the file is re-filed and the status moves to `validated`, so `file_key` and `status` join the diff:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -203,8 +203,9 @@ Custom pages on the PocketBase instance:
|
||||
|
||||
- File list: all files with status, filters, combined search over full text (FTS5) + metadata/fields.
|
||||
- Detail: PDF preview streamed from S3 through a dedicated route, along with extracted fields; debug mode adds detailed log lines explaining every rule evaluation and action performed.
|
||||
- Correction: performed directly from the standard PocketBase collections UI (or any PocketBase client) by editing the extracted fields. The application's `OnRecordUpdateRequest` hook on `documents` intercepts the save: it evaluates the selected template with the new values and refuses the save if the template cannot be evaluated without error; otherwise it re-files the file (copy to the new key, delete the old one, incremental suffix on conflict, move skipped when the rendered name is unchanged), updates `file_key`, sets the status to `validated` whatever the previous status, and appends the `corrected` event. A corrected `pending` file is thereby removed from `pending/`. On a mid-re-filing failure the save is rejected and the S3 state is restored. History is preserved.
|
||||
- Validation: performed from the same interface by changing the status to `validated`; the hook appends the `validated` event, and any other status change is refused. A `pending` file can only leave the review queue through a correction that can be saved, which itself sets the status to `validated`.
|
||||
- Correction: performed directly from the standard PocketBase collections UI (or any PocketBase client) by editing the extracted fields. The application's `OnRecordUpdateRequest` hook on `documents` intercepts the save and appends the `corrected` event carrying the field diff — and nothing else. The UI edits one field at a time with no preview of the resulting file name, so the hook deliberately performs no template evaluation and no re-filing, and the status does not move: an inconsistent draft cannot trigger irreversible side effects. The full correction — all fields at once, dry-run of the resulting name, re-filing and validation — is the custom dashboard's job (US-3x).
|
||||
- Validation: performed from the same interface by changing the status to `validated`; the hook appends the `validated` event, and any other status change is refused. A `pending` file can only leave the review queue through a full correction from the custom dashboard (US-3x).
|
||||
- Deletion: deleting a `documents` record from the same interface first deletes the S3 object at `file_key` (US-24); on S3 failure the deletion is refused and the record stays. The cascade delete of the `document_events` relation removes the history with the record; with versioning the object stays recoverable via its delete marker.
|
||||
- Rule management: config display and dry-run on an existing document. The configuration is reloaded only on restart; there is no runtime reload.
|
||||
|
||||
Authentication: single account via PocketBase's standard auth.
|
||||
@@ -249,11 +250,12 @@ The scenarios form one narrative; each subtest names the US it proves:
|
||||
2. Import — a valid simulated statement is classified and filed to `files/` with its `imported` event (US-10 to US-17).
|
||||
3. Duplicate — the same content re-uploaded is deleted from `inbox/`, no record, no event (US-12).
|
||||
4. Pending — a missing mandatory date routes the document to `pending/` with `errors` in the imported event and the recognized fields kept (US-13 to US-15).
|
||||
5. Correction — a field PATCH through the API re-files and validates, `corrected` event appended (US-22).
|
||||
5. Correction — a field PATCH through the API persists the values with no side effect (no re-filing, no status change), `corrected` event carrying the field diff (US-22).
|
||||
6. Validation — a status-only PATCH appends the `validated` event, `file_key` untouched (US-23).
|
||||
7. Append-only — PATCH and DELETE on `document_events` are refused (US-04).
|
||||
8. Recovery — a stuck record (`file_key` still under `inbox/`, created directly through the API) is completed by the next pass (US-11).
|
||||
9. Restart — the container restarts with all state intact.
|
||||
8. Deletion — a deleted record removes the S3 object; the history follows via the cascade delete (US-24).
|
||||
9. Recovery — a stuck record (`file_key` still under `inbox/`, created directly through the API) is completed by the next pass (US-11).
|
||||
10. Restart — the container restarts with all state intact.
|
||||
|
||||
`POST /api/archivist/poll` (superuser only) runs one poll pass immediately. It exists so the gate and the operator trigger passes on demand instead of waiting for the cron; a fresh upload needs two calls because the poller waits for size/ETag stability across two passes (US-10).
|
||||
|
||||
|
||||
+52
-18
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// The suite is one narrative: it starts a disposable Garage and the image
|
||||
// under test, then follows documents through the whole pipeline - import,
|
||||
// duplicate, pending, correction, validation, append-only, recovery,
|
||||
// duplicate, pending, correction, validation, append-only, deletion, recovery,
|
||||
// restart. It is a regression gate for dependency upgrades (PocketBase,
|
||||
// minio-go, ledongthuc/pdf, expr, Garage), not an acceptance proof.
|
||||
//
|
||||
@@ -670,9 +670,10 @@ func TestE2E(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
// 5. Correction (US-22): a field PATCH through the API re-files the
|
||||
// document, sets status validated and appends the corrected event.
|
||||
t.Run("correction: a field PATCH re-files and validates", func(t *testing.T) {
|
||||
// 5. Correction (US-22): a field PATCH persists the values with no
|
||||
// side effect - no re-filing, no status change; the corrected event
|
||||
// carries the field diff alone.
|
||||
t.Run("correction: a field PATCH persists with no side effect", func(t *testing.T) {
|
||||
pending := listRecords(t, "documents", "(status='pending')", "")
|
||||
if len(pending) != 1 {
|
||||
t.Fatalf("want 1 pending document, got %d", len(pending))
|
||||
@@ -683,28 +684,37 @@ func TestE2E(t *testing.T) {
|
||||
if status != 200 {
|
||||
t.Fatalf("correction refused: %d %v", status, out)
|
||||
}
|
||||
want := "files/BRED/2026-01_Releve de compte_Thomas.pdf"
|
||||
doc := oneRecord(t, "documents", "(id='"+id+"')")
|
||||
if doc["status"] != "validated" {
|
||||
t.Fatalf("status: %v", doc["status"])
|
||||
if doc["status"] != "pending" {
|
||||
t.Fatalf("status must not change on a field edit: %v", doc["status"])
|
||||
}
|
||||
if doc["file_key"] != want {
|
||||
t.Fatalf("file_key: %v, want %v", doc["file_key"], want)
|
||||
if doc["file_key"] != "pending/no-date.pdf" {
|
||||
t.Fatalf("file_key must not change on a field edit: %v", doc["file_key"])
|
||||
}
|
||||
if !s3Exists(t, want) || s3Exists(t, "pending/no-date.pdf") {
|
||||
t.Fatal("S3 state does not match the correction")
|
||||
if !s3Exists(t, "pending/no-date.pdf") {
|
||||
t.Fatal("the object must stay in pending/")
|
||||
}
|
||||
if s3Exists(t, "files/BRED/2026-01_Releve de compte_Thomas.pdf") {
|
||||
t.Fatal("a field edit must not re-file the object")
|
||||
}
|
||||
p := eventPayloadOfType(t, eventsOf(t, id), "corrected")
|
||||
before, _ := p["before"].(map[string]any)
|
||||
after, _ := p["after"].(map[string]any)
|
||||
if before["file_key"] != "pending/no-date.pdf" || before["status"] != "pending" {
|
||||
t.Fatalf("before: %v", before)
|
||||
if _, has := before["emissionDate"]; has {
|
||||
t.Fatalf("emissionDate was unknown before the correction: %v", before)
|
||||
}
|
||||
if after["file_key"] != want || after["status"] != "validated" {
|
||||
t.Fatalf("after: %v", after)
|
||||
if after["emissionDate"] != "2026-01-15T00:00:00Z" {
|
||||
t.Fatalf("after.emissionDate: %v", after)
|
||||
}
|
||||
for _, s := range []string{"file_key", "status"} {
|
||||
if _, has := before[s]; has {
|
||||
t.Fatalf("%s must not appear in a field-edit diff: %v", s, before)
|
||||
}
|
||||
if _, has := after[s]; has {
|
||||
t.Fatalf("%s must not appear in a field-edit diff: %v", s, after)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 6. Validation (US-23): a status-only PATCH appends the validated
|
||||
// event; file_key is untouched and the snapshots carry the status
|
||||
// alone.
|
||||
@@ -759,7 +769,31 @@ func TestE2E(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
// 8. Recovery (US-11): a stuck record - file_key still under inbox/,
|
||||
// 8. Deletion (US-24): deleting a documents record deletes the S3
|
||||
// object; the history follows via the cascade delete.
|
||||
t.Run("deletion: a deleted record removes the S3 object", func(t *testing.T) {
|
||||
pending := listRecords(t, "documents", "(status='pending')", "")
|
||||
if len(pending) != 1 {
|
||||
t.Fatalf("want 1 pending document, got %d", len(pending))
|
||||
}
|
||||
id := pending[0]["id"].(string)
|
||||
if n := len(eventsOf(t, id)); n == 0 {
|
||||
t.Fatal("no events before deletion")
|
||||
}
|
||||
status, out := call(t, http.MethodDelete, "/api/collections/documents/records/"+id, nil)
|
||||
if status != 204 {
|
||||
t.Fatalf("delete refused: %d %v", status, out)
|
||||
}
|
||||
if s3Exists(t, "pending/no-date.pdf") {
|
||||
t.Fatal("S3 object not deleted with the record")
|
||||
}
|
||||
if n := len(eventsOf(t, id)); n != 0 {
|
||||
t.Fatal("history not cascade-deleted with the record")
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// 9. Recovery (US-11): a stuck record - file_key still under inbox/,
|
||||
// created directly through the API - is completed by the next pass.
|
||||
t.Run("recovery: a stuck filing is completed by the next pass", func(t *testing.T) {
|
||||
hash := putPDF(t, "inbox/stuck.pdf",
|
||||
@@ -788,7 +822,7 @@ func TestE2E(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
// 9. Restart: the container restarts with all state intact.
|
||||
// 10. Restart: the container restarts with all state intact.
|
||||
t.Run("restart: state survives a container restart", func(t *testing.T) {
|
||||
before := listRecords(t, "documents", "", "")
|
||||
if out, err := run("docker", "restart", appC); err != nil {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/napnap75/archivist/internal/storage"
|
||||
)
|
||||
|
||||
// BindHooks registers the request hooks (US-04, US-22, US-23).
|
||||
// BindHooks registers the request hooks (US-04, US-22, US-23, US-24).
|
||||
func BindHooks(app core.App, cfg *config.Config, s3 *storage.Client) {
|
||||
// US-04: document_events is append-only for everyone, superusers included.
|
||||
app.OnRecordUpdateRequest(eventsName).BindFunc(func(e *core.RecordRequestEvent) error {
|
||||
@@ -20,13 +20,35 @@ func BindHooks(app core.App, cfg *config.Config, s3 *storage.Client) {
|
||||
return errAppendOnly
|
||||
})
|
||||
|
||||
// US-22 / US-23: corrections and validations through the standard UI.
|
||||
// US-24: deleting a documents record deletes the underlying S3 object
|
||||
// first; on failure the deletion is refused and both the record and
|
||||
// the object are left in place. Like US-04, the hook binds to
|
||||
// superusers as well: API rules alone cannot enforce it.
|
||||
app.OnRecordDeleteRequest("documents").BindFunc(func(e *core.RecordRequestEvent) error {
|
||||
key := e.Record.GetString("file_key")
|
||||
if key == "" {
|
||||
return e.Next()
|
||||
}
|
||||
if err := s3.Remove(e.Request.Context(), key); err != nil {
|
||||
return fmt.Errorf("refused: S3 deletion of %s failed: %v", key, err)
|
||||
}
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
// US-22 / US-23: draft corrections and validations through the standard UI.
|
||||
app.OnRecordUpdateRequest("documents").BindFunc(func(e *core.RecordRequestEvent) error {
|
||||
return handleDocumentUpdate(app, cfg, s3, e)
|
||||
return handleDocumentUpdate(app, cfg, e)
|
||||
})
|
||||
}
|
||||
|
||||
func handleDocumentUpdate(app core.App, cfg *config.Config, s3 *storage.Client, e *core.RecordRequestEvent) error {
|
||||
// handleDocumentUpdate implements US-22 and US-23. A field edit from the
|
||||
// standard UI is a draft correction: the values persist as they are, no
|
||||
// template evaluation, no re-filing, no status change — the UI edits one
|
||||
// field at a time and shows no preview of the resulting file name. The full
|
||||
// correction (all fields, dry-run, re-filing, validation) is the custom
|
||||
// dashboard's job (US-3x). The only direct status change is processed ->
|
||||
// validated (US-23).
|
||||
func handleDocumentUpdate(app core.App, cfg *config.Config, e *core.RecordRequestEvent) error {
|
||||
newRec := e.Record
|
||||
|
||||
// The original state, before the submitted data is persisted.
|
||||
@@ -36,6 +58,7 @@ func handleDocumentUpdate(app core.App, cfg *config.Config, s3 *storage.Client,
|
||||
}
|
||||
oldStatus := orig.GetString("status")
|
||||
newStatus := newRec.GetString("status")
|
||||
|
||||
fieldsChanged := false
|
||||
for _, f := range cfg.FieldList {
|
||||
if fmt.Sprintf("%v", orig.Get(f.Name)) != fmt.Sprintf("%v", newRec.Get(f.Name)) {
|
||||
@@ -44,62 +67,29 @@ func handleDocumentUpdate(app core.App, cfg *config.Config, s3 *storage.Client,
|
||||
}
|
||||
}
|
||||
|
||||
if !fieldsChanged {
|
||||
// US-23: direct validation, processed -> validated, no field change.
|
||||
if newStatus == "validated" && oldStatus == "processed" {
|
||||
before := Snapshot{"status": oldStatus}
|
||||
after := Snapshot{"status": "validated"}
|
||||
if err := AppendEvent(app, newRec.Id, "validated", before, after, nil); err != nil {
|
||||
return err
|
||||
// US-23: the only direct status change is processed -> validated.
|
||||
if newStatus != oldStatus && (newStatus != "validated" || oldStatus != "processed") {
|
||||
return fmt.Errorf("refused: the only direct status change is processed -> validated; full corrections belong to the review dashboard")
|
||||
}
|
||||
|
||||
// US-22: append the corrected event carrying the field diff alone;
|
||||
// file_key and status are absent since they did not change.
|
||||
if fieldsChanged {
|
||||
before, after := DiffSnapshots(SnapshotFromRecord(cfg, orig, false), SnapshotFromRecord(cfg, newRec, false))
|
||||
if len(before) > 0 || len(after) > 0 {
|
||||
if err := AppendEvent(app, newRec.Id, "corrected", before, after, nil); err != nil {
|
||||
app.Logger().Error("corrected event append failed", "document", newRec.Id, "error", err)
|
||||
}
|
||||
return e.Next()
|
||||
}
|
||||
if newStatus != oldStatus {
|
||||
return fmt.Errorf("refused: the only direct status change is processed -> validated; other changes go through a field correction")
|
||||
}
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
// US-22: a field correction. Whatever the previous status, the document
|
||||
// becomes validated after a successful re-filing.
|
||||
fields := FieldsFromRecord(cfg, newRec)
|
||||
tplName := "default"
|
||||
if v, ok := fields["template"].(string); ok && v != "" {
|
||||
tplName = v
|
||||
}
|
||||
tpl, ok := cfg.Templates[tplName]
|
||||
if !ok {
|
||||
return fmt.Errorf("refused: unknown template %q", tplName)
|
||||
}
|
||||
rendered, err := tpl.Render(fields, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("refused: %v", err)
|
||||
}
|
||||
|
||||
oldKey := orig.GetString("file_key")
|
||||
target := "files/" + rendered
|
||||
finalKey := oldKey
|
||||
if target != oldKey {
|
||||
finalKey, err = s3.ResolveConflict(e.Request.Context(), target)
|
||||
if err != nil {
|
||||
// US-23: the validated event, snapshots carrying the status change alone.
|
||||
if newStatus != oldStatus {
|
||||
before := Snapshot{"status": oldStatus}
|
||||
after := Snapshot{"status": "validated"}
|
||||
if err := AppendEvent(app, newRec.Id, "validated", before, after, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s3.Copy(e.Request.Context(), oldKey, finalKey); err != nil {
|
||||
return fmt.Errorf("refused: re-filing failed: %v", err)
|
||||
}
|
||||
if err := s3.Remove(e.Request.Context(), oldKey); err != nil {
|
||||
// Roll back the partial copy so the S3 state matches the record.
|
||||
_ = s3.Remove(e.Request.Context(), finalKey)
|
||||
return fmt.Errorf("refused: re-filing failed: %v", err)
|
||||
}
|
||||
}
|
||||
newRec.Set("file_key", finalKey)
|
||||
newRec.Set("status", "validated")
|
||||
|
||||
before := SnapshotFromRecord(cfg, orig, true)
|
||||
after := SnapshotFromRecord(cfg, newRec, true)
|
||||
if err := AppendEvent(app, newRec.Id, "corrected", before, after, nil); err != nil {
|
||||
app.Logger().Error("corrected event append failed", "document", newRec.Id, "error", err)
|
||||
}
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ func main() {
|
||||
app.Logger().Warn(s3.StartupWarning, "bucket", envCfg.S3Bucket)
|
||||
}
|
||||
|
||||
// US-22 / US-23 / US-04: request hooks on documents and document_events.
|
||||
// US-04 / US-22 / US-23 / US-24: request hooks on documents and document_events.
|
||||
pbapp.BindHooks(app, cfg, s3)
|
||||
|
||||
// Manual poll pass: a superuser-only route used by the E2E gate and the
|
||||
|
||||
+24
-14
@@ -176,8 +176,7 @@ Acceptance criteria:
|
||||
|
||||
## Epic: Data visualization (standard PocketBase UI)
|
||||
|
||||
The standard PocketBase collections UI is the nominal interface for reading and correcting documents. Field edits and status changes made through it (or through any PocketBase API client) are intercepted by the application's `OnRecordUpdateRequest` hook on `documents`, which performs the required side effects: template validation, re-filing, event tracking. The custom dashboard (later epic) adds search, PDF preview and dry-run on top of the same hook, not a separate correction flow.
|
||||
|
||||
The standard PocketBase collections UI is the nominal interface for reading and correcting documents. Field edits and status changes made through it (or through any PocketBase API client) are intercepted by the application's `OnRecordUpdateRequest` hook on `documents`, which records the change in the event history (US-22) and enforces the status rules (US-23) — but deliberately performs no re-filing: the UI edits one field at a time and shows no preview of the resulting file name, so an inconsistent draft cannot trigger irreversible side effects. The full correction — all fields at once, dry-run of the resulting name, re-filing and validation — is the custom dashboard's job (US-3x), and record deletions propagate to S3 (US-24).
|
||||
### US-20 — Browse documents in the collections UI
|
||||
|
||||
As the user,
|
||||
@@ -202,21 +201,19 @@ Acceptance criteria:
|
||||
- Given an event, when I open it, then the `payload` JSON is displayed and readable, matching the section-4 format.
|
||||
- Given the collection, when I try to edit or delete a record from the UI, then the operation is rejected by the application's request hooks (US-04) — including for the superuser account the admin UI uses.
|
||||
|
||||
### US-22 — Correct a document's fields from the standard PocketBase UI
|
||||
### US-22 — Edit a document's fields from the standard PocketBase UI without side effects
|
||||
|
||||
As the user,
|
||||
I want to edit a document's fields directly in the collections UI (or any PocketBase client) and have the application do the rest in the update hook,
|
||||
so that a file needing correction is re-filed and traced without any custom interface.
|
||||
I want field edits saved from the standard PocketBase UI (or any PocketBase client) to persist the new values without any side effect — no template evaluation, no re-filing, no status change,
|
||||
so that the UI's editing model — one field at a time, no preview of the resulting file name — cannot trigger irreversible actions I cannot review before saving.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Given a document, when I edit one or more fields in the collections UI and save, then the `OnRecordUpdateRequest` hook on `documents` evaluates the selected template with the new field values and refuses the save with an explicit error if the template is unknown or cannot be rendered (missing mandatory field, evaluation error); the record is left unchanged and no event is appended.
|
||||
- Given the save is valid, when the hook runs, then the file is re-filed: copy to the new key, delete the old key, incremental suffix on conflict; the record's `file_key` is updated to the final, conflict-resolved key.
|
||||
- Given the rendered name equals the current `file_key`, when the hook runs, then the copy and delete are skipped, the event is still appended, and the status still moves to `validated`.
|
||||
- Given the save is valid, when the record is persisted, then the status moves to `validated` whatever the previous status, and a `corrected` event is appended with `before`/`after` snapshots conforming to the section-4 format (`file_key` and `status` included in the diff when they change).
|
||||
- Given the previous status was `pending`, when the correction succeeds, then the `pending/` key is deleted and the file leaves the review queue.
|
||||
- Given the re-filing fails midway (S3 error), when the hook runs, then the save is rejected, the S3 state is restored (any partial copy is deleted), and the record and history are left unchanged.
|
||||
|
||||
- Given a document, when I edit one or more declared fields in the collections UI and save, then the `OnRecordUpdateRequest` hook on `documents` persists the new values as they are: no template evaluation, no re-filing, no `file_key` change, no status change.
|
||||
- Given a field edit is saved, when the record is persisted, then a `corrected` event is appended whose `before`/`after` diff (section-4 format) carries the changed fields only; `file_key` and `status` do not appear since they did not change.
|
||||
- Given a save where no declared field actually changed, when the hook runs, then no event is appended.
|
||||
- Given the save also changes the status, when the hook runs, then the status rules of US-23 apply on their own: `processed` → `validated` is appended as a separate `validated` event, anything else is refused.
|
||||
- Given a `pending` document, when fields are edited this way, then it stays `pending` and its object stays in `pending/`; leaving the review queue requires the full correction of the custom dashboard (US-3x), whose form shows all fields and the resulting name before saving.
|
||||
### US-23 — Validate a document from the standard PocketBase UI
|
||||
|
||||
As the user,
|
||||
@@ -226,10 +223,23 @@ so that the validation is traced as a `validated` event without a custom interfa
|
||||
Acceptance criteria:
|
||||
|
||||
- Given a `processed` document, when I set its status to `validated` in the collections UI and save, then the update hook appends a `validated` event with the section-4 payload (snapshots carrying the status change alone) and persists the new status.
|
||||
- Given any update that tries to change the status to something else than the allowed transition (`processed`→`validated` directly, any status→`validated` through a correction), when the hook runs, then the save is refused with an explicit error.
|
||||
- Given any update that tries a status change other than the allowed transition (`processed`→`validated`), when the hook runs, then the save is refused with an explicit error.
|
||||
- Given a status change to `validated`, when no field changed, then no re-filing occurs and the `file_key` is untouched.
|
||||
|
||||
### US-24 — Delete a document and its S3 object together
|
||||
|
||||
As the user,
|
||||
I want deleting a `documents` record from the standard PocketBase UI (or any PocketBase client) to also delete the underlying S3 object,
|
||||
so that the database and the bucket never contradict each other.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Given a `documents` record, when it is deleted, then the `OnRecordDeleteRequest` hook on `documents` deletes the S3 object at `file_key` before the record is removed; the cascade delete of the `document_events` relation (US-04) removes the history with it.
|
||||
- Given the S3 deletion fails (transient error, unreachable cluster), when the hook runs, then the record deletion is refused with an explicit error and both the record and the object are left in place.
|
||||
- Given the object at `file_key` is already gone from the bucket, when the hook runs, then the deletion is idempotent: the record is removed.
|
||||
- Given the bucket is versioned, when the object is deleted, then a delete marker keeps it recoverable; without versioning the deletion is final (same caveat as US-01).
|
||||
- Given a superuser issues the DELETE, when the hook runs, then the hook applies: like the append-only protection of US-04, it cannot rely on API rules alone.
|
||||
## Stories deferred to later epics
|
||||
|
||||
- US-3x (Custom review dashboard): file list with status, combined search (FTS5 + fields), detail with PDF preview streamed from S3, correction form pre-filled from the last event snapshot, rule dry-run. Correction and validation semantics are those of US-22/US-23; the dashboard is a convenience layer over the same hook.
|
||||
- US-3x (Custom review dashboard): file list with status, combined search (FTS5 + fields), detail with PDF preview streamed from S3, correction form pre-filled from the last event snapshot, rule dry-run. The dashboard owns the full correction: its form shows every field and the resulting file name (dry-run) before saving; saving a full correction re-files the object and sets the status to `validated` — the side effects US-22 deliberately keeps out of the standard UI. Validation remains that of US-23.
|
||||
- US-4x (FTS5): full-text index synchronization via hooks.
|
||||
|
||||
Reference in New Issue
Block a user