feat(documents): let the picker bring back a deleted document

The document picker showed a document deleted in HealthLog as "Deleted
in HealthLog" and would not let it be ticked, so a person who deleted an
imported document by mistake met a dead end exactly where they would look
for it again.

Such a row can now be ticked, and the import sends revive: true for it.
A document still in Recently deleted is restored through the shared
restore, nothing is fetched, and the row reads "Restored from Recently
deleted". One the purge already removed is fetched and stored again, and
only once it is stored does the ledger forget its key, so a failed fetch
leaves the deletion standing. Without revive the route answers "deleted"
as before, and the token upload has no such switch: a Paperless workflow
or the import script re-sending a deleted document is still refused,
whether it is in Recently deleted or long purged.

The search names the document a deleted key still points at (documentId,
null once purged), so the row can say "In Recently deleted" rather than
"Deleted in HealthLog". The outcome gains "restored", sent only in answer
to revive. The row logic moves to picker-rows.ts.

Refs #1275
This commit is contained in:
Marc-André Bombeck committed 2026-10-24 20:55:19 +02:00
1 parent 4752115d7d
commit 52fa5a06b1
22 files changed
+562 -103

No files matched your search

+13 -4
View File
@@ -67,7 +67,12 @@ const resultSchema = z
tags: z.array(z.string()),
sizeBytes: z.number().int().nullable(),
state: z.enum(["new", "imported", "deleted"]),
documentId: z.string().nullable(),
documentId: z
.string()
.nullable()
.describe(
"The vault document for `imported`. For `deleted`, the document while it is still in Recently deleted (restorable), null once it has been removed for good.",
),
})
.meta({ id: "DocumentSourceResult" });
@@ -81,7 +86,11 @@ const searchSchema = z
const importResultSchema = z
.object({
outcome: z.enum(["imported", "duplicate", "deleted"]),
outcome: z
.enum(["imported", "duplicate", "deleted", "restored"])
.describe(
"`restored` only answers a request that sent `revive: true`: the deleted document was still in Recently deleted and is back in the vault.",
),
documentId: z.string().nullable(),
linked: z.boolean(),
})
@@ -335,7 +344,7 @@ export const documentSourcePaths: NonNullable<ZodOpenApiObject["paths"]> = {
tags: ["Documents"],
summary: "Import one picked document",
description:
"`sourceId` must be an id the system issues (Paperless-ngx: digits; Papra: letters, digits, `_`, `-`), else 422 before anything is fetched. Fetches one document from the source and stores it in the vault through the same path as an upload, with `sourceSystem` / `sourceId` / `sourceInstance` (the connection's origin) provenance; the same id from another instance is another document. One import per key runs at a time; a concurrent second one waits and then answers `duplicate`. The key check counts toward the lookup allowance, the fetch toward the search allowance, and only a file that arrived toward the upload allowance. A source key the vault already holds is answered without a download: `duplicate` for a live document (same id), `deleted` for one the person deleted in HealthLog, which is never stored again. Bytes already in the vault under another key are `duplicate` too, and the key is remembered for that document. `link` files the document against the record the picker was opened from (a condition, a visit or a vaccination, which must be the caller's own and live; checked before anything is fetched), including a duplicate. `kind` sets the document type (default OTHER). Automatic AI reading follows the person's own setting. Counts against the person's upload allowance (60 an hour) and storage quota. 201 for a new document, 200 otherwise. Cookie session only; not delegable.",
"`sourceId` must be an id the system issues (Paperless-ngx: digits; Papra: letters, digits, `_`, `-`), else 422 before anything is fetched. Fetches one document from the source and stores it in the vault through the same path as an upload, with `sourceSystem` / `sourceId` / `sourceInstance` (the connection's origin) provenance; the same id from another instance is another document. One import per key runs at a time; a concurrent second one waits and then answers `duplicate`. The key check counts toward the lookup allowance, the fetch toward the search allowance, and only a file that arrived toward the upload allowance. A source key the vault already holds is answered without a download: `duplicate` for a live document (same id), `deleted` for one the person deleted in HealthLog, which is not stored again unless the request sends `revive: true`: then a document still in Recently deleted is restored (`restored`), and one already removed for good is fetched and stored again, its key leaving the import ledger. A token upload (`POST /api/documents/inbound`) has no such switch, so an importer never brings a deleted document back. Bytes already in the vault under another key are `duplicate` too, and the key is remembered for that document. `link` files the document against the record the picker was opened from (a condition, a visit or a vaccination, which must be the caller's own and live; checked before anything is fetched), including a duplicate. `kind` sets the document type (default OTHER). Automatic AI reading follows the person's own setting. Counts against the person's upload allowance (60 an hour) and storage quota. 201 for a new document, 200 otherwise. Cookie session only; not delegable.",
security: cookieOnly,
parameters: [systemParameter],
requestBody: {
@@ -358,7 +367,7 @@ export const documentSourcePaths: NonNullable<ZodOpenApiObject["paths"]> = {
},
"200": {
description:
"Already held (`duplicate`) or deleted in HealthLog (`deleted`).",
"Already held (`duplicate`), deleted in HealthLog (`deleted`), or brought back from Recently deleted (`restored`).",
content: {
"application/json": {
schema: dataEnvelope(