All files / src/server/api collections.ts

94.44% Statements 17/18
62.5% Branches 5/8
100% Functions 10/10
94.44% Lines 17/18

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 82 83 84 85 86 87 88 89 90            9x         9x               22x     22x       8x               3x             3x 3x 3x                 1x 1x     1x             2x 2x               2x             2x             1x      
import { z } from "zod";
import type { Collection } from "@/types";
import { generateId } from "@/lib/id";
import type { Database } from "@/lib/db";
import { CollectionRepository } from "@/server/repositories/implementations/CollectionRepository";
 
const CreateCollectionSchema = z.object({
  name: z.string().min(1),
  description: z.string().optional(),
});
 
const UpdateCollectionSchema = z.object({
  name: z.string().min(1).optional(),
  description: z.string().optional(),
  status: z.enum(["active", "archived", "deleted"]).optional(),
});
 
 
export class CollectionController {
  constructor(private collectionRepo: CollectionRepository) {}
 
  static fromDb(db: Database): CollectionController {
    return new CollectionController(new CollectionRepository(db));
  }
 
  async getIncludingDeleted(collectionId: string): Promise<Collection | null> {
    return this.collectionRepo.findByIdIncludingDeleted(collectionId);
  }
 
  async listCollections(userId: string): Promise<Collection[]> {
    // Owned ∪ collections where a membership row exists for the user (accepted
    // members — a row is created only on invitation-accept). Pending invitations
    // are deliberately NOT read, so an unaccepted invite never surfaces a
    // collection the user hasn't joined (IDOR-by-list).
    return this.collectionRepo.findByOwnerOrMember(userId);
  }
 
  async createCollection(
    userId: string,
    data: unknown,
  ): Promise<Collection> {
    const parsed = CreateCollectionSchema.parse(data);
    const id = generateId("coll");
    return this.collectionRepo.create({
      id,
      ownerId: userId,
      name: parsed.name,
      description: parsed.description,
    });
  }
 
  async getCollection(collectionId: string): Promise<Collection> {
    const collection = await this.collectionRepo.findById(collectionId);
    Iif (!collection) {
      throw new Error("Collection not found");
    }
    return collection;
  }
 
  async updateCollection(
    collectionId: string,
    data: unknown,
  ): Promise<Collection> {
    const parsed = UpdateCollectionSchema.parse(data);
    return this.collectionRepo.update(collectionId, {
      ...(parsed.name && { name: parsed.name }),
      ...(parsed.description && { description: parsed.description }),
      ...(parsed.status && { status: parsed.status }),
    });
  }
 
  async archiveCollection(collectionId: string): Promise<Collection> {
    return this.collectionRepo.update(collectionId, {
      status: "archived",
      updatedAt: new Date(),
    });
  }
 
  async restoreCollection(collectionId: string): Promise<Collection> {
    return this.collectionRepo.update(collectionId, {
      status: "active",
      updatedAt: new Date(),
    });
  }
 
  async deleteCollection(collectionId: string): Promise<Collection> {
    return this.collectionRepo.softDelete(collectionId);
  }
}