Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | 17x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 13x 13x 13x 14x 13x 14x 13x | import type { Database } from "@/lib/db";
import type { Collection } from "@/types";
import { withTransaction } from "@/lib/transactions";
import { lockCollectionCurators } from "@/server/services/purge-lock";
import { del } from "@vercel/blob";
import { deleteVariants, variantPrefix } from "@/lib/photo-variants";
import { SortResultShareRepository } from "@/server/repositories/implementations/SortResultShareRepository";
import { SortResultItemRepository } from "@/server/repositories/implementations/SortResultItemRepository";
import { SortResultRepository } from "@/server/repositories/implementations/SortResultRepository";
import { DecisionEventRepository } from "@/server/repositories/implementations/DecisionEventRepository";
import { SessionRepository } from "@/server/repositories/implementations/SessionRepository";
import { CollectionPreferenceItemRepository } from "@/server/repositories/implementations/CollectionPreferenceItemRepository";
import { ItemDispositionRepository } from "@/server/repositories/implementations/ItemDispositionRepository";
import { PhotoRepository } from "@/server/repositories/implementations/PhotoRepository";
import { ItemRepository } from "@/server/repositories/implementations/ItemRepository";
import { InvitationRepository } from "@/server/repositories/implementations/InvitationRepository";
import { MembershipRepository } from "@/server/repositories/implementations/MembershipRepository";
import { CollectionRepository } from "@/server/repositories/implementations/CollectionRepository";
import { AuditLogRepository } from "@/server/repositories/implementations/AuditLogRepository";
/**
* Permanent owner-decided erasure of a WHOLE collection. The route has resolved
* the collection (incl. soft-deleted) and gated the caller as its owner; this
* service does the destructive work. Total-erase: every curator's sessions,
* preferences, and sort results (draft + finalized) go.
*
* Children-first hard delete in ONE transaction (no FK has ON DELETE CASCADE),
* then a collection.purge audit row in the SAME tx (a committed purge always
* carries its actor record). Blobs are del'd best-effort AFTER commit.
*
* Invariant: inside the transaction, EVERY write goes through a tx-bound repo
* (`new XRepository(tx)`), never an injected/`this.db` repo — a non-tx write
* would not roll back with the transaction.
*/
export class CollectionPurgeService {
constructor(private db: Database) {}
async purge(collection: Collection, actorId: string): Promise<void> {
const blobKeys = await withTransaction(this.db, async (tx) => {
await lockCollectionCurators(tx, collection.id);
await new SortResultShareRepository(tx).deleteByCollection(collection.id);
await new SortResultItemRepository(tx).deleteByCollection(collection.id);
await new SortResultRepository(tx).deleteByCollection(collection.id);
await new DecisionEventRepository(tx).deleteByCollection(collection.id);
await new SessionRepository(tx).deleteByCollection(collection.id);
await new CollectionPreferenceItemRepository(tx).deleteByCollection(collection.id);
// No ON DELETE CASCADE on the item_dispositions FK — clear it before the
// items hard-delete or that delete throws a 23503.
await new ItemDispositionRepository(tx).deleteByCollection(collection.id);
const keys = await new PhotoRepository(tx).hardDeleteByCollection(collection.id);
const itemsDeleted = await new ItemRepository(tx).hardDeleteByCollection(collection.id);
await new InvitationRepository(tx).deleteByCollection(collection.id);
await new MembershipRepository(tx).deleteByCollection(collection.id);
await new CollectionRepository(tx).hardDelete(collection.id);
await new AuditLogRepository(tx).record({
actorId,
action: "collection.purge",
targetType: "collection",
targetId: collection.id,
collectionId: collection.id,
metadata: { photosDeleted: keys.length, itemsDeleted },
});
return keys;
});
// Best-effort, post-commit. Only http(s) keys are real blob URLs; a del
// rejection is logged, not fatal — the DB purge already committed.
const results = await Promise.allSettled(
blobKeys.filter((k) => k.startsWith("http")).map((k) => del(k)),
);
for (const r of results) {
Iif (r.status === "rejected") {
console.error("CollectionPurgeService: blob del failed (orphaned, sweepable)", r.reason);
}
}
// Best-effort: sweep every persisted resized variant for the collection.
await deleteVariants(variantPrefix(collection.id));
}
}
|