Files
triggerdotdev--trigger.dev/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts
Matt Aitken c405ae7117 Schedule limits and timezone support (#1165)
* Added maximumScheduleInstancesLimit column to Org, default to 20

* Docs on the schedule limits and improved soft-limit communication

* Added limit info to the schedules list page

* Created a task that creates schedules, useful for testing

* Make deduplicationKey required when creating/updating a schedule using the SDK

* New schedule button shows an alert if you’re over the limit

* Added timezone to the form and db

* WIP on the timezone dropdown for the create/edit schedule form

* Use the new filter search for timezones

* Made the timezone dropdown faster by fixing the virtualization

* The preview table is working and added a nice message about daylight savings

* Created a page where you can view the full list of timezones

The URL is included in the error message if you send an invalid time using the SDK

* Creating tasks with the timezone

* Added timezone support the the scheduler and the schedules list

* Added timezone support to more of the schedules UI

* The timezone comes through to scheduled runs with nice JSDocs

* Allow setting the timezone from the SDK

* Always have a timezone on a schedule

* Updated jsdocs

* Updated catalog example

* Changed the column to be a string, not null. Added the timezone across the SDK

* API endpoint for getting the timezones

* Added an SDK function to get the list of timezones

* Added timezones to the docs

* Changeset: Added timezone support to schedules

* Added support for testing timezone

* Tidied up imports

* Imports

* Imports

* Update limits.mdx

* Fixed a couple type issues and use the already exported zodfetch

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2024-06-14 13:31:27 +01:00

209 lines
5.1 KiB
TypeScript

import { ScheduledTaskPayload, parsePacket, prettyPrintPacket } from "@trigger.dev/core/v3";
import {
RuntimeEnvironmentType,
TaskRunAttemptStatus,
TaskRunStatus,
TaskTriggerSource,
} from "@trigger.dev/database";
import { sqlDatabaseSchema, PrismaClient, prisma } from "~/db.server";
import { getTimezones } from "~/utils/timezones.server";
import { getUsername } from "~/utils/username";
type TestTaskOptions = {
userId: string;
projectSlug: string;
taskFriendlyId: string;
};
type Task = {
id: string;
taskIdentifier: string;
filePath: string;
exportName: string;
friendlyId: string;
environment: {
id: string;
type: RuntimeEnvironmentType;
userId?: string;
userName?: string;
};
};
export type TestTask =
| {
triggerSource: "STANDARD";
task: Task;
runs: StandardRun[];
}
| {
triggerSource: "SCHEDULED";
task: Task;
possibleTimezones: string[];
runs: ScheduledRun[];
};
type RawRun = {
id: string;
number: BigInt;
friendlyId: string;
createdAt: Date;
status: TaskRunStatus;
payload: string;
payloadType: string;
runtimeEnvironmentId: string;
};
export type StandardRun = Omit<RawRun, "number"> & {
number: number;
};
export type ScheduledRun = Omit<RawRun, "number" | "payload"> & {
number: number;
payload: {
timestamp: Date;
lastTimestamp?: Date;
externalId?: string;
timezone: string;
};
};
export class TestTaskPresenter {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call({ userId, projectSlug, taskFriendlyId }: TestTaskOptions): Promise<TestTask> {
const task = await this.#prismaClient.backgroundWorkerTask.findFirstOrThrow({
select: {
id: true,
filePath: true,
exportName: true,
slug: true,
triggerSource: true,
runtimeEnvironment: {
select: {
id: true,
type: true,
orgMember: {
select: {
user: {
select: {
id: true,
name: true,
displayName: true,
},
},
},
},
},
},
},
where: {
friendlyId: taskFriendlyId,
},
});
const latestRuns = await this.#prismaClient.$queryRaw<RawRun[]>`
WITH taskruns AS (
SELECT
tr.*
FROM
${sqlDatabaseSchema}."TaskRun" as tr
JOIN
${sqlDatabaseSchema}."BackgroundWorkerTask" as bwt
ON
tr."taskIdentifier" = bwt.slug
WHERE
bwt."friendlyId" = ${taskFriendlyId} AND
tr."runtimeEnvironmentId" = ${task.runtimeEnvironment.id}
ORDER BY
tr."createdAt" DESC
LIMIT 5
)
SELECT
taskr.id,
taskr.number,
taskr."friendlyId",
taskr."taskIdentifier",
taskr."createdAt",
taskr.status,
taskr.payload,
taskr."payloadType",
taskr."runtimeEnvironmentId"
FROM
taskruns AS taskr
WHERE
taskr."payloadType" = 'application/json' OR taskr."payloadType" = 'application/super+json'
ORDER BY
taskr."createdAt" DESC;`;
const taskWithEnvironment = {
id: task.id,
taskIdentifier: task.slug,
filePath: task.filePath,
exportName: task.exportName,
friendlyId: taskFriendlyId,
environment: {
id: task.runtimeEnvironment.id,
type: task.runtimeEnvironment.type,
userId: task.runtimeEnvironment.orgMember?.user.id,
userName: getUsername(task.runtimeEnvironment.orgMember?.user),
},
};
switch (task.triggerSource) {
case "STANDARD":
return {
triggerSource: "STANDARD",
task: taskWithEnvironment,
runs: await Promise.all(
latestRuns.map(async (r) => {
const number = Number(r.number);
return {
...r,
number,
payload: await prettyPrintPacket(r.payload, r.payloadType),
};
})
),
};
case "SCHEDULED":
const possibleTimezones = getTimezones();
return {
triggerSource: "SCHEDULED",
task: taskWithEnvironment,
possibleTimezones,
runs: (
await Promise.all(
latestRuns.map(async (r) => {
const number = Number(r.number);
const payload = await getScheduleTaskRunPayload(r);
if (payload.success) {
return {
...r,
number,
payload: payload.data,
};
}
})
)
).filter(Boolean),
};
}
}
}
async function getScheduleTaskRunPayload(run: RawRun) {
const payload = await parsePacket({ data: run.payload, dataType: run.payloadType });
if (!payload.timezone) {
payload.timezone = "UTC";
}
const parsed = ScheduledTaskPayload.safeParse(payload);
return parsed;
}