Files
liqa/server/src/session/session.service.ts
T
ars9 5cc16725fb chore(repo): restructure as monorepo with server and client workspaces
- move NestJS app into server/ subdirectory
- add client/ React+TypeScript (Vite) app with Hello World
- update docker-compose to build and run both services
- add root package.json declaring npm workspaces
- update .gitignore to cover node_modules and dist at all depths
2026-04-08 21:28:01 +03:00

144 lines
3.7 KiB
TypeScript

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<SessionEntity>,
) {}
async onApplicationBootstrap(): Promise<void> {
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<string, string>,
): Promise<SessionEntity> {
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<SessionEntity | null> {
return this.repo.findOneBy({ sessionName });
}
findById(id: number): Promise<SessionEntity | null> {
return this.repo.findOneBy({ id });
}
async markOpen(sessionName: string): Promise<void> {
await this.repo.update({ sessionName }, { status: "open" });
}
async markClosed(sessionName: string): Promise<void> {
await this.repo.update({ sessionName }, { status: "closed" });
}
async touchLastUsed(sessionName: string): Promise<void> {
await this.repo.update({ sessionName }, { lastUsedAt: new Date() });
}
findExpiredOpen(cutoff: Date): Promise<SessionEntity[]> {
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<SessionEntity[]> {
return this.repo.find({
where: { status: "closed", updatedAt: LessThan(cutoff) },
});
}
async findAll(
query: PaginationQueryDto<SessionOrderBy> = {},
): 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<void> {
await this.repo.delete(id);
}
}