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
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user