All files / src/server/api photos.ts

82.35% Statements 14/17
100% Branches 0/0
78.57% Functions 11/14
82.35% Lines 14/17

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                    115x 115x       115x       27x         31x       3x       41x                             3x       2x               6x 6x 6x               6x       3x              
import type { Database } from "@/lib/db";
import type { Photo } from "@/types";
import { PhotoRepository } from "@/server/repositories/implementations/PhotoRepository";
import { AuditLogRepository } from "@/server/repositories/implementations/AuditLogRepository";
import { withTransaction } from "@/lib/transactions";
import { recordPooledPhoto, type PooledPhotoInput } from "@/server/photos/recordPooledPhoto";
 
export class PhotoController {
  private photoRepo: PhotoRepository;
 
  constructor(private db: Database) {
    this.photoRepo = new PhotoRepository(db);
  }
 
  static fromDb(db: Database): PhotoController {
    return new PhotoController(db);
  }
 
  async findById(photoId: string): Promise<Photo | null> {
    return this.photoRepo.findById(photoId);
  }
 
  // `limit` opts into the bounded workspace preview (e.g. 6); omit for the full pool.
  async findPoolByCollection(collectionId: string, limit?: number): Promise<Photo[]> {
    return this.photoRepo.findPoolByCollection(collectionId, limit);
  }
 
  async findByItem(itemId: string): Promise<Photo[]> {
    return this.photoRepo.findByItem(itemId);
  }
 
  async findCoverPhotosByItemIds(itemIds: string[]): Promise<Map<string, Photo>> {
    return this.photoRepo.findCoverPhotosByItemIds(itemIds);
  }
 
  async findCoverPhotosByCollectionIds(collectionIds: string[]): Promise<Map<string, Photo>> {
    return this.photoRepo.findCoverPhotosByCollectionIds(collectionIds);
  }
 
  // Access-scoped pool cue for /collections: callers MUST pass the authenticated
  // user's already-access-filtered collection ids (from listCollections), never
  // raw/client ids — the repo method is bounded but not an access boundary.
  async findCollectionIdsWithPooledPhotos(collectionIds: string[]): Promise<Set<string>> {
    return this.photoRepo.findCollectionIdsWithPooledPhotos(collectionIds);
  }
 
  async update(photoId: string, data: Partial<Photo>): Promise<Photo> {
    return this.photoRepo.update(photoId, data);
  }
 
  async setHero(photoId: string, itemId: string): Promise<void> {
    await this.photoRepo.setHero(photoId, itemId);
  }
 
  /** Hard-deletes the photo row + records audit in one transaction. Returns blobKey for post-commit blob cleanup. */
  async deleteWithAudit(
    photo: Photo,
    actorId: string,
  ): Promise<{ blobKey: string }> {
    await withTransaction(this.db, async (tx) => {
      await new PhotoRepository(tx).delete(photo.id);
      await new AuditLogRepository(tx).record({
        actorId,
        action: "photo.delete",
        targetType: "photo",
        targetId: photo.id,
        collectionId: photo.collectionId,
      });
    });
    return { blobKey: photo.blobKey };
  }
 
  async create(data: Parameters<PhotoRepository["create"]>[0]): Promise<Photo> {
    return this.photoRepo.create(data);
  }
 
  async recordPooled(input: PooledPhotoInput): Promise<Photo> {
    return recordPooledPhoto(this.photoRepo, input);
  }
}