chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:32:57 +08:00
commit cd420f9332
4811 changed files with 884702 additions and 0 deletions
@@ -0,0 +1,520 @@
import { type ClickhouseQueryBuilder } from "@internal/clickhouse";
import { ErrorId, RunId } from "@trigger.dev/core/v3/isomorphic";
import {
type FilterRunsOptions,
type IRunsRepository,
type ListRunsOptions,
type RunIdsPage,
type RunListInputOptions,
type RunsRepositoryOptions,
type TagListOptions,
convertRunListInputOptionsToFilterRunsOptions,
} from "./runsRepository.server";
import parseDuration from "parse-duration";
import { decodeRunsCursor, encodeRunsCursor } from "./runsCursor.server";
import { runStore } from "~/v3/runStore.server";
import { type PrismaClientOrTransaction } from "~/db.server";
type RunCursorRow = { runId: string; createdAt: number };
/**
* Hydrates a set of rows for a ClickHouse-derived run-id set against the given
* read client. The closure MUST select `id` so `#hydrateRunsByIds` can key
* set-membership and re-impose ordering; the call site projects `id` away if its
* result type excludes it.
*/
type HydrateFn<T extends { id: string }> = (
client: PrismaClientOrTransaction,
ids: string[]
) => Promise<T[]>;
export class ClickHouseRunsRepository implements IRunsRepository {
constructor(private readonly options: RunsRepositoryOptions) {}
get name() {
return "clickhouse";
}
async runExistsInEnvironment(options: {
organizationId: string;
projectId: string;
environmentId: string;
createdAtLowerBoundMs?: number;
}): Promise<boolean> {
const queryBuilder = this.options.clickhouse.taskRuns.existsQueryBuilder();
queryBuilder
.where("organization_id = {organizationId: String}", {
organizationId: options.organizationId,
})
.where("project_id = {projectId: String}", { projectId: options.projectId })
.where("environment_id = {environmentId: String}", { environmentId: options.environmentId });
if (typeof options.createdAtLowerBoundMs === "number") {
queryBuilder.where("created_at >= fromUnixTimestamp64Milli({createdAtLowerBound: Int64})", {
createdAtLowerBound: options.createdAtLowerBoundMs,
});
}
queryBuilder.limit(1);
const [queryError, result] = await queryBuilder.execute();
if (queryError) {
throw queryError;
}
return (result?.length ?? 0) > 0;
}
/**
* Runs the keyset-paginated query and returns `{ runId, createdAt }` rows
* (one extra beyond `page.size` to signal "has more"). The ordering is always
* the composite `(created_at, run_id)`; the cursor predicate must match it.
*
* Composite cursors carry both components, so we cut on the
* `(created_at, run_id)` tuple — sound regardless of how run_id order relates
* to created_at order. Legacy bare-run_id cursors fall back to the old
* `run_id`-only predicate (knowingly unsound) for backwards compatibility
* with in-flight cursors.
*/
private async listRunRows(options: ListRunsOptions): Promise<RunCursorRow[]> {
const queryBuilder = this.options.clickhouse.taskRuns.queryBuilder();
applyRunFiltersToQueryBuilder(
queryBuilder,
await convertRunListInputOptionsToFilterRunsOptions(
options,
this.options.prisma,
this.options.runStore ?? runStore
)
);
const forward = options.page.direction === "forward" || !options.page.direction;
if (options.page.cursor) {
const decoded = decodeRunsCursor(options.page.cursor);
if (forward) {
if (decoded.kind === "composite") {
queryBuilder.where(
"(created_at, run_id) < (fromUnixTimestamp64Milli({cursorCreatedAt: Int64}), {runId: String})",
{ cursorCreatedAt: decoded.createdAt, runId: decoded.runId }
);
} else {
queryBuilder.where("run_id < {runId: String}", { runId: decoded.runId });
}
queryBuilder.orderBy("created_at DESC, run_id DESC");
} else {
if (decoded.kind === "composite") {
queryBuilder.where(
"(created_at, run_id) > (fromUnixTimestamp64Milli({cursorCreatedAt: Int64}), {runId: String})",
{ cursorCreatedAt: decoded.createdAt, runId: decoded.runId }
);
} else {
queryBuilder.where("run_id > {runId: String}", { runId: decoded.runId });
}
queryBuilder.orderBy("created_at ASC, run_id ASC");
}
queryBuilder.limit(options.page.size + 1);
} else {
// Initial page - no cursor provided
queryBuilder.orderBy("created_at DESC, run_id DESC").limit(options.page.size + 1);
}
const [queryError, result] = await queryBuilder.execute();
if (queryError) {
throw queryError;
}
return result.map((row) => ({ runId: row.run_id, createdAt: row.created_at_ms }));
}
/**
* A keyset-paginated page of run ids ordered by `(created_at, run_id)`, plus
* the cursors to page forward/backward. Cursors are composite tokens that
* match the ordering, so pagination can't duplicate or skip runs even when
* run_id order diverges from created_at order. This is the single source of
* cursor construction — `listRuns` and bulk actions both build on it.
*/
async listRunIds(options: ListRunsOptions): Promise<RunIdsPage> {
const rows = await this.listRunRows(options);
// listRunRows fetches one extra row beyond page.size to detect "has more".
const hasMore = rows.length > options.page.size;
const cursorFor = (row: RunCursorRow | undefined): string | null =>
row ? encodeRunsCursor(row.createdAt, row.runId) : null;
let nextCursor: string | null = null;
let previousCursor: string | null = null;
const direction = options.page.direction ?? "forward";
switch (direction) {
case "forward": {
previousCursor = options.page.cursor ? cursorFor(rows.at(0)) : null;
if (hasMore) {
// The next cursor is the last run on this page.
nextCursor = cursorFor(rows[options.page.size - 1]);
}
break;
}
case "backward": {
const reversedRows = [...rows].reverse();
if (hasMore) {
previousCursor = cursorFor(reversedRows.at(1));
nextCursor = cursorFor(reversedRows.at(options.page.size));
} else {
// No newer rows, so there's no previous (newer) page. The next
// (older) cursor is the oldest row on this page = rows[0] (rows are
// ASC here). Index by the actual row count, not page.size — on a
// partial page (fewer than page.size rows) page.size-1 overshoots
// and would null the cursor, stranding forward navigation.
nextCursor = cursorFor(rows.at(0));
}
break;
}
}
// The page is always the first `page.size` rows of the result. listRunRows
// fetches one extra row only to detect `hasMore`; that extra row is the
// farthest from the cursor in BOTH directions (forward orders DESC, backward
// orders ASC), so it's always the trailing element to drop — never the
// leading one. (Slicing `[1, size+1]` for backward dropped the row closest
// to the cursor and kept the has-more sentinel, straddling two pages.)
const runIds = rows.slice(0, options.page.size).map((row) => row.runId);
return { runIds, pagination: { nextCursor, previousCursor } };
}
/**
* Hydrates a ClickHouse-derived run-id set from the run-ops store.
* Split ON: new run-ops client first, then the LEGACY RUN-OPS READ REPLICA ONLY
* for ids not known-migrated — never the legacy primary. The mixed-residency
* fan-out lives here because `RoutingRunStore.findRuns` punts it.
* Split OFF (single-DB / self-host): one plain `store.findRuns(args, prisma)`
* (passthrough) — no legacy read, no known-migrated probe, no second connection.
*/
async #hydrateRunsByIds<T extends { id: string }>(
runIds: string[],
hydrate: HydrateFn<T>
): Promise<T[]> {
if (runIds.length === 0) {
return [];
}
const splitEnabled = this.options.readThrough?.splitEnabled ?? false;
let rows: T[];
if (!splitEnabled) {
rows = await hydrate(this.options.prisma, runIds);
} else {
const newClient = this.options.readThrough?.newClient ?? this.options.prisma;
const legacyReplica = this.options.readThrough?.legacyReplica ?? this.options.prisma;
const newRows = await hydrate(newClient, runIds);
const foundIds = new Set(newRows.map((r) => r.id));
const missing = runIds.filter((id) => !foundIds.has(id));
// Any id not hydrated from the new store is probed on the legacy replica.
const toProbeLegacy = missing;
const legacyRows = toProbeLegacy.length ? await hydrate(legacyReplica, toProbeLegacy) : [];
rows = [...newRows, ...legacyRows];
}
// Preserve the ClickHouse keyset order (created_at desc, run_id desc) by re-ordering the
// hydrated rows to match the input `runIds`. Sorting by raw `id` was only ~chronological
// when every id was a time-prefixed cuid; a mixed cuid/run-ops id page sorts the two id-spaces
// into separate blocks, burying recent runs. Rows whose PG row is gone (e.g. past
// retention) drop out, exactly as before.
const byId = new Map(rows.map((r) => [r.id, r] as const));
return runIds.map((id) => byId.get(id)).filter((r): r is T => r !== undefined);
}
async listFriendlyRunIds(options: ListRunsOptions) {
// First get internal IDs from ClickHouse
const { runIds } = await this.listRunIds(options);
if (runIds.length === 0) {
return [];
}
const store = this.options.runStore ?? runStore;
// Then get friendly IDs from the run-ops store (id added for set-membership;
// projected away below so the returned shape stays `string[]`).
const runs = await this.#hydrateRunsByIds(runIds, (client, ids) =>
store.findRuns(
{
where: { id: { in: ids } },
select: { id: true, friendlyId: true },
},
client
)
);
return runs.map((run) => run.friendlyId);
}
async listRuns(options: ListRunsOptions) {
const { runIds, pagination } = await this.listRunIds(options);
const store = this.options.runStore ?? runStore;
let runs = await this.#hydrateRunsByIds(runIds, (client, ids) =>
store.findRuns(
{
where: {
id: {
in: ids,
},
},
orderBy: {
id: "desc",
},
select: {
id: true,
friendlyId: true,
taskIdentifier: true,
taskVersion: true,
runtimeEnvironmentId: true,
status: true,
createdAt: true,
startedAt: true,
lockedAt: true,
delayUntil: true,
updatedAt: true,
completedAt: true,
isTest: true,
spanId: true,
idempotencyKey: true,
ttl: true,
expiredAt: true,
costInCents: true,
baseCostInCents: true,
usageDurationMs: true,
runTags: true,
depth: true,
rootTaskRunId: true,
batchId: true,
metadata: true,
metadataType: true,
machinePreset: true,
queue: true,
workerQueue: true,
region: true,
annotations: true,
},
},
client
)
);
// ClickHouse is slightly delayed, so we're going to do in-memory status filtering too
if (options.statuses && options.statuses.length > 0) {
runs = runs.filter((run) => options.statuses!.includes(run.status));
}
return {
runs,
pagination,
};
}
async countRuns(options: RunListInputOptions) {
const queryBuilder = this.options.clickhouse.taskRuns.countQueryBuilder();
applyRunFiltersToQueryBuilder(
queryBuilder,
await convertRunListInputOptionsToFilterRunsOptions(
options,
this.options.prisma,
this.options.runStore ?? runStore
)
);
const [queryError, result] = await queryBuilder.execute();
if (queryError) {
throw queryError;
}
if (result.length === 0) {
throw new Error("No count rows returned");
}
return result[0].count;
}
async listTags(options: TagListOptions) {
const queryBuilder = this.options.clickhouse.taskRuns
.tagQueryBuilder()
.where("organization_id = {organizationId: String}", {
organizationId: options.organizationId,
})
.where("project_id = {projectId: String}", {
projectId: options.projectId,
})
.where("environment_id = {environmentId: String}", {
environmentId: options.environmentId,
});
const periodMs = options.period ? (parseDuration(options.period) ?? undefined) : undefined;
if (periodMs) {
queryBuilder.where("created_at >= fromUnixTimestamp64Milli({period: Int64})", {
period: new Date(Date.now() - periodMs).getTime(),
});
}
if (options.from) {
queryBuilder.where("created_at >= fromUnixTimestamp64Milli({from: Int64})", {
from: options.from,
});
}
if (options.to) {
queryBuilder.where("created_at <= fromUnixTimestamp64Milli({to: Int64})", { to: options.to });
}
// Filter by query (case-insensitive contains search)
if (options.query && options.query.trim().length > 0) {
queryBuilder.where("positionCaseInsensitiveUTF8(tag, {query: String}) > 0", {
query: options.query,
});
}
// Add ordering and pagination
queryBuilder.orderBy("tag ASC").limit(options.limit);
const [queryError, result] = await queryBuilder.execute();
if (queryError) {
throw queryError;
}
return {
tags: result.map((row) => row.tag),
};
}
}
function applyRunFiltersToQueryBuilder<T>(
queryBuilder: ClickhouseQueryBuilder<T>,
options: FilterRunsOptions
) {
queryBuilder
.where("organization_id = {organizationId: String}", {
organizationId: options.organizationId,
})
.where("project_id = {projectId: String}", {
projectId: options.projectId,
})
.where("environment_id = {environmentId: String}", {
environmentId: options.environmentId,
});
if (options.tasks && options.tasks.length > 0) {
queryBuilder.where("task_identifier IN {tasks: Array(String)}", { tasks: options.tasks });
}
if (options.versions && options.versions.length > 0) {
queryBuilder.where("task_version IN {versions: Array(String)}", {
versions: options.versions,
});
}
if (options.statuses && options.statuses.length > 0) {
queryBuilder.where("status IN {statuses: Array(String)}", { statuses: options.statuses });
}
if (options.tags && options.tags.length > 0) {
// Both hasAny and hasAll are served by the tags bloom_filter skip index.
const tagsFn = options.tagsMatch === "all" ? "hasAll" : "hasAny";
queryBuilder.where(`${tagsFn}(tags, {tags: Array(String)})`, { tags: options.tags });
}
if (options.scheduleId) {
queryBuilder.where("schedule_id = {scheduleId: String}", { scheduleId: options.scheduleId });
}
// Period is a number of milliseconds duration
if (options.period) {
queryBuilder.where("created_at >= fromUnixTimestamp64Milli({period: Int64})", {
period: new Date(Date.now() - options.period).getTime(),
});
}
if (options.from) {
queryBuilder.where("created_at >= fromUnixTimestamp64Milli({from: Int64})", {
from: options.from,
});
}
if (options.to) {
queryBuilder.where("created_at <= fromUnixTimestamp64Milli({to: Int64})", { to: options.to });
}
if (typeof options.isTest === "boolean") {
queryBuilder.where("is_test = {isTest: Boolean}", { isTest: options.isTest });
}
if (options.rootOnly) {
queryBuilder.where("root_run_id = ''");
}
if (options.batchId) {
queryBuilder.where("batch_id = {batchId: String}", { batchId: options.batchId });
}
if (options.bulkId) {
queryBuilder.where("hasAny(bulk_action_group_ids, {bulkActionGroupIds: Array(String)})", {
bulkActionGroupIds: [options.bulkId],
});
}
if (options.runId && options.runId.length > 0) {
// it's important that in the query it's "runIds", otherwise it clashes with the cursor which is called "runId"
queryBuilder.where("friendly_id IN {runIds: Array(String)}", {
runIds: options.runId.map((runId) => RunId.toFriendlyId(runId)),
});
}
if (options.queues && options.queues.length > 0) {
queryBuilder.where("queue IN {queues: Array(String)}", { queues: options.queues });
}
if (options.regions && options.regions.length > 0) {
queryBuilder.where("if(region != '', region, worker_queue) IN {regions: Array(String)}", {
regions: options.regions,
});
}
if (options.machines && options.machines.length > 0) {
queryBuilder.where("machine_preset IN {machines: Array(String)}", {
machines: options.machines,
});
}
if (options.errorId) {
queryBuilder.where("error_fingerprint = {errorFingerprint: String}", {
errorFingerprint: ErrorId.toId(options.errorId),
});
}
if (options.taskKinds && options.taskKinds.length > 0) {
const includesStandard = options.taskKinds.includes("STANDARD");
// Include empty string when filtering for STANDARD (default value for pre-existing runs)
const effectiveKinds = includesStandard ? [...options.taskKinds, ""] : options.taskKinds;
if (effectiveKinds.length === 1) {
queryBuilder.where("task_kind = {taskKind: String}", {
taskKind: effectiveKinds[0]!,
});
} else {
queryBuilder.where("task_kind IN {taskKinds: Array(String)}", {
taskKinds: effectiveKinds,
});
}
}
}
@@ -0,0 +1,47 @@
/**
* Cursor encoding for keyset pagination over `(created_at, run_id)`.
*
* The list query orders by the composite key `(created_at, run_id)`, so a sound
* cursor must carry BOTH components — cutting on `run_id` alone re-includes and
* skips rows whenever `run_id` order diverges from `created_at` order.
*
* A cursor is an opaque URL-safe base64 token wrapping `{ c: createdAtMs, r:
* runId }`. Cursors are server-issued (the SDK just echoes
* `pagination.next`/`previous` back), so this format needs no client update.
*
* Legacy cursors were the bare internal run_id (a cuid). They are detected by
* decode failure: a cuid base64-decodes to non-JSON bytes, so it falls through
* to `{ kind: "legacy" }` and the old (knowingly unsound) `run_id`-only
* predicate. In-flight legacy cursors keep working and drain naturally.
*/
import { z } from "zod";
export type DecodedRunsCursor =
| { kind: "composite"; createdAt: number; runId: string }
| { kind: "legacy"; runId: string };
// `c` = created_at (ms since epoch), `r` = run_id. Short keys keep the token small.
const CompositeCursor = z.object({
c: z.number().int(),
r: z.string().min(1),
});
export function encodeRunsCursor(createdAtMs: number, runId: string): string {
return Buffer.from(JSON.stringify({ c: createdAtMs, r: runId })).toString("base64url");
}
export function decodeRunsCursor(cursor: string): DecodedRunsCursor {
try {
const parsed = CompositeCursor.safeParse(
JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"))
);
if (parsed.success) {
return { kind: "composite", createdAt: parsed.data.c, runId: parsed.data.r };
}
} catch {
// JSON.parse threw — not a composite cursor.
}
return { kind: "legacy", runId: cursor };
}
@@ -0,0 +1,370 @@
import { type ClickHouse } from "@internal/clickhouse";
import { type RunStore } from "@internal/run-store";
import { type Tracer } from "@internal/tracing";
import { type Logger, type LogLevel } from "@trigger.dev/core/logger";
import { MachinePresetName } from "@trigger.dev/core/v3";
import { BulkActionId, RunId } from "@trigger.dev/core/v3/isomorphic";
import { type Prisma, TaskRunStatus } from "@trigger.dev/database";
import parseDuration from "parse-duration";
import { z } from "zod";
import { timeFilters } from "~/components/runs/v3/SharedFilters";
import { type PrismaClientOrTransaction } from "~/db.server";
import { runStore as defaultRunStore } from "~/v3/runStore.server";
import { startActiveSpan } from "~/v3/tracer.server";
import { ClickHouseRunsRepository } from "./clickhouseRunsRepository.server";
export type RunsRepositoryOptions = {
clickhouse: ClickHouse;
prisma: PrismaClientOrTransaction;
logger?: Logger;
logLevel?: LogLevel;
tracer?: Tracer;
// Injectable run-ops store; defaults to the `~/v3/runStore.server` singleton
// (passthrough). The list-hydrate fan-out below does not depend on the store
// routing mixed-residency id sets — it applies the read-through fan-out itself.
runStore?: RunStore;
// Run-ops read-through wiring for the list hydrate. Omitted => passthrough.
readThrough?: {
// `legacyReplica` is a READ REPLICA handle only — there is no legacy-primary field.
newClient?: PrismaClientOrTransaction;
legacyReplica?: PrismaClientOrTransaction;
// Resolved boot constant; when false the split branch is never entered.
splitEnabled?: boolean;
};
};
const RunStatus = z.enum(Object.values(TaskRunStatus) as [TaskRunStatus, ...TaskRunStatus[]]);
const RunListInputOptionsSchema = z.object({
organizationId: z.string(),
projectId: z.string(),
environmentId: z.string(),
//filters
tasks: z.array(z.string()).optional(),
versions: z.array(z.string()).optional(),
statuses: z.array(RunStatus).optional(),
tags: z.array(z.string()).optional(),
// "any" (default) = run has at least one of `tags`; "all" = run has every tag.
tagsMatch: z.enum(["any", "all"]).optional(),
scheduleId: z.string().optional(),
period: z.string().optional(),
from: z.number().optional(),
to: z.number().optional(),
isTest: z.boolean().optional(),
rootOnly: z.boolean().optional(),
batchId: z.string().optional(),
runId: z.array(z.string()).optional(),
bulkId: z.string().optional(),
queues: z.array(z.string()).optional(),
regions: z.array(z.string()).optional(),
machines: MachinePresetName.array().optional(),
errorId: z.string().optional(),
taskKinds: z.array(z.string()).optional(),
});
export type RunListInputOptions = z.infer<typeof RunListInputOptionsSchema>;
export type RunListInputFilters = Omit<
RunListInputOptions,
"organizationId" | "projectId" | "environmentId"
>;
export type ParsedRunFilters = RunListInputFilters & {
cursor?: string;
direction?: "forward" | "backward";
sources?: string[];
};
export type FilterRunsOptions = Omit<RunListInputOptions, "period"> & {
period: number | undefined;
};
type Pagination = {
page: {
size: number;
cursor?: string;
direction?: "forward" | "backward";
};
};
type OffsetPagination = {
offset: number;
limit: number;
};
export type ListedRun = Prisma.TaskRunGetPayload<{
select: {
id: true;
friendlyId: true;
taskIdentifier: true;
taskVersion: true;
runtimeEnvironmentId: true;
status: true;
createdAt: true;
startedAt: true;
lockedAt: true;
delayUntil: true;
updatedAt: true;
completedAt: true;
isTest: true;
spanId: true;
idempotencyKey: true;
ttl: true;
expiredAt: true;
costInCents: true;
baseCostInCents: true;
usageDurationMs: true;
runTags: true;
depth: true;
rootTaskRunId: true;
batchId: true;
metadata: true;
metadataType: true;
machinePreset: true;
queue: true;
workerQueue: true;
region: true;
annotations: true;
};
}>;
export type ListRunsOptions = RunListInputOptions & Pagination;
export type TagListOptions = {
organizationId: string;
projectId: string;
environmentId: string;
period?: string;
from?: number;
to?: number;
/** Performs a case insensitive contains search on the tag name */
query?: string;
} & OffsetPagination;
export type TagList = {
tags: string[];
};
export type CursorPagination = {
nextCursor: string | null;
previousCursor: string | null;
};
export type RunIdsPage = {
runIds: string[];
pagination: CursorPagination;
};
export interface IRunsRepository {
name: string;
/**
* A keyset-paginated page of run ids plus the cursors to navigate
* forward/backward. The cursors are opaque composite `(created_at, run_id)`
* tokens, so pagination can't duplicate or skip runs. This is the single
* cursor-aware list primitive — `listRuns` and bulk actions build on it.
*/
listRunIds(options: ListRunsOptions): Promise<RunIdsPage>;
/** Returns friendly IDs (e.g., run_xxx) instead of internal UUIDs. Used for ClickHouse task_events queries. */
listFriendlyRunIds(options: ListRunsOptions): Promise<string[]>;
listRuns(options: ListRunsOptions): Promise<{
runs: ListedRun[];
pagination: {
nextCursor: string | null;
previousCursor: string | null;
};
}>;
countRuns(options: RunListInputOptions): Promise<number>;
listTags(options: TagListOptions): Promise<TagList>;
runExistsInEnvironment(options: {
organizationId: string;
projectId: string;
environmentId: string;
createdAtLowerBoundMs?: number;
}): Promise<boolean>;
}
export class RunsRepository implements IRunsRepository {
private readonly clickHouseRunsRepository: ClickHouseRunsRepository;
constructor(private readonly options: RunsRepositoryOptions) {
this.clickHouseRunsRepository = new ClickHouseRunsRepository(options);
}
get name() {
return "runsRepository";
}
async runExistsInEnvironment(options: {
organizationId: string;
projectId: string;
environmentId: string;
createdAtLowerBoundMs?: number;
}): Promise<boolean> {
return startActiveSpan(
"runsRepository.runExistsInEnvironment",
async () => this.clickHouseRunsRepository.runExistsInEnvironment(options),
{
attributes: {
"repository.name": "clickhouse",
organizationId: options.organizationId,
projectId: options.projectId,
environmentId: options.environmentId,
},
}
);
}
async listRunIds(options: ListRunsOptions): Promise<RunIdsPage> {
return startActiveSpan(
"runsRepository.listRunIds",
async () => this.clickHouseRunsRepository.listRunIds(options),
{
attributes: {
"repository.name": "clickhouse",
organizationId: options.organizationId,
projectId: options.projectId,
environmentId: options.environmentId,
},
}
);
}
async listFriendlyRunIds(options: ListRunsOptions): Promise<string[]> {
return startActiveSpan(
"runsRepository.listFriendlyRunIds",
async () => this.clickHouseRunsRepository.listFriendlyRunIds(options),
{
attributes: {
"repository.name": "clickhouse",
"readThrough.split": Boolean(this.options.readThrough?.splitEnabled),
organizationId: options.organizationId,
projectId: options.projectId,
environmentId: options.environmentId,
},
}
);
}
async listRuns(options: ListRunsOptions): Promise<{
runs: ListedRun[];
pagination: {
nextCursor: string | null;
previousCursor: string | null;
};
}> {
return startActiveSpan(
"runsRepository.listRuns",
async () => this.clickHouseRunsRepository.listRuns(options),
{
attributes: {
"repository.name": "clickhouse",
"readThrough.split": Boolean(this.options.readThrough?.splitEnabled),
organizationId: options.organizationId,
projectId: options.projectId,
environmentId: options.environmentId,
},
}
);
}
async countRuns(options: RunListInputOptions): Promise<number> {
return startActiveSpan(
"runsRepository.countRuns",
async () => this.clickHouseRunsRepository.countRuns(options),
{
attributes: {
"repository.name": "clickhouse",
organizationId: options.organizationId,
projectId: options.projectId,
environmentId: options.environmentId,
},
}
);
}
async listTags(options: TagListOptions): Promise<TagList> {
return startActiveSpan(
"runsRepository.listTags",
async () => this.clickHouseRunsRepository.listTags(options),
{
attributes: {
"repository.name": "clickhouse",
organizationId: options.organizationId,
projectId: options.projectId,
environmentId: options.environmentId,
},
}
);
}
}
export function parseRunListInputOptions(data: any): RunListInputOptions {
return RunListInputOptionsSchema.parse(data);
}
export async function convertRunListInputOptionsToFilterRunsOptions(
options: RunListInputOptions,
prisma: RunsRepositoryOptions["prisma"],
store: RunStore = defaultRunStore
): Promise<FilterRunsOptions> {
const convertedOptions: FilterRunsOptions = {
...options,
period: undefined,
};
// Convert time period to ms
const time = timeFilters({
period: options.period,
from: options.from,
to: options.to,
});
convertedOptions.period = time.period ? (parseDuration(time.period) ?? undefined) : undefined;
// Cross-DB resolution: BatchTaskRun is a RUN-OPS table. A run-ops batch resident on the
// dedicated run-ops DB must resolve via the store's NEW->LEGACY probe — a single control-plane
// client would miss it and leave the friendlyId in the ClickHouse `batch_id` filter, matching
// nothing. Split off / self-host: the store is a passthrough over the one client.
if (options.batchId && options.batchId.startsWith("batch_")) {
const batch = await store.findBatchTaskRunByFriendlyId(options.batchId, options.environmentId);
if (batch) {
convertedOptions.batchId = batch.id;
}
}
// ScheduleId can be a friendlyId. TaskSchedule is a CONTROL-PLANE table, so this stays on
// the passed `prisma` (the control-plane client) in both single-DB and split modes.
if (options.scheduleId && options.scheduleId.startsWith("sched_")) {
const schedule = await prisma.taskSchedule.findFirst({
select: {
id: true,
},
where: {
friendlyId: options.scheduleId,
projectId: options.projectId,
},
});
if (schedule) {
convertedOptions.scheduleId = schedule?.id;
}
}
if (options.bulkId && options.bulkId.startsWith("bulk_")) {
convertedOptions.bulkId = BulkActionId.toId(options.bulkId);
}
if (options.runId) {
// Convert to friendlyId
convertedOptions.runId = options.runId.map((r) => RunId.toFriendlyId(r));
}
// batchId/runId/scheduleId target specific runs, so rootOnly is meaningless and forced off.
// tasks is intentionally excluded so rootOnly can narrow a task filter to root runs only.
if (options.batchId || options.runId?.length || options.scheduleId) {
convertedOptions.rootOnly = false;
}
return convertedOptions;
}