import { type ClickHouse } from "@internal/clickhouse"; import { type Tracer } from "@internal/tracing"; import { type Logger, type LogLevel } from "@trigger.dev/core/logger"; import { type Prisma } from "@trigger.dev/database"; import parseDuration from "parse-duration"; import { z } from "zod"; import { type PrismaClientOrTransaction } from "~/db.server"; import { startActiveSpan } from "~/v3/tracer.server"; import { ClickHouseSessionsRepository } from "./clickhouseSessionsRepository.server"; export type SessionsRepositoryOptions = { clickhouse: ClickHouse; prisma: PrismaClientOrTransaction; logger?: Logger; logLevel?: LogLevel; tracer?: Tracer; }; /** * Derived status values — `Session` rows don't have a stored status column. * `ACTIVE` is the base state; `CLOSED` means `closedAt` is set; `EXPIRED` * means `expiresAt` has passed. */ export const SessionStatus = z.enum(["ACTIVE", "CLOSED", "EXPIRED"]); export type SessionStatus = z.infer; /** * Legacy marker tag for sessions created from the Test/playground before the * `Session.isTest` boolean existed. New sessions set `isTest` instead; this tag * is hidden from the Tags display so it doesn't surface on pre-isTest rows. */ export const LEGACY_PLAYGROUND_TAG = "playground"; const SessionListInputOptionsSchema = z.object({ organizationId: z.string(), projectId: z.string(), environmentId: z.string(), // filters types: z.array(z.string()).optional(), tags: z.array(z.string()).optional(), taskIdentifiers: z.array(z.string()).optional(), externalId: z.string().optional(), statuses: z.array(SessionStatus).optional(), period: z.string().optional(), from: z.number().optional(), to: z.number().optional(), }); export type SessionListInputOptions = z.infer; export type SessionListInputFilters = Omit< SessionListInputOptions, "organizationId" | "projectId" | "environmentId" >; export type FilterSessionsOptions = Omit & { /** period converted to milliseconds duration */ period: number | undefined; }; type Pagination = { page: { size: number; cursor?: string; direction?: "forward" | "backward"; }; }; export type ListSessionsOptions = SessionListInputOptions & Pagination; type OffsetPagination = { offset: number; limit: number; }; export type SessionTagListOptions = { organizationId: string; projectId: string; environmentId: string; period?: string; from?: number; to?: number; /** Case-insensitive substring match on the tag name */ query?: string; } & OffsetPagination; export type SessionTagList = { tags: string[]; }; export type ListedSession = Prisma.SessionGetPayload<{ select: { id: true; friendlyId: true; externalId: true; type: true; taskIdentifier: true; isTest: true; tags: true; metadata: true; closedAt: true; closedReason: true; expiresAt: true; createdAt: true; updatedAt: true; runtimeEnvironmentId: true; currentRunId: true; }; }>; export type ISessionsRepository = { name: string; listSessionIds(options: ListSessionsOptions): Promise; listSessions(options: ListSessionsOptions): Promise<{ sessions: ListedSession[]; pagination: { nextCursor: string | null; previousCursor: string | null; }; }>; countSessions(options: SessionListInputOptions): Promise; listTags(options: SessionTagListOptions): Promise; }; export class SessionsRepository implements ISessionsRepository { private readonly clickHouseSessionsRepository: ClickHouseSessionsRepository; constructor(private readonly options: SessionsRepositoryOptions) { this.clickHouseSessionsRepository = new ClickHouseSessionsRepository(options); } get name() { return "sessionsRepository"; } async listSessionIds(options: ListSessionsOptions): Promise { return startActiveSpan( "sessionsRepository.listSessionIds", async () => this.clickHouseSessionsRepository.listSessionIds(options), { attributes: { "repository.name": "clickhouse", organizationId: options.organizationId, projectId: options.projectId, environmentId: options.environmentId, }, } ); } async listSessions(options: ListSessionsOptions) { return startActiveSpan( "sessionsRepository.listSessions", async () => this.clickHouseSessionsRepository.listSessions(options), { attributes: { "repository.name": "clickhouse", organizationId: options.organizationId, projectId: options.projectId, environmentId: options.environmentId, }, } ); } async countSessions(options: SessionListInputOptions) { return startActiveSpan( "sessionsRepository.countSessions", async () => this.clickHouseSessionsRepository.countSessions(options), { attributes: { "repository.name": "clickhouse", organizationId: options.organizationId, projectId: options.projectId, environmentId: options.environmentId, }, } ); } async listTags(options: SessionTagListOptions) { return startActiveSpan( "sessionsRepository.listTags", async () => this.clickHouseSessionsRepository.listTags(options), { attributes: { "repository.name": "clickhouse", organizationId: options.organizationId, projectId: options.projectId, environmentId: options.environmentId, }, } ); } } export function parseSessionListInputOptions(data: unknown): SessionListInputOptions { return SessionListInputOptionsSchema.parse(data); } export function convertSessionListInputOptionsToFilterOptions( options: SessionListInputOptions ): FilterSessionsOptions { return { ...options, period: options.period ? (parseDuration(options.period) ?? undefined) : undefined, }; }