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);
}
}
|