refactor: added new logic for soft-delete#265
Conversation
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Polliog
left a comment
There was a problem hiding this comment.
Review: soft-delete projects
Solid approach overall. The service queries are consistently re-scoped with deleted_at IS NULL, the soft-delete/restore updates are atomic and guarded, and dropping ON DELETE CASCADE in favor of the controlled purge path is the right call. No cross-tenant scoping issues found. A few things should be addressed before merge.
Changes required
1. verifyApiKey does not filter deleted_at — a "deleted" project keeps accepting ingestion for 30 days
packages/backend/src/modules/api-keys/service.ts (~L118): the api_keys -> projects join in the hot auth path has no deleted_at IS NULL filter. An API key for a soft-deleted project stays fully valid, so logs/spans/metrics keep flowing for the whole grace window, accrue against storage/metering, and then get purged. Deleting a project no longer stops writes.
- Add
.where('projects.deleted_at', 'is', null)to the verify query. - Invalidate the
CacheManager.apiKeyKey(...)entries on soft-delete. The cached verification survives until TTL, so even after hard-delete a cached key resolves to a now-removedproject_idand ingestion inserts hit an FK violation for a few seconds.
2. Control-plane callers don't check deletedAt even though getProjectById now returns soft-deleted rows
getProjectById intentionally returns deleted projects (per the doc comment), but the api-keys routes (packages/backend/src/modules/api-keys/routes.ts L41/L68/L125) only check if (!project). As a result you can list/create/revoke API keys on a trashed project. Add an explicit deletedAt guard at these call sites (or expose a getActiveProjectById helper for callers that want active-only).
3. Purge worker can leave a project stuck un-purged on TimescaleDB
packages/backend/src/worker.ts (~L643) + migration 051: with CASCADE removed, the FK becomes RESTRICT. The worker runs reservoir.purgeProject() and then db.deleteFrom('projects'). If deleteByTimeRange doesn't remove every row (compressed TimescaleDB chunks that a plain DELETE can't touch, or rows outside the [1970, 2100) window), the projects delete fails the FK constraint, gets swallowed by the catch, and the project stays soft-deleted forever with only a console.error. Please confirm deleteByTimeRange decompresses / uses the same chunk strategy as retention cleanup, and consider surfacing an alert when a project repeatedly fails to purge past its grace window.
Minor (non-blocking)
packages/backend/src/modules/projects/service.ts(~L368):orderBy('deleted_at', 'asc')with the comment "active (null) first" is wrong. Postgres ordersNULLS LASTforASC, so active projects sort last. No practical impact (the frontend re-splits bydeletedAt), but either useasc nulls firstor fix the comment.- The
deletedAtcheck in the/restoreroute (routes.ts~L165) is duplicated insiderestoreProject— harmless redundancy.
|
Giustino Gragnaniello seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account. You have signed the CLA already but the status is still pending? Let us recheck it. |
Summary
Tenant safety
LogTide is multi-tenant. Confirm the following for any new/changed query, endpoint,
or background job (see
docs/security/tenant-isolation-audit.md):organization_id(andproject_idwhere relevant).npm run check:tenant-scopingpasses (run frompackages/backend).Testing