What — two additions to the uploads module, promoting a pattern proven in a downstream consumer:
remove() becomes idempotent: a lookup that matches no record (e.g. a filename already deleted by a previous pass) is a debug-logged NO-OP returning { deletedCount: 0, notFound: true } — not a thrown error. A genuine GridFS bucket failure on an existing file still throws.
- New
sweepUnreferenced(kind, collection, paths, minAgeMs): sweep GridFS blobs of a given kind that are unreferenced by ANY of several reference paths on another collection (scalar or array-of-subdocuments paths, normalised before the OR check), respecting a minimum-age grace window so a blob written just before its referencing document persists is never swept.
Why — retention/cleanup jobs that re-run over the same window need remove() to treat "already gone" as success (otherwise every re-pass logs phantom errors). And purge() cannot express "referenced by path A OR B": its $lookup equality join is right for exactly one reference key; extending it would either lose the indexed join for existing callers or reintroduce per-row sub-queries. Multi-path referencing is generic (any consumer storing one blob referenced from several fields).
Scope — modules/uploads/repositories/uploads.repository.js (+ service wiring if the repo layer is not exported directly):
remove() no-op semantics as above (logger.debug, not warn).
sweepUnreferenced: ONE streaming pass over the referencing collection building an in-memory Set of referenced filenames across all paths, then one streaming pass over age-eligible candidate uploads checking Set membership. Deliberately a separate function from purge() (different, non-interchangeable query strategies — document that in the JSDoc).
- Unit tests: no-op remove (missing record) · genuine failure still throws · sweep respects the grace window · scalar path · array-of-subdocs path · blob referenced by only ONE of several paths is KEPT (the data-loss guard).
Scope: validated 2026-08-04
What — two additions to the uploads module, promoting a pattern proven in a downstream consumer:
remove()becomes idempotent: a lookup that matches no record (e.g. a filename already deleted by a previous pass) is a debug-logged NO-OP returning{ deletedCount: 0, notFound: true }— not a thrown error. A genuine GridFS bucket failure on an existing file still throws.sweepUnreferenced(kind, collection, paths, minAgeMs): sweep GridFS blobs of a givenkindthat are unreferenced by ANY of several reference paths on another collection (scalar or array-of-subdocuments paths, normalised before the OR check), respecting a minimum-age grace window so a blob written just before its referencing document persists is never swept.Why — retention/cleanup jobs that re-run over the same window need
remove()to treat "already gone" as success (otherwise every re-pass logs phantom errors). Andpurge()cannot express "referenced by path A OR B": its$lookupequality join is right for exactly one reference key; extending it would either lose the indexed join for existing callers or reintroduce per-row sub-queries. Multi-path referencing is generic (any consumer storing one blob referenced from several fields).Scope —
modules/uploads/repositories/uploads.repository.js(+ service wiring if the repo layer is not exported directly):remove()no-op semantics as above (logger.debug, not warn).sweepUnreferenced: ONE streaming pass over the referencing collection building an in-memory Set of referenced filenames across allpaths, then one streaming pass over age-eligible candidate uploads checking Set membership. Deliberately a separate function frompurge()(different, non-interchangeable query strategies — document that in the JSDoc).Scope: validated 2026-08-04