|
| 1 | +/** |
| 2 | + * Org-wide lookup for the Dashboard Agent: given a run or error id the agent could not find in |
| 3 | + * the current project/environment, name every scope inside the token's organization where it |
| 4 | + * exists. The organization is a hard boundary — an object in another org reads as not found, so |
| 5 | + * the endpoint never confirms its existence. |
| 6 | + */ |
| 7 | + |
| 8 | +import { boundedIn, type RuntimeEnvironmentType } from "@trigger.dev/database"; |
| 9 | +import { ErrorId } from "@trigger.dev/core/v3/isomorphic"; |
| 10 | +import { $replica } from "~/db.server"; |
| 11 | +import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; |
| 12 | +import { runStore } from "~/v3/runStore.server"; |
| 13 | + |
| 14 | +export type LocatedScope = { |
| 15 | + projectRef: string; |
| 16 | + projectName: string; |
| 17 | + environmentName: string; |
| 18 | + environmentType: RuntimeEnvironmentType; |
| 19 | + branchName?: string; |
| 20 | + /** Whether the caller may act in this scope. An inaccessible scope is still reported. */ |
| 21 | + targetable: boolean; |
| 22 | +}; |
| 23 | + |
| 24 | +export type LocateResult = |
| 25 | + | { found: true; scopes: LocatedScope[]; checked: "organization" } |
| 26 | + | { found: false; checked: "organization" }; |
| 27 | + |
| 28 | +const NOT_FOUND: LocateResult = { found: false, checked: "organization" }; |
| 29 | + |
| 30 | +export type LocateTarget = { |
| 31 | + kind: "run" | "error"; |
| 32 | + id: string; |
| 33 | + organizationId: string; |
| 34 | + userId: string; |
| 35 | +}; |
| 36 | + |
| 37 | +export async function isOrganizationMember( |
| 38 | + organizationId: string, |
| 39 | + userId: string |
| 40 | +): Promise<boolean> { |
| 41 | + const organization = await $replica.organization.findFirst({ |
| 42 | + where: { id: organizationId, deletedAt: null, members: { some: { userId } } }, |
| 43 | + select: { id: true }, |
| 44 | + }); |
| 45 | + |
| 46 | + return !!organization; |
| 47 | +} |
| 48 | + |
| 49 | +export async function locateAgentObject(target: LocateTarget): Promise<LocateResult> { |
| 50 | + const environmentIds = |
| 51 | + target.kind === "run" |
| 52 | + ? await runEnvironmentIds(target.id) |
| 53 | + : await errorEnvironmentIds(target.id, target.organizationId); |
| 54 | + |
| 55 | + const scopes = await scopesForEnvironments(environmentIds, target.organizationId, target.userId); |
| 56 | + |
| 57 | + return scopes.length > 0 ? { found: true, scopes, checked: "organization" } : NOT_FOUND; |
| 58 | +} |
| 59 | + |
| 60 | +/** A run friendlyId is globally unique, so this is one lookup; the org filter comes later. */ |
| 61 | +async function runEnvironmentIds(friendlyId: string): Promise<string[]> { |
| 62 | + const run = await runStore.findRun({ friendlyId }, { select: { runtimeEnvironmentId: true } }); |
| 63 | + |
| 64 | + return run ? [run.runtimeEnvironmentId] : []; |
| 65 | +} |
| 66 | + |
| 67 | +/** A fingerprint is legitimately present in many environments, so every one of them answers. */ |
| 68 | +async function errorEnvironmentIds(errorId: string, organizationId: string): Promise<string[]> { |
| 69 | + let fingerprint: string; |
| 70 | + try { |
| 71 | + fingerprint = ErrorId.toId(errorId); |
| 72 | + } catch { |
| 73 | + return []; |
| 74 | + } |
| 75 | + |
| 76 | + const clickhouse = await clickhouseFactory.getClickhouseForOrganization(organizationId, "logs"); |
| 77 | + const [queryError, rows] = await clickhouse.errors.getScopes({ |
| 78 | + organizationId, |
| 79 | + errorFingerprint: fingerprint, |
| 80 | + }); |
| 81 | + |
| 82 | + if (queryError) { |
| 83 | + throw queryError; |
| 84 | + } |
| 85 | + |
| 86 | + return (rows ?? []).map((row) => row.environment_id); |
| 87 | +} |
| 88 | + |
| 89 | +async function scopesForEnvironments( |
| 90 | + environmentIds: string[], |
| 91 | + organizationId: string, |
| 92 | + userId: string |
| 93 | +): Promise<LocatedScope[]> { |
| 94 | + if (environmentIds.length === 0) { |
| 95 | + return []; |
| 96 | + } |
| 97 | + |
| 98 | + const environments = await $replica.runtimeEnvironment.findMany({ |
| 99 | + where: { |
| 100 | + id: { in: boundedIn(environmentIds) }, |
| 101 | + organizationId, |
| 102 | + archivedAt: null, |
| 103 | + project: { deletedAt: null }, |
| 104 | + }, |
| 105 | + select: { |
| 106 | + slug: true, |
| 107 | + type: true, |
| 108 | + branchName: true, |
| 109 | + orgMember: { select: { userId: true } }, |
| 110 | + project: { select: { externalRef: true, name: true } }, |
| 111 | + }, |
| 112 | + }); |
| 113 | + |
| 114 | + return environments.map((environment) => ({ |
| 115 | + projectRef: environment.project.externalRef, |
| 116 | + projectName: environment.project.name, |
| 117 | + environmentName: environment.slug, |
| 118 | + environmentType: environment.type, |
| 119 | + ...(environment.branchName ? { branchName: environment.branchName } : {}), |
| 120 | + // dev is per-user: another member's dev environment exists but can't be acted in. |
| 121 | + targetable: environment.type !== "DEVELOPMENT" || environment.orgMember?.userId === userId, |
| 122 | + })); |
| 123 | +} |
0 commit comments