import { Injectable, OnApplicationBootstrap } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { LessThan, Repository } from "typeorm"; import { SessionEntity } from "./session.entity"; import { TraceLogger } from "../common/trace-logger"; import type { Cookie } from "playwright"; import { PaginationQueryDto, PaginatedResult, } from "../common/dto/pagination.dto"; export type SessionOrderBy = | "id" | "sessionName" | "status" | "lastUsedAt" | "createdAt" | "updatedAt"; @Injectable() export class SessionService implements OnApplicationBootstrap { private readonly logger = new TraceLogger(SessionService.name); constructor( @InjectRepository(SessionEntity) private readonly repo: Repository, ) {} async onApplicationBootstrap(): Promise { const result = await this.repo.update( { status: "open" }, { status: "closed" }, ); if ((result.affected ?? 0) > 0) { this.logger.log( `${result.affected} open session(s) closed on startup (no Playwright context available)`, ); } } async upsert( sessionName: string, token: string, cookies: Cookie[], localStorage: Record, ): Promise { const now = new Date(); const existing = await this.repo.findOneBy({ sessionName }); if (existing) { existing.token = token; existing.cookies = JSON.stringify(cookies); existing.localStorage = JSON.stringify(localStorage); existing.status = "open"; existing.lastUsedAt = now; return this.repo.save(existing); } return this.repo.save( this.repo.create({ sessionName, token, cookies: JSON.stringify(cookies), localStorage: JSON.stringify(localStorage), status: "open", lastUsedAt: now, }), ); } findBySessionName(sessionName: string): Promise { return this.repo.findOneBy({ sessionName }); } findById(id: number): Promise { return this.repo.findOneBy({ id }); } async markOpen(sessionName: string): Promise { await this.repo.update({ sessionName }, { status: "open" }); } async markClosed(sessionName: string): Promise { await this.repo.update({ sessionName }, { status: "closed" }); } async touchLastUsed(sessionName: string): Promise { await this.repo.update({ sessionName }, { lastUsedAt: new Date() }); } findExpiredOpen(cutoff: Date): Promise { return this.repo .createQueryBuilder("s") .where("s.status = :status", { status: "open" }) .andWhere("(s.lastUsedAt IS NULL OR s.lastUsedAt < :cutoff)", { cutoff }) .getMany(); } findOldClosed(cutoff: Date): Promise { return this.repo.find({ where: { status: "closed", updatedAt: LessThan(cutoff) }, }); } async findAll( query: PaginationQueryDto = {}, ): Promise< PaginatedResult< Pick< SessionEntity, | "id" | "sessionName" | "status" | "lastUsedAt" | "createdAt" | "updatedAt" > > > { const page = query.page ?? 1; const limit = query.limit ?? 20; const orderBy = query.orderBy ?? "id"; const orderDir = query.orderDir ?? "ASC"; const [data, total] = await this.repo.findAndCount({ select: [ "id", "sessionName", "status", "lastUsedAt", "createdAt", "updatedAt", ], order: { [orderBy]: orderDir }, skip: (page - 1) * limit, take: limit, }); return { data, total, page, limit }; } async remove(id: number): Promise { await this.repo.delete(id); } }