All files / src/server/api sessions.ts

94.73% Statements 18/19
83.33% Branches 5/6
90% Functions 9/10
94.73% Lines 18/19

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 91 92 93 94 95 96                39x       39x                       165x     165x               9x 9x                       109x             7x       29x               7x 7x       1x                   3x 3x 3x       3x 1x     2x      
import { z } from "zod";
import type { Session } from "@/types";
import type { Database } from "@/lib/db";
import {
  SessionRepository,
  CurrentSessionExistsError,
} from "@/server/repositories/implementations/SessionRepository";
 
const CreateSessionSchema = z.object({
  goal: z.string().optional(),
});
 
const UpdateSessionSchema = z.object({
  status: z.enum(["active", "paused", "completed", "abandoned"]).optional(),
  goal: z.string().optional(),
});
 
 
// Defined in the repository (where the unique-violation is caught) to keep the
// api -> repo dependency direction clean; re-exported here for route callers
// (and used directly by restoreSession's friendly pre-check below).
export { CurrentSessionExistsError };
 
export class SessionController {
  constructor(private sessionRepo: SessionRepository) {}
 
  static fromDb(db: Database): SessionController {
    return new SessionController(new SessionRepository(db));
  }
 
  async createSession(
    collectionId: string,
    curatorId: string,
    data: unknown,
  ): Promise<Session> {
    const parsed = CreateSessionSchema.parse(data);
    return this.sessionRepo.create({
      collectionId,
      curatorId,
      goal: parsed.goal,
    });
  }
 
  // Returns null for an absent session so the caller maps it to 404 WITHOUT
  // swallowing a real DB/infra error (a thrown getter caught blanketly would
  // collapse an outage into 404). Mirrors ItemController.getItem. (The old
  // throwing getSession was dropped — every caller migrated to this null getter.)
  async getSessionOrNull(sessionId: string): Promise<Session | null> {
    return this.sessionRepo.findById(sessionId);
  }
 
  // Like getSessionOrNull but reaches set-aside (soft-deleted) sessions too, so
  // the undo path can confirm a session exists before restoring it. Returns null
  // for an absent session (-> 404) without swallowing real DB/infra errors.
  async getSessionIncludingDeletedOrNull(sessionId: string): Promise<Session | null> {
    return this.sessionRepo.findByIdIncludingDeleted(sessionId);
  }
 
  async listByCollectionAndCurator(collectionId: string, curatorId: string): Promise<Session[]> {
    return this.sessionRepo.findByCollectionAndCurator(collectionId, curatorId);
  }
 
  async getByCollectionAndNumber(collectionId: string, number: number): Promise<Session | null> {
    return this.sessionRepo.findByCollectionAndNumber(collectionId, number);
  }
 
  async updateSession(sessionId: string, data: unknown): Promise<Session> {
    const parsed = UpdateSessionSchema.parse(data);
    return this.sessionRepo.update(sessionId, parsed);
  }
 
  async deleteSession(sessionId: string): Promise<void> {
    await this.sessionRepo.softDelete(sessionId);
  }
 
  // Undo "set aside" — re-activate a soft-deleted session. After migration 0009
  // the partial index frees the slot on set-aside, so a NEWER in-progress
  // session may already hold it; re-activating this one would put two
  // in-progress sessions in the slot. The friendly pre-check rejects that with a
  // gentle 409 BEFORE the UPDATE; the repo's restore() catch is the race
  // backstop if a session is created between this read and the UPDATE.
  async restoreSession(sessionId: string): Promise<Session> {
    const target = await this.sessionRepo.findByIdIncludingDeleted(sessionId);
    Eif (target) {
      const inProgress = await this.sessionRepo.findInProgress(
        target.collectionId,
        target.curatorId,
      );
      if (inProgress && inProgress.id !== sessionId) {
        throw new CurrentSessionExistsError();
      }
    }
    return this.sessionRepo.restore(sessionId);
  }
}