test(webapp): avoid repeated replication setup

This commit is contained in:
Chris Arderne
2026-08-19 16:59:39 +01:00
parent e0136cd13e
commit ff42675572
3 changed files with 390 additions and 323 deletions
@@ -0,0 +1,142 @@
import { describe, expect, vi } from "vitest";
// The presenter graph imports `~/db.server` singletons even though the asserted reads use explicit
// clients. These lazy proxies delegate every access to the real per-test Postgres containers.
const legacyReplicaHolder = vi.hoisted(() => ({ client: undefined as any }));
const newClientHolder = vi.hoisted(() => ({ client: undefined as any }));
const clickhouseHolder = vi.hoisted(() => ({ client: undefined as any }));
vi.mock("~/services/clickhouse/clickhouseFactoryInstance.server", () => ({
clickhouseFactory: {
getClickhouseForOrganization: async () => {
if (!clickhouseHolder.client) {
throw new Error("clickhouseHolder.client not set for this test");
}
return clickhouseHolder.client;
},
},
}));
vi.mock("~/db.server", async () => {
const { Prisma } = await import("@trigger.dev/database");
const lazyProxy = (holder: { client: any }, label: string) =>
new Proxy(
{},
{
get(_target, property) {
if (!holder.client) {
throw new Error(`${label} not set for this test`);
}
return holder.client[property];
},
}
);
const replicaProxy = lazyProxy(legacyReplicaHolder, "legacyReplicaHolder.client");
const newProxy = lazyProxy(newClientHolder, "newClientHolder.client");
return {
prisma: replicaProxy,
$replica: replicaProxy,
runOpsNewPrisma: newProxy,
runOpsNewReplica: newProxy,
runOpsLegacyPrisma: replicaProxy,
runOpsLegacyReplica: replicaProxy,
sqlDatabaseSchema: Prisma.sql([`public`]),
};
});
import { createPostgresContainer, replicationContainerTest } from "@internal/testcontainers";
import { PrismaClient } from "@trigger.dev/database";
import { setTimeout } from "node:timers/promises";
import { CURRENT_API_VERSION } from "~/api/versions";
import { ApiRunListPresenter } from "~/presenters/v3/ApiRunListPresenter.server";
import { createRun, mirrorParents, seedParents } from "./helpers/apiRunListPresenterTestHelpers";
import { setupClickhouseReplication } from "./utils/replicationUtils";
vi.setConfig({ testTimeout: 90_000 });
describe("ApiRunListPresenter public /runs routed read-through", () => {
replicationContainerTest(
"public payload lists run-ops rows served via the routed store (NEW + legacy union)",
async ({ clickhouseContainer, redisOptions, postgresContainer, prisma, network }) => {
const { clickhouse } = await setupClickhouseReplication({
prisma,
databaseUrl: postgresContainer.getConnectionUri(),
clickhouseUrl: clickhouseContainer.getConnectionUrl(),
redisOptions,
});
const { url: newUrl } = await createPostgresContainer(network, {
imageTag: "docker.io/postgres:17",
});
const prismaNew = new PrismaClient({ datasources: { db: { url: newUrl } } });
legacyReplicaHolder.client = prisma;
clickhouseHolder.client = clickhouse;
newClientHolder.client = prismaNew;
try {
const ctx = await seedParents(prisma, "hydrate");
await mirrorParents(prismaNew, ctx, "hydrate");
// PG14 is the logical-replication source, so ClickHouse receives the complete ID set.
const legacyOnlyA = await createRun(prisma, ctx, { friendlyId: "run_legacyA" });
const legacyOnlyB = await createRun(prisma, ctx, { friendlyId: "run_legacyB" });
const migratedA = await createRun(prisma, ctx, { friendlyId: "run_newA" });
const migratedB = await createRun(prisma, ctx, { friendlyId: "run_newB" });
// The routed PG17 rows use distinguishing values that prove NEW hydration won.
await createRun(prismaNew, ctx, {
friendlyId: "run_newA",
taskIdentifier: "my-task-NEW",
});
await createRun(prismaNew, ctx, {
friendlyId: "run_newB",
taskIdentifier: "my-task-NEW",
});
await prismaNew.taskRun.update({
where: { friendlyId: "run_newA" },
data: { id: migratedA.id },
});
await prismaNew.taskRun.update({
where: { friendlyId: "run_newB" },
data: { id: migratedB.id },
});
await setTimeout(1500);
const presenter = new ApiRunListPresenter(prisma, prisma, {
newClient: prismaNew,
legacyReplica: prisma,
splitEnabled: true,
});
const result = await presenter.call(
{ id: ctx.projectId },
{ "page[size]": 10 } as any,
CURRENT_API_VERSION,
{ id: ctx.environmentId, organizationId: ctx.organizationId }
);
const expectedFriendlyIds = [
{ id: migratedA.id, friendlyId: "run_newA" },
{ id: migratedB.id, friendlyId: "run_newB" },
{ id: legacyOnlyA.id, friendlyId: "run_legacyA" },
{ id: legacyOnlyB.id, friendlyId: "run_legacyB" },
]
.sort((a, b) => (a.id < b.id ? 1 : a.id > b.id ? -1 : 0))
.map((run) => run.friendlyId);
expect(result.data.map((run) => run.id)).toEqual(expectedFriendlyIds);
const migratedRow = result.data.find((run) => run.id === "run_newA");
expect(migratedRow?.taskIdentifier).toBe("my-task-NEW");
expect(migratedRow?.taskKind).toBe("STANDARD");
expect(result.data.find((run) => run.id === "run_legacyA")?.taskIdentifier).toBe("my-task");
expect(result.pagination).toHaveProperty("next");
expect(result.pagination).toHaveProperty("previous");
} finally {
await prismaNew.$disconnect();
}
}
);
});
+70 -323
View File
@@ -1,20 +1,11 @@
import { describe, expect, vi } from "vitest";
// The presenter graph imports `~/v3/runStore.server` (via RunsRepository) which imports
// `~/db.server` at load, and the presenter itself reaches `~/db.server`'s `$replica` singleton
// through `findDisplayableEnvironment` and `getTaskIdentifiers`. Stub the module so those
// singleton reads resolve. This is the ONLY mock — the DB is NEVER mocked; the proxy delegates
// to the per-test REAL legacy (PG14) container so the env-lookup + task-identifier reads hit a
// real database. Everything asserted runs against real containers. Mirrors
// nextRunListPresenter.readthrough.test.ts.
// The presenter graph imports `~/db.server` singletons even though these tests pass explicit real
// clients. The proxies keep those singleton reads on the current warm Postgres fixture.
const legacyReplicaHolder = vi.hoisted(() => ({ client: undefined as any }));
const newClientHolder = vi.hoisted(() => ({ client: undefined as any }));
// `ApiRunListPresenter` resolves its read ClickHouse internally via the `clickhouseFactory`
// singleton (which imports `~/env.server` and binds to a process-wide default client). Stub the
// instance module so `getClickhouseForOrganization` returns the per-test container's ClickHouse
// handle (set by each test before calling). This is a module-resolution shim — the ClickHouse is
// a REAL testcontainer, never mocked — mirroring the `~/db.server` stub below.
const clickhouseHolder = vi.hoisted(() => ({ client: undefined as any }));
vi.mock("~/services/clickhouse/clickhouseFactoryInstance.server", () => ({
clickhouseFactory: {
getClickhouseForOrganization: async () => {
@@ -25,22 +16,24 @@ vi.mock("~/services/clickhouse/clickhouseFactoryInstance.server", () => ({
},
},
}));
vi.mock("~/db.server", async () => {
const { Prisma } = await import("@trigger.dev/database");
const lazyProxy = (holder: { client: any }, label: string) =>
new Proxy(
{},
{
get(_t, prop) {
get(_target, property) {
if (!holder.client) {
throw new Error(`${label} not set for this test`);
}
return holder.client[prop];
return holder.client[property];
},
}
);
const replicaProxy = lazyProxy(legacyReplicaHolder, "legacyReplicaHolder.client");
const newProxy = lazyProxy(newClientHolder, "newClientHolder.client");
return {
prisma: replicaProxy,
$replica: replicaProxy,
@@ -52,347 +45,101 @@ vi.mock("~/db.server", async () => {
};
});
import { createPostgresContainer, replicationContainerTest } from "@internal/testcontainers";
import { PrismaClient } from "@trigger.dev/database";
import { setTimeout } from "node:timers/promises";
import { ClickHouse } from "@internal/clickhouse";
import { containerTest } from "@internal/testcontainers";
import { CURRENT_API_VERSION } from "~/api/versions";
import { ApiRunListPresenter } from "~/presenters/v3/ApiRunListPresenter.server";
import { setupClickhouseReplication } from "./utils/replicationUtils";
import {
addEnvironment,
createRun,
insertTaskRunV2Rows,
seedParents,
} from "./helpers/apiRunListPresenterTestHelpers";
vi.setConfig({ testTimeout: 90_000 });
type SeedContext = {
organizationId: string;
projectId: string;
environmentId: string;
environmentSlug: string;
};
/**
* Creates the org/project/env parents on a single prisma client. TaskRun FKs require these to
* exist on every DB a run lives on, so identical parents (same ids) are seeded on both the
* legacy (PG14) and new (PG17) databases.
*/
async function seedParents(
prisma: PrismaClient,
slug: string,
envSlug = `env-${slug}`
): Promise<SeedContext> {
const organization = await prisma.organization.create({
data: { title: `org-${slug}`, slug: `org-${slug}` },
function setupClients(prisma: unknown, clickhouseUrl: string): ClickHouse {
const clickhouse = new ClickHouse({
url: clickhouseUrl,
name: "api-run-list-presenter-test",
compression: { request: true },
});
const project = await prisma.project.create({
data: {
name: `proj-${slug}`,
slug: `proj-${slug}`,
organizationId: organization.id,
externalRef: `proj-${slug}`,
},
});
const runtimeEnvironment = await prisma.runtimeEnvironment.create({
data: {
slug: envSlug,
type: "DEVELOPMENT",
projectId: project.id,
organizationId: organization.id,
apiKey: `tr_dev_${slug}`,
pkApiKey: `pk_dev_${slug}`,
shortcode: `sc-${slug}`,
},
});
return {
organizationId: organization.id,
projectId: project.id,
environmentId: runtimeEnvironment.id,
environmentSlug: runtimeEnvironment.slug,
};
legacyReplicaHolder.client = prisma;
newClientHolder.client = prisma;
clickhouseHolder.client = clickhouse;
return clickhouse;
}
/** Adds an extra RuntimeEnvironment (control-plane row) to an existing project. */
async function addEnvironment(
prisma: PrismaClient,
ctx: SeedContext,
slug: string,
envSlug: string
): Promise<string> {
const env = await prisma.runtimeEnvironment.create({
data: {
slug: envSlug,
type: "STAGING",
projectId: ctx.projectId,
organizationId: ctx.organizationId,
apiKey: `tr_${envSlug}_${slug}`,
pkApiKey: `pk_${envSlug}_${slug}`,
shortcode: `sc-${envSlug}-${slug}`,
},
});
return env.id;
}
/** Mirrors the org/project/env parents onto a second DB with the SAME ids. */
async function mirrorParents(prisma: PrismaClient, ctx: SeedContext, slug: string): Promise<void> {
await prisma.organization.create({
data: { id: ctx.organizationId, title: `org-${slug}`, slug: `org-${slug}` },
});
await prisma.project.create({
data: {
id: ctx.projectId,
name: `proj-${slug}`,
slug: `proj-${slug}`,
organizationId: ctx.organizationId,
externalRef: `proj-${slug}`,
},
});
await prisma.runtimeEnvironment.create({
data: {
id: ctx.environmentId,
slug: ctx.environmentSlug,
type: "DEVELOPMENT",
projectId: ctx.projectId,
organizationId: ctx.organizationId,
apiKey: `tr_dev_${slug}_b`,
pkApiKey: `pk_dev_${slug}_b`,
shortcode: `sc-${slug}-b`,
},
});
}
async function createRun(
prisma: PrismaClient,
ctx: SeedContext,
run: {
friendlyId: string;
taskIdentifier?: string;
status?: any;
runtimeEnvironmentId?: string;
}
) {
return prisma.taskRun.create({
data: {
friendlyId: run.friendlyId,
taskIdentifier: run.taskIdentifier ?? "my-task",
status: run.status ?? "PENDING",
payload: JSON.stringify({ foo: run.friendlyId }),
traceId: run.friendlyId,
spanId: run.friendlyId,
queue: "test",
runTags: [],
runtimeEnvironmentId: run.runtimeEnvironmentId ?? ctx.environmentId,
projectId: ctx.projectId,
organizationId: ctx.organizationId,
environmentType: "DEVELOPMENT",
engine: "V2",
},
});
}
describe("ApiRunListPresenter public /runs list (PG14 legacy + PG17 new)", () => {
// Public list serves run-ops rows through the routed store. The
// forwarded readThroughDeps thread the dual-DB union into NextRunListPresenter; the public
// payload (`{ data, pagination }`) must list the NEW legacy union, proving the public API
// surfaces routed run-ops rows. The migrated/straggler rows (run_newA/run_newB) live on BOTH
// DBs with the same id + friendlyId but a DISTINGUISHING taskIdentifier ("my-task-NEW" on PG17),
// so a row served from the threaded newClient is identifiable in the public payload.
replicationContainerTest(
"public payload lists run-ops rows served via the routed store (NEW + legacy union)",
async ({ clickhouseContainer, redisOptions, postgresContainer, prisma, network }) => {
const { clickhouse } = await setupClickhouseReplication({
prisma,
databaseUrl: postgresContainer.getConnectionUri(),
clickhouseUrl: clickhouseContainer.getConnectionUrl(),
redisOptions,
});
const { url: newUrl } = await createPostgresContainer(network, {
imageTag: "docker.io/postgres:17",
});
const prismaNew = new PrismaClient({ datasources: { db: { url: newUrl } } });
legacyReplicaHolder.client = prisma;
clickhouseHolder.client = clickhouse;
// The routed store's default known-migrated probe reads `runOpsNewPrisma` -> PG17.
newClientHolder.client = prismaNew;
try {
const ctx = await seedParents(prisma, "hydrate");
await mirrorParents(prismaNew, ctx, "hydrate");
// All four runs land on PG14 (legacy + replication source -> CH gets the full id-set).
const legacyOnlyA = await createRun(prisma, ctx, { friendlyId: "run_legacyA" });
const legacyOnlyB = await createRun(prisma, ctx, { friendlyId: "run_legacyB" });
const migratedA = await createRun(prisma, ctx, { friendlyId: "run_newA" });
const migratedB = await createRun(prisma, ctx, { friendlyId: "run_newB" });
// The two "migrated" runs also live on NEW (authoritative during retention), same ids +
// friendlyIds, but a DISTINGUISHING taskIdentifier so a row served from PG17 is
// identifiable in the public payload.
await createRun(prismaNew, ctx, { friendlyId: "run_newA", taskIdentifier: "my-task-NEW" });
await createRun(prismaNew, ctx, { friendlyId: "run_newB", taskIdentifier: "my-task-NEW" });
await prismaNew.taskRun.update({
where: { friendlyId: "run_newA" },
data: { id: migratedA.id },
});
await prismaNew.taskRun.update({
where: { friendlyId: "run_newB" },
data: { id: migratedB.id },
});
// Wait for CH replication so the id-set page is non-empty.
await setTimeout(1500);
const presenter = new ApiRunListPresenter(prisma, prisma, {
newClient: prismaNew,
legacyReplica: prisma,
splitEnabled: true,
});
const result = await presenter.call(
{ id: ctx.projectId },
{ "page[size]": 10 } as any,
CURRENT_API_VERSION,
{ id: ctx.environmentId, organizationId: ctx.organizationId }
);
// The public payload lists runs by `id` = `run.friendlyId`, id-desc ordered.
const expectedFriendlyIds = [
{ id: migratedA.id, friendlyId: "run_newA" },
{ id: migratedB.id, friendlyId: "run_newB" },
{ id: legacyOnlyA.id, friendlyId: "run_legacyA" },
{ id: legacyOnlyB.id, friendlyId: "run_legacyB" },
]
.sort((a, b) => (a.id < b.id ? 1 : a.id > b.id ? -1 : 0))
.map((r) => r.friendlyId);
expect(result.data.map((r) => r.id)).toEqual(expectedFriendlyIds);
// The migrated rows must carry the PG17-only taskIdentifier — only possible if the public
// path hydrated them through the threaded newClient (PG17). taskKind falls back to STANDARD.
const migratedRow = result.data.find((r) => r.id === "run_newA");
expect(migratedRow?.taskIdentifier).toBe("my-task-NEW");
expect(migratedRow?.taskKind).toBe("STANDARD");
// The legacy-only rows surface from PG14, proving the legacyReplica is also exercised.
expect(result.data.find((r) => r.id === "run_legacyA")?.taskIdentifier).toBe("my-task");
// Pagination shape is present.
expect(result.pagination).toHaveProperty("next");
expect(result.pagination).toHaveProperty("previous");
} finally {
await prismaNew.$disconnect();
}
}
);
// Genuinely-empty env returns { data: [], pagination } without error. Exercises the
// empty-state probe beneath NextRunListPresenter (no rows on either DB; empty CH page).
replicationContainerTest(
describe("ApiRunListPresenter public /runs list", () => {
containerTest(
"genuinely-empty env returns { data: [], pagination } without error",
async ({ clickhouseContainer, redisOptions, postgresContainer, prisma, network }) => {
const { clickhouse } = await setupClickhouseReplication({
prisma,
databaseUrl: postgresContainer.getConnectionUri(),
clickhouseUrl: clickhouseContainer.getConnectionUrl(),
redisOptions,
async ({ clickhouseContainer, prisma }) => {
setupClients(prisma, clickhouseContainer.getConnectionUrl());
const ctx = await seedParents(prisma, "empty");
// Keep the split/read-through branch active while both real clients point at the empty DB.
const presenter = new ApiRunListPresenter(prisma, prisma, {
newClient: prisma,
legacyReplica: prisma,
splitEnabled: true,
});
const { url: newUrl } = await createPostgresContainer(network, {
imageTag: "docker.io/postgres:17",
});
const prismaNew = new PrismaClient({ datasources: { db: { url: newUrl } } });
legacyReplicaHolder.client = prisma;
clickhouseHolder.client = clickhouse;
const result = await presenter.call(
{ id: ctx.projectId },
{ "page[size]": 10 } as any,
CURRENT_API_VERSION,
{ id: ctx.environmentId, organizationId: ctx.organizationId }
);
try {
const ctx = await seedParents(prisma, "empty");
await mirrorParents(prismaNew, ctx, "empty");
const presenter = new ApiRunListPresenter(prisma, prisma, {
newClient: prismaNew,
legacyReplica: prisma,
splitEnabled: true,
});
const result = await presenter.call(
{ id: ctx.projectId },
{ "page[size]": 10 } as any,
CURRENT_API_VERSION,
{ id: ctx.environmentId, organizationId: ctx.organizationId }
);
expect(result.data).toEqual([]);
expect(result.pagination).toHaveProperty("next");
expect(result.pagination).toHaveProperty("previous");
} finally {
await prismaNew.$disconnect();
}
expect(result.data).toEqual([]);
expect(result.pagination).toHaveProperty("next");
expect(result.pagination).toHaveProperty("previous");
}
);
// Env scoping unchanged: the control-plane runtimeEnvironment.findMany lookup
// resolves the requested env via the `_replica` handle (NOT routed), with the 4th `environment`
// arg omitted to force that branch. Result is scoped to the requested env only.
replicationContainerTest(
containerTest(
"env scoping resolves via the control-plane _replica handle (filter[env], 4th arg omitted)",
async ({ clickhouseContainer, redisOptions, postgresContainer, prisma }) => {
const { clickhouse } = await setupClickhouseReplication({
prisma,
databaseUrl: postgresContainer.getConnectionUri(),
clickhouseUrl: clickhouseContainer.getConnectionUrl(),
redisOptions,
});
legacyReplicaHolder.client = prisma;
clickhouseHolder.client = clickhouse;
async ({ clickhouseContainer, prisma }) => {
const clickhouse = setupClients(prisma, clickhouseContainer.getConnectionUrl());
const ctx = await seedParents(prisma, "scoping", "prod");
const stagingEnvId = await addEnvironment(prisma, ctx, "scoping", "staging");
const stagingEnvironmentId = await addEnvironment(prisma, ctx, "scoping", "staging");
// Runs in prod only; a run in staging must NOT surface when filter[env]=prod.
await createRun(prisma, ctx, { friendlyId: "run_prod1" });
await createRun(prisma, ctx, { friendlyId: "run_prod2" });
await createRun(prisma, ctx, {
friendlyId: "run_staging",
runtimeEnvironmentId: stagingEnvId,
});
// The Postgres rows exercise real hydration; matching ClickHouse rows provide the list IDs.
const runs = await Promise.all([
createRun(prisma, ctx, { friendlyId: "run_prod1" }),
createRun(prisma, ctx, { friendlyId: "run_prod2" }),
createRun(prisma, ctx, {
friendlyId: "run_staging",
runtimeEnvironmentId: stagingEnvironmentId,
}),
]);
await insertTaskRunV2Rows(clickhouse, runs);
await setTimeout(1500);
// Single-handle passthrough; the env lookup runs on `_replica` (= prisma) via findMany.
const presenter = new ApiRunListPresenter(prisma, prisma);
// 4th `environment` arg OMITTED -> forces the runtimeEnvironment.findMany branch.
// Omitting the fourth argument forces the control-plane runtimeEnvironment.findMany branch.
const result = await presenter.call(
{ id: ctx.projectId },
{ "page[size]": 10, "filter[env]": ["prod"] } as any,
CURRENT_API_VERSION
);
// Scoped to the resolved prod env only.
expect(result.data.map((r) => r.id).sort()).toEqual(["run_prod1", "run_prod2"]);
expect(result.data.map((run) => run.id).sort()).toEqual(["run_prod1", "run_prod2"]);
}
);
// Passthrough (single-DB): two-arg-style construction (no readThroughDeps) ->
// NextRunListPresenter receives undefined deps -> byte-identical single-DB path. The public
// { data, pagination } shape is unchanged.
replicationContainerTest(
containerTest(
"single-DB passthrough: no readThroughDeps lists the seeded runs unchanged",
async ({ clickhouseContainer, redisOptions, postgresContainer, prisma }) => {
const { clickhouse } = await setupClickhouseReplication({
prisma,
databaseUrl: postgresContainer.getConnectionUri(),
clickhouseUrl: clickhouseContainer.getConnectionUrl(),
redisOptions,
});
legacyReplicaHolder.client = prisma;
clickhouseHolder.client = clickhouse;
async ({ clickhouseContainer, prisma }) => {
const clickhouse = setupClients(prisma, clickhouseContainer.getConnectionUrl());
const ctx = await seedParents(prisma, "passthrough");
await createRun(prisma, ctx, { friendlyId: "run_pt1" });
await createRun(prisma, ctx, { friendlyId: "run_pt2" });
const runs = await Promise.all([
createRun(prisma, ctx, { friendlyId: "run_pt1" }),
createRun(prisma, ctx, { friendlyId: "run_pt2" }),
]);
await insertTaskRunV2Rows(clickhouse, runs);
await setTimeout(1500);
// No readThroughDeps -> passthrough, exactly as the routes construct it today.
// No readThroughDeps preserves the single-database path used by existing callers.
const presenter = new ApiRunListPresenter(prisma, prisma);
const result = await presenter.call(
@@ -402,7 +149,7 @@ describe("ApiRunListPresenter public /runs list (PG14 legacy + PG17 new)", () =>
{ id: ctx.environmentId, organizationId: ctx.organizationId }
);
expect(result.data.map((r) => r.id).sort()).toEqual(["run_pt1", "run_pt2"]);
expect(result.data.map((run) => run.id).sort()).toEqual(["run_pt1", "run_pt2"]);
expect(result).toHaveProperty("pagination");
expect(result.pagination).toHaveProperty("next");
expect(result.pagination).toHaveProperty("previous");
@@ -0,0 +1,178 @@
import type { ClickHouse, TaskRunV2 } from "@internal/clickhouse";
import type { PrismaClient, TaskRun, TaskRunStatus } from "@trigger.dev/database";
import { z } from "zod";
export type SeedContext = {
organizationId: string;
projectId: string;
environmentId: string;
environmentSlug: string;
};
/** Creates the org/project/environment parents needed by TaskRun foreign keys. */
export async function seedParents(
prisma: PrismaClient,
slug: string,
envSlug = `env-${slug}`
): Promise<SeedContext> {
const organization = await prisma.organization.create({
data: { title: `org-${slug}`, slug: `org-${slug}` },
});
const project = await prisma.project.create({
data: {
name: `proj-${slug}`,
slug: `proj-${slug}`,
organizationId: organization.id,
externalRef: `proj-${slug}`,
},
});
const runtimeEnvironment = await prisma.runtimeEnvironment.create({
data: {
slug: envSlug,
type: "DEVELOPMENT",
projectId: project.id,
organizationId: organization.id,
apiKey: `tr_dev_${slug}`,
pkApiKey: `pk_dev_${slug}`,
shortcode: `sc-${slug}`,
},
});
return {
organizationId: organization.id,
projectId: project.id,
environmentId: runtimeEnvironment.id,
environmentSlug: runtimeEnvironment.slug,
};
}
/** Adds another control-plane environment to an existing project. */
export async function addEnvironment(
prisma: PrismaClient,
ctx: SeedContext,
slug: string,
envSlug: string
): Promise<string> {
const environment = await prisma.runtimeEnvironment.create({
data: {
slug: envSlug,
type: "STAGING",
projectId: ctx.projectId,
organizationId: ctx.organizationId,
apiKey: `tr_${envSlug}_${slug}`,
pkApiKey: `pk_${envSlug}_${slug}`,
shortcode: `sc-${envSlug}-${slug}`,
},
});
return environment.id;
}
/** Mirrors the parents onto another database with the same IDs. */
export async function mirrorParents(
prisma: PrismaClient,
ctx: SeedContext,
slug: string
): Promise<void> {
await prisma.organization.create({
data: { id: ctx.organizationId, title: `org-${slug}`, slug: `org-${slug}` },
});
await prisma.project.create({
data: {
id: ctx.projectId,
name: `proj-${slug}`,
slug: `proj-${slug}`,
organizationId: ctx.organizationId,
externalRef: `proj-${slug}`,
},
});
await prisma.runtimeEnvironment.create({
data: {
id: ctx.environmentId,
slug: ctx.environmentSlug,
type: "DEVELOPMENT",
projectId: ctx.projectId,
organizationId: ctx.organizationId,
apiKey: `tr_dev_${slug}_b`,
pkApiKey: `pk_dev_${slug}_b`,
shortcode: `sc-${slug}-b`,
},
});
}
export async function createRun(
prisma: PrismaClient,
ctx: SeedContext,
run: {
friendlyId: string;
taskIdentifier?: string;
status?: TaskRunStatus;
runtimeEnvironmentId?: string;
}
): Promise<TaskRun> {
return prisma.taskRun.create({
data: {
friendlyId: run.friendlyId,
taskIdentifier: run.taskIdentifier ?? "my-task",
status: run.status ?? "PENDING",
payload: JSON.stringify({ foo: run.friendlyId }),
traceId: run.friendlyId,
spanId: run.friendlyId,
queue: "test",
runTags: [],
runtimeEnvironmentId: run.runtimeEnvironmentId ?? ctx.environmentId,
projectId: ctx.projectId,
organizationId: ctx.organizationId,
environmentType: "DEVELOPMENT",
engine: "V2",
},
});
}
/** Inserts the ClickHouse list-index rows synchronously, without logical replication. */
export async function insertTaskRunV2Rows(clickhouse: ClickHouse, runs: TaskRun[]): Promise<void> {
const insert = clickhouse.writer.insert({
name: "insertApiRunListPresenterTaskRuns",
table: "trigger_dev.task_runs_v2",
schema: z.any(),
settings: { async_insert: 0, enable_json_type: 1, type_json_skip_duplicated_paths: 1 },
});
const rows: TaskRunV2[] = runs.map((run) => ({
environment_id: run.runtimeEnvironmentId,
organization_id: run.organizationId ?? "",
project_id: run.projectId,
run_id: run.id,
friendly_id: run.friendlyId,
updated_at: run.updatedAt.getTime(),
created_at: run.createdAt.getTime(),
status: run.status,
environment_type: run.environmentType ?? "DEVELOPMENT",
attempt: run.attemptNumber ?? 1,
engine: run.engine,
task_identifier: run.taskIdentifier,
queue: run.queue,
schedule_id: "",
batch_id: "",
task_version: run.taskVersion ?? "",
sdk_version: run.sdkVersion ?? "",
cli_version: run.cliVersion ?? "",
machine_preset: run.machinePreset ?? "",
root_run_id: "",
parent_run_id: "",
span_id: run.spanId,
trace_id: run.traceId,
idempotency_key: run.idempotencyKey ?? "",
expiration_ttl: run.ttl ?? "",
tags: run.runTags,
worker_queue: run.workerQueue,
region: run.region ?? "",
_version: String(run.updatedAt.getTime()),
_is_deleted: 0,
}));
const [error] = await insert(rows);
if (error) {
throw error;
}
}