Skip to content

Commit 7552e1c

Browse files
committed
Throw errors and push them through to the CLI dev command
1 parent a780f8a commit 7552e1c

File tree

4 files changed

+157
-95
lines changed

4 files changed

+157
-95
lines changed

apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { CreateBackgroundWorkerRequestBody } from "@trigger.dev/core/v3";
33
import { z } from "zod";
44
import { authenticateApiRequest } from "~/services/apiAuth.server";
55
import { logger } from "~/services/logger.server";
6-
import { CreateBackgroundWorkerService } from "~/v3/services/createBackgroundWorker.server";
6+
import { ServiceValidationError } from "~/v3/services/baseService.server";
7+
import {
8+
CreateBackgroundWorkerService,
9+
CreateDeclarativeScheduleError,
10+
} from "~/v3/services/createBackgroundWorker.server";
711

812
const ParamsSchema = z.object({
913
projectRef: z.string(),
@@ -42,14 +46,26 @@ export async function action({ request, params }: ActionFunctionArgs) {
4246

4347
const service = new CreateBackgroundWorkerService();
4448

45-
const backgroundWorker = await service.call(projectRef, authenticatedEnv, body.data);
49+
try {
50+
const backgroundWorker = await service.call(projectRef, authenticatedEnv, body.data);
4651

47-
return json(
48-
{
49-
id: backgroundWorker.friendlyId,
50-
version: backgroundWorker.version,
51-
contentHash: backgroundWorker.contentHash,
52-
},
53-
{ status: 200 }
54-
);
52+
return json(
53+
{
54+
id: backgroundWorker.friendlyId,
55+
version: backgroundWorker.version,
56+
contentHash: backgroundWorker.contentHash,
57+
},
58+
{ status: 200 }
59+
);
60+
} catch (e) {
61+
logger.error("Failed to create background worker", { error: e });
62+
63+
if (e instanceof ServiceValidationError) {
64+
return json({ error: e.message }, { status: 400 });
65+
} else if (e instanceof CreateDeclarativeScheduleError) {
66+
return json({ error: e.message }, { status: 400 });
67+
}
68+
69+
return json({ error: "Failed to create background worker" }, { status: 500 });
70+
}
5571
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { ZodError } from "zod";
2+
import { CronPattern } from "../schedules";
3+
import { BaseService, ServiceValidationError } from "./baseService.server";
4+
import { getLimit } from "~/services/platform.v3.server";
5+
import { getTimezones } from "~/utils/timezones.server";
6+
import { env } from "~/env.server";
7+
8+
type Schedule = {
9+
cron: string;
10+
timezone?: string;
11+
taskIdentifier: string;
12+
friendlyId?: string;
13+
};
14+
15+
export class CheckScheduleService extends BaseService {
16+
public async call(projectId: string, schedule: Schedule) {
17+
//validate the cron expression
18+
try {
19+
CronPattern.parse(schedule.cron);
20+
} catch (e) {
21+
if (e instanceof ZodError) {
22+
throw new ServiceValidationError(`Invalid cron expression: ${e.issues[0].message}`);
23+
}
24+
25+
throw new ServiceValidationError(
26+
`Invalid cron expression: ${e instanceof Error ? e.message : JSON.stringify(e)}`
27+
);
28+
}
29+
30+
//chek it's a valid timezone
31+
if (schedule.timezone) {
32+
const possibleTimezones = getTimezones();
33+
if (!possibleTimezones.includes(schedule.timezone)) {
34+
throw new ServiceValidationError(
35+
`Invalid IANA timezone: '${schedule.timezone}'. View the list of valid timezones at ${env.APP_ORIGIN}/timezones`
36+
);
37+
}
38+
}
39+
40+
//check the task exists
41+
const task = await this._prisma.backgroundWorkerTask.findFirst({
42+
where: {
43+
slug: schedule.taskIdentifier,
44+
projectId: projectId,
45+
},
46+
orderBy: {
47+
createdAt: "desc",
48+
},
49+
});
50+
51+
if (!task) {
52+
throw new ServiceValidationError(
53+
`Task with identifier ${schedule.taskIdentifier} not found in project.`
54+
);
55+
}
56+
57+
if (task.triggerSource !== "SCHEDULED") {
58+
throw new ServiceValidationError(
59+
`Task with identifier ${schedule.taskIdentifier} is not a scheduled task.`
60+
);
61+
}
62+
63+
//if creating a schedule, check they're under the limits
64+
if (!schedule.friendlyId) {
65+
//check they're within their limit
66+
const project = await this._prisma.project.findFirst({
67+
where: {
68+
id: projectId,
69+
},
70+
select: {
71+
organizationId: true,
72+
},
73+
});
74+
75+
if (!project) {
76+
throw new ServiceValidationError("Project not found");
77+
}
78+
79+
const limit = await getLimit(project.organizationId, "schedules", 500);
80+
const schedulesCount = await this._prisma.taskSchedule.count({
81+
where: {
82+
projectId,
83+
},
84+
});
85+
86+
if (schedulesCount >= limit) {
87+
throw new ServiceValidationError(
88+
`You have created ${schedulesCount}/${limit} schedules so you'll need to increase your limits or delete some schedules. Increase your limits by contacting support.`
89+
);
90+
}
91+
}
92+
}
93+
}

apps/webapp/app/v3/services/createBackgroundWorker.server.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { BaseService } from "./baseService.server";
1010
import { projectPubSub } from "./projectPubSub.server";
1111
import { RegisterNextTaskScheduleInstanceService } from "./registerNextTaskScheduleInstance.server";
1212
import cronstrue from "cronstrue";
13+
import { CheckScheduleService } from "./checkSchedule.server";
1314

1415
export class CreateBackgroundWorkerService extends BaseService {
1516
public async call(
@@ -229,6 +230,14 @@ export async function createBackgroundTasks(
229230
}
230231
}
231232

233+
//CreateDeclarativeScheduleError with a message
234+
export class CreateDeclarativeScheduleError extends Error {
235+
constructor(message: string) {
236+
super(message);
237+
this.name = "CreateDeclarativeScheduleError";
238+
}
239+
}
240+
232241
export async function syncDeclarativeSchedules(
233242
tasks: TaskResource[],
234243
worker: BackgroundWorker,
@@ -251,6 +260,7 @@ export async function syncDeclarativeSchedules(
251260
},
252261
});
253262

263+
const checkSchedule = new CheckScheduleService(prisma);
254264
const registerNextService = new RegisterNextTaskScheduleInstanceService(prisma);
255265

256266
//start out by assuming they're all missing
@@ -260,13 +270,21 @@ export async function syncDeclarativeSchedules(
260270

261271
//create/update schedules (+ instances)
262272
for (const task of tasksWithDeclarativeSchedules) {
273+
if (task.schedule === undefined) continue;
274+
263275
const existingSchedule = existingDeclarativeSchedules.find(
264276
(schedule) =>
265277
schedule.taskIdentifier === task.id &&
266278
schedule.instances.some((instance) => instance.environmentId === environment.id)
267279
);
268280

269-
if (task.schedule === undefined) continue;
281+
//this throws errors if the schedule is invalid
282+
await checkSchedule.call(environment.projectId, {
283+
cron: task.schedule.cron,
284+
timezone: task.schedule.timezone,
285+
taskIdentifier: task.id,
286+
friendlyId: existingSchedule?.friendlyId,
287+
});
270288

271289
if (existingSchedule) {
272290
const schedule = await prisma.taskSchedule.update({
@@ -288,9 +306,9 @@ export async function syncDeclarativeSchedules(
288306
if (instance) {
289307
await registerNextService.call(instance.id);
290308
} else {
291-
logger.error("Missing instance for declarative schedule", {
292-
schedule,
293-
});
309+
throw new CreateDeclarativeScheduleError(
310+
`Missing instance for declarative schedule ${schedule.id}`
311+
);
294312
}
295313
} else {
296314
const newSchedule = await prisma.taskSchedule.create({
@@ -315,7 +333,15 @@ export async function syncDeclarativeSchedules(
315333
},
316334
});
317335

318-
await registerNextService.call(newSchedule.instances[0].id);
336+
const instance = newSchedule.instances.at(0);
337+
338+
if (instance) {
339+
await registerNextService.call(instance.id);
340+
} else {
341+
throw new CreateDeclarativeScheduleError(
342+
`Missing instance for declarative schedule ${newSchedule.id}`
343+
);
344+
}
319345
}
320346
}
321347

apps/webapp/app/v3/services/upsertTaskSchedule.server.ts

Lines changed: 7 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import { Prisma, TaskSchedule } from "@trigger.dev/database";
2+
import cronstrue from "cronstrue";
23
import { nanoid } from "nanoid";
3-
import { ZodError } from "zod";
44
import { $transaction, PrismaClientOrTransaction } from "~/db.server";
55
import { generateFriendlyId } from "../friendlyIdentifiers";
6-
import { CronPattern, UpsertSchedule } from "../schedules";
6+
import { UpsertSchedule } from "../schedules";
7+
import { calculateNextScheduledTimestamp } from "../utils/calculateNextSchedule.server";
78
import { BaseService, ServiceValidationError } from "./baseService.server";
9+
import { CheckScheduleService } from "./checkSchedule.server";
810
import { RegisterNextTaskScheduleInstanceService } from "./registerNextTaskScheduleInstance.server";
9-
import cronstrue from "cronstrue";
10-
import { calculateNextScheduledTimestamp } from "../utils/calculateNextSchedule.server";
11-
import { getTimezones } from "~/utils/timezones.server";
12-
import { env } from "~/env.server";
13-
import { getLimit } from "~/services/platform.v3.server";
1411

1512
export type UpsertTaskScheduleServiceOptions = UpsertSchedule;
1613

@@ -30,79 +27,9 @@ type InstanceWithEnvironment = Prisma.TaskScheduleInstanceGetPayload<{
3027

3128
export class UpsertTaskScheduleService extends BaseService {
3229
public async call(projectId: string, schedule: UpsertTaskScheduleServiceOptions) {
33-
//validate the cron expression
34-
try {
35-
CronPattern.parse(schedule.cron);
36-
} catch (e) {
37-
if (e instanceof ZodError) {
38-
throw new ServiceValidationError(`Invalid cron expression: ${e.issues[0].message}`);
39-
}
40-
41-
throw new ServiceValidationError(
42-
`Invalid cron expression: ${e instanceof Error ? e.message : JSON.stringify(e)}`
43-
);
44-
}
45-
46-
const task = await this._prisma.backgroundWorkerTask.findFirst({
47-
where: {
48-
slug: schedule.taskIdentifier,
49-
projectId: projectId,
50-
},
51-
orderBy: {
52-
createdAt: "desc",
53-
},
54-
});
55-
56-
if (!task) {
57-
throw new ServiceValidationError(
58-
`Task with identifier ${schedule.taskIdentifier} not found in project.`
59-
);
60-
}
61-
62-
if (task.triggerSource !== "SCHEDULED") {
63-
throw new ServiceValidationError(
64-
`Task with identifier ${schedule.taskIdentifier} is not a scheduled task.`
65-
);
66-
}
67-
68-
//if creating a schedule, check they're under the limits
69-
if (!schedule.friendlyId) {
70-
//check they're within their limit
71-
const project = await this._prisma.project.findFirst({
72-
where: {
73-
id: projectId,
74-
},
75-
select: {
76-
organizationId: true,
77-
},
78-
});
79-
80-
if (!project) {
81-
throw new ServiceValidationError("Project not found");
82-
}
83-
84-
const limit = await getLimit(project.organizationId, "schedules", 500);
85-
const schedulesCount = await this._prisma.taskSchedule.count({
86-
where: {
87-
projectId,
88-
},
89-
});
90-
91-
if (schedulesCount >= limit) {
92-
throw new ServiceValidationError(
93-
`You have created ${schedulesCount}/${limit} schedules so you'll need to increase your limits or delete some schedules. Increase your limits by contacting support.`
94-
);
95-
}
96-
}
97-
98-
if (schedule.timezone) {
99-
const possibleTimezones = getTimezones();
100-
if (!possibleTimezones.includes(schedule.timezone)) {
101-
throw new ServiceValidationError(
102-
`Invalid IANA timezone: "${schedule.timezone}". View the list of valid timezones at ${env.APP_ORIGIN}/timezones`
103-
);
104-
}
105-
}
30+
//this throws errors if the schedule is invalid
31+
const checkSchedule = new CheckScheduleService(this._prisma);
32+
await checkSchedule.call(projectId, schedule);
10633

10734
const result = await $transaction(this._prisma, async (tx) => {
10835
const deduplicationKey =

0 commit comments

Comments
 (0)