Skip to content

Commit 2f5b4a8

Browse files
nicktrnmatt-aitken
andauthored
v3: fix raw queries for custom schemas (#1033)
* add custom validation to db url env vars * extract schema from db url and use in all raw queries * use qualified names in scheduling raw queries * cook a few raw queries * Added missing raw query schema specifier to DeploymentListPresenter --------- Co-authored-by: Matt Aitken <[email protected]>
1 parent 69f6891 commit 2f5b4a8

12 files changed

+99
-46
lines changed

apps/webapp/app/db.server.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { z } from "zod";
44
import { logger } from "./services/logger.server";
55
import { env } from "./env.server";
66
import { singleton } from "./utils/singleton";
7+
import { isValidDatabaseUrl } from "./utils/db";
78

89
export type PrismaTransactionClient = Omit<
910
PrismaClient,
@@ -138,3 +139,23 @@ export type { PrismaClient } from "@trigger.dev/database";
138139
export const PrismaErrorSchema = z.object({
139140
code: z.string(),
140141
});
142+
143+
function getDatabaseSchema() {
144+
if (!isValidDatabaseUrl(env.DATABASE_URL)) {
145+
throw new Error("Invalid Database URL");
146+
}
147+
148+
const databaseUrl = new URL(env.DATABASE_URL);
149+
const schemaFromSearchParam = databaseUrl.searchParams.get("schema");
150+
151+
if (!schemaFromSearchParam) {
152+
console.debug("❗ database schema unspecified, will default to `public` schema");
153+
return "public";
154+
}
155+
156+
return schemaFromSearchParam;
157+
}
158+
159+
export const DATABASE_SCHEMA = singleton("DATABASE_SCHEMA", getDatabaseSchema);
160+
161+
export const sqlDatabaseSchema = Prisma.sql([`${DATABASE_SCHEMA}`]);

apps/webapp/app/env.server.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
import { z } from "zod";
22
import { SecretStoreOptionsSchema } from "./services/secrets/secretStore.server";
33
import { isValidRegex } from "./utils/regex";
4+
import { isValidDatabaseUrl } from "./utils/db";
45

56
const EnvironmentSchema = z.object({
67
NODE_ENV: z.union([z.literal("development"), z.literal("production"), z.literal("test")]),
7-
DATABASE_URL: z.string(),
8+
DATABASE_URL: z
9+
.string()
10+
.refine(
11+
isValidDatabaseUrl,
12+
"DATABASE_URL is invalid, for details please check the additional output above this message."
13+
),
814
DATABASE_CONNECTION_LIMIT: z.coerce.number().int().default(10),
915
DATABASE_POOL_TIMEOUT: z.coerce.number().int().default(60),
10-
DIRECT_URL: z.string(),
16+
DIRECT_URL: z
17+
.string()
18+
.refine(
19+
isValidDatabaseUrl,
20+
"DIRECT_URL is invalid, for details please check the additional output above this message."
21+
),
1122
SESSION_SECRET: z.string(),
1223
MAGIC_LINK_SECRET: z.string(),
1324
ENCRYPTION_KEY: z.string(),

apps/webapp/app/presenters/JobListPresenter.server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
DisplayPropertySchema,
44
EventSpecificationSchema,
55
} from "@trigger.dev/core";
6-
import { PrismaClient, Prisma, prisma } from "~/db.server";
6+
import { PrismaClient, Prisma, prisma, sqlDatabaseSchema } from "~/db.server";
77
import { Organization } from "~/models/organization.server";
88
import { Project } from "~/models/project.server";
99
import { User } from "~/models/user.server";
@@ -122,7 +122,7 @@ export class JobListPresenter {
122122
"jobId",
123123
ROW_NUMBER() OVER(PARTITION BY "jobId" ORDER BY "createdAt" DESC) as rn
124124
FROM
125-
"JobRun"
125+
${sqlDatabaseSchema}."JobRun"
126126
WHERE
127127
"jobId" IN (${Prisma.join(jobs.map((j) => j.id))})
128128
) t

apps/webapp/app/presenters/OrgUsagePresenter.server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { estimate } from "@trigger.dev/billing";
2-
import { PrismaClient, prisma } from "~/db.server";
2+
import { sqlDatabaseSchema, PrismaClient, prisma } from "~/db.server";
33
import { featuresForRequest } from "~/features.server";
44
import { BillingService } from "~/services/billing.server";
55

@@ -53,7 +53,7 @@ export class OrgUsagePresenter {
5353
month: string;
5454
count: number;
5555
}[]
56-
>`SELECT TO_CHAR("createdAt", 'YYYY-MM') as month, COUNT(*) as count FROM "JobRun" WHERE "organizationId" = ${organization.id} AND "createdAt" >= NOW() - INTERVAL '6 months' AND "internal" = FALSE GROUP BY month ORDER BY month ASC`;
56+
>`SELECT TO_CHAR("createdAt", 'YYYY-MM') as month, COUNT(*) as count FROM ${sqlDatabaseSchema}."JobRun" WHERE "organizationId" = ${organization.id} AND "createdAt" >= NOW() - INTERVAL '6 months' AND "internal" = FALSE GROUP BY month ORDER BY month ASC`;
5757

5858
const hasMonthlyRunData = monthlyRunsDataRaw.length > 0;
5959
const monthlyRunsData = monthlyRunsDataRaw.map((obj) => ({
@@ -117,7 +117,7 @@ export class OrgUsagePresenter {
117117

118118
const dailyRunsRawData = await this.#prismaClient.$queryRaw<
119119
{ day: Date; runs: BigInt }[]
120-
>`SELECT date_trunc('day', "createdAt") as day, COUNT(*) as runs FROM "JobRun" WHERE "organizationId" = ${organization.id} AND "createdAt" >= NOW() - INTERVAL '30 days' AND "internal" = FALSE GROUP BY day`;
120+
>`SELECT date_trunc('day', "createdAt") as day, COUNT(*) as runs FROM ${sqlDatabaseSchema}."JobRun" WHERE "organizationId" = ${organization.id} AND "createdAt" >= NOW() - INTERVAL '30 days' AND "internal" = FALSE GROUP BY day`;
121121

122122
const hasDailyRunsData = dailyRunsRawData.length > 0;
123123
const dailyRunsDataFilledIn = fillInMissingDailyRuns(ThirtyDaysAgo, 31, dailyRunsRawData);

apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { WorkerDeploymentStatus } from "@trigger.dev/database";
2-
import { PrismaClient, prisma } from "~/db.server";
2+
import { sqlDatabaseSchema, PrismaClient, prisma } from "~/db.server";
33
import { Organization } from "~/models/organization.server";
44
import { Project } from "~/models/project.server";
55
import { User } from "~/models/user.server";
@@ -97,7 +97,7 @@ export class DeploymentListPresenter {
9797
wd."id",
9898
wd."shortCode",
9999
wd."version",
100-
(SELECT COUNT(*) FROM "BackgroundWorkerTask" WHERE "BackgroundWorkerTask"."workerId" = wd."workerId") AS "tasksCount",
100+
(SELECT COUNT(*) FROM ${sqlDatabaseSchema}."BackgroundWorkerTask" WHERE "BackgroundWorkerTask"."workerId" = wd."workerId") AS "tasksCount",
101101
wd."environmentId",
102102
wd."status",
103103
u."id" AS "userId",
@@ -106,9 +106,9 @@ export class DeploymentListPresenter {
106106
u."avatarUrl" AS "userAvatarUrl",
107107
wd."deployedAt"
108108
FROM
109-
"WorkerDeployment" as wd
109+
${sqlDatabaseSchema}."WorkerDeployment" as wd
110110
INNER JOIN
111-
"User" as u ON wd."triggeredById" = u."id"
111+
${sqlDatabaseSchema}."User" as u ON wd."triggeredById" = u."id"
112112
WHERE
113113
wd."projectId" = ${project.id}
114114
ORDER BY

apps/webapp/app/presenters/v3/EditSchedulePresenter.server.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,13 @@ export class EditSchedulePresenter {
5858
},
5959
});
6060

61-
const possibleTasks = await this.#prismaClient.$queryRaw<{ slug: string }[]>`
62-
SELECT DISTINCT(slug)
63-
FROM "BackgroundWorkerTask"
64-
WHERE "projectId" = ${project.id}
65-
AND "triggerSource" = 'SCHEDULED';
66-
`;
61+
const possibleTasks = await this.#prismaClient.backgroundWorkerTask.findMany({
62+
distinct: ["slug"],
63+
where: {
64+
projectId: project.id,
65+
triggerSource: "SCHEDULED",
66+
},
67+
});
6768

6869
const possibleEnvironments = project.environments.map((environment) => {
6970
let userName: undefined | string;

apps/webapp/app/presenters/v3/RunListPresenter.server.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Prisma, TaskRunStatus } from "@trigger.dev/database";
22
import { Direction } from "~/components/runs/RunStatuses";
3-
import { PrismaClient, prisma } from "~/db.server";
3+
import { sqlDatabaseSchema, PrismaClient, prisma } from "~/db.server";
44
import { getUsername } from "~/utils/username";
55
import { CANCELLABLE_STATUSES } from "~/v3/services/cancelTaskRun.server";
66

@@ -85,11 +85,12 @@ export class RunListPresenter {
8585
});
8686

8787
//get all possible tasks
88-
const possibleTasks = await this.#prismaClient.$queryRaw<{ slug: string }[]>`
89-
SELECT DISTINCT(slug)
90-
FROM "BackgroundWorkerTask"
91-
WHERE "projectId" = ${project.id};
92-
`;
88+
const possibleTasks = await this.#prismaClient.backgroundWorkerTask.findMany({
89+
distinct: ["slug"],
90+
where: {
91+
projectId: project.id,
92+
},
93+
});
9394

9495
//get the runs
9596
let runs = await this.#prismaClient.$queryRaw<
@@ -122,15 +123,15 @@ export class RunListPresenter {
122123
tr."isTest" AS "isTest",
123124
COUNT(tra.id) AS attempts
124125
FROM
125-
"TaskRun" tr
126+
${sqlDatabaseSchema}."TaskRun" tr
126127
LEFT JOIN
127128
(
128129
SELECT *,
129130
ROW_NUMBER() OVER (PARTITION BY "taskRunId" ORDER BY "createdAt" DESC) rn
130-
FROM "TaskRunAttempt"
131+
FROM ${sqlDatabaseSchema}."TaskRunAttempt"
131132
) tra ON tr.id = tra."taskRunId" AND tra.rn = 1
132133
LEFT JOIN
133-
"BackgroundWorker" bw ON tra."backgroundWorkerId" = bw.id
134+
${sqlDatabaseSchema}."BackgroundWorker" bw ON tra."backgroundWorkerId" = bw.id
134135
WHERE
135136
-- project
136137
tr."projectId" = ${project.id}

apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Prisma, RuntimeEnvironmentType } from "@trigger.dev/database";
22
import { ScheduleListFilters } from "~/components/runs/v3/ScheduleFilters";
3-
import { PrismaClient, prisma } from "~/db.server";
3+
import { PrismaClient, prisma, sqlDatabaseSchema } from "~/db.server";
44
import { getUsername } from "~/utils/username";
55
import { calculateNextScheduledTimestamp } from "~/v3/utils/calculateNextSchedule.server";
66

@@ -81,12 +81,13 @@ export class ScheduleListPresenter {
8181
});
8282

8383
//get all possible scheduled tasks
84-
const possibleTasks = await this.#prismaClient.$queryRaw<{ slug: string }[]>`
85-
SELECT DISTINCT(slug)
86-
FROM "BackgroundWorkerTask"
87-
WHERE "projectId" = ${project.id}
88-
AND "triggerSource" = 'SCHEDULED';
89-
`;
84+
const possibleTasks = await this.#prismaClient.backgroundWorkerTask.findMany({
85+
distinct: ["slug"],
86+
where: {
87+
projectId: project.id,
88+
triggerSource: "SCHEDULED",
89+
},
90+
});
9091

9192
//do this here to protect against SQL injection
9293
search = search && search !== "" ? `%${search}%` : undefined;
@@ -201,11 +202,11 @@ export class ScheduleListPresenter {
201202
SELECT t."scheduleId", t."createdAt"
202203
FROM (
203204
SELECT "scheduleId", MAX("createdAt") as "LatestRun"
204-
FROM "TaskRun"
205+
FROM ${sqlDatabaseSchema}."TaskRun"
205206
WHERE "scheduleId" IN (${Prisma.join(rawSchedules.map((s) => s.id))})
206207
GROUP BY "scheduleId"
207208
) r
208-
JOIN "TaskRun" t
209+
JOIN ${sqlDatabaseSchema}."TaskRun" t
209210
ON t."scheduleId" = r."scheduleId" AND t."createdAt" = r."LatestRun";`
210211
: [];
211212

apps/webapp/app/presenters/v3/TaskListPresenter.server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Prisma, TaskRunStatus, TaskTriggerSource } from "@trigger.dev/database";
2-
import { PrismaClient, prisma } from "~/db.server";
2+
import { PrismaClient, prisma, sqlDatabaseSchema } from "~/db.server";
33
import { Organization } from "~/models/organization.server";
44
import { Project } from "~/models/project.server";
55
import { User } from "~/models/user.server";
@@ -73,7 +73,7 @@ export class TaskListPresenter {
7373
bwt."createdAt",
7474
bwt."triggerSource"
7575
FROM
76-
"BackgroundWorkerTask" as bwt
76+
${sqlDatabaseSchema}."BackgroundWorkerTask" as bwt
7777
WHERE bwt."projectId" = ${project.id}
7878
ORDER BY
7979
bwt.slug,
@@ -101,7 +101,7 @@ export class TaskListPresenter {
101101
"lockedById",
102102
ROW_NUMBER() OVER (PARTITION BY "lockedById" ORDER BY "updatedAt" DESC) AS rn
103103
FROM
104-
"TaskRun"
104+
${sqlDatabaseSchema}."TaskRun"
105105
WHERE
106106
"lockedById" IN(${Prisma.join(tasks.map((t) => t.id))})
107107
) t

apps/webapp/app/presenters/v3/TestPresenter.server.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { TaskTriggerSource } from "@trigger.dev/database";
2-
import { PrismaClient, prisma } from "~/db.server";
2+
import { sqlDatabaseSchema, PrismaClient, prisma } from "~/db.server";
33
import { TestSearchParams } from "~/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.test/route";
44
import { sortEnvironments } from "~/services/environmentSort.server";
55
import { createSearchParams } from "~/utils/searchParams";
@@ -97,14 +97,14 @@ export class TestPresenter {
9797
bw.*,
9898
ROW_NUMBER() OVER(ORDER BY string_to_array(bw.version, '.')::int[] DESC) AS rn
9999
FROM
100-
"BackgroundWorker" bw
100+
${sqlDatabaseSchema}."BackgroundWorker" bw
101101
WHERE "runtimeEnvironmentId" = ${matchingEnvironment.id}
102102
),
103103
latest_workers AS (SELECT * FROM workers WHERE rn = 1)
104-
SELECT "BackgroundWorkerTask".id, version, slug as "taskIdentifier", "filePath", "exportName", "BackgroundWorkerTask"."friendlyId", "BackgroundWorkerTask"."triggerSource"
104+
SELECT bwt.id, version, slug as "taskIdentifier", "filePath", "exportName", bwt."friendlyId"
105105
FROM latest_workers
106-
JOIN "BackgroundWorkerTask" ON "BackgroundWorkerTask"."workerId" = latest_workers.id
107-
ORDER BY "BackgroundWorkerTask"."exportName" ASC;
106+
JOIN ${sqlDatabaseSchema}."BackgroundWorkerTask" bwt ON bwt."workerId" = latest_workers.id
107+
ORDER BY bwt."exportName" ASC;
108108
`;
109109

110110
return {

apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
TaskRunStatus,
66
TaskTriggerSource,
77
} from "@trigger.dev/database";
8-
import { PrismaClient, prisma } from "~/db.server";
8+
import { sqlDatabaseSchema, PrismaClient, prisma } from "~/db.server";
99
import { getUsername } from "~/utils/username";
1010

1111
type TestTaskOptions = {
@@ -107,9 +107,9 @@ export class TestTaskPresenter {
107107
SELECT
108108
tr.*
109109
FROM
110-
"TaskRun" as tr
110+
${sqlDatabaseSchema}."TaskRun" as tr
111111
JOIN
112-
"BackgroundWorkerTask" as bwt
112+
${sqlDatabaseSchema}."BackgroundWorkerTask" as bwt
113113
ON
114114
tr."taskIdentifier" = bwt.slug
115115
WHERE

apps/webapp/app/utils/db.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export function isValidDatabaseUrl(url: string) {
2+
try {
3+
const databaseUrl = new URL(url);
4+
const schemaFromSearchParam = databaseUrl.searchParams.get("schema");
5+
6+
if (schemaFromSearchParam === "") {
7+
console.error(
8+
"Invalid Database URL: The schema search param can't have an empty value. To use the `public` schema, either omit the schema param entirely or specify it in full: `?schema=public`"
9+
);
10+
return false;
11+
}
12+
13+
return true;
14+
} catch (err) {
15+
console.error(err);
16+
return false;
17+
}
18+
}

0 commit comments

Comments
 (0)