Skip to content

Commit fce2f38

Browse files
committed
Improve the SDK function types and expose a new APIError instead of the APIResult type
1 parent fde939a commit fce2f38

File tree

19 files changed

+480
-262
lines changed

19 files changed

+480
-262
lines changed

.changeset/ninety-pets-travel.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@trigger.dev/sdk": patch
3+
"@trigger.dev/core": patch
4+
---
5+
6+
Improve the SDK function types and expose a new APIError instead of the APIResult type

apps/webapp/app/routes/api.v1.schedules.$scheduleId.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { prisma } from "~/db.server";
66
import { ViewSchedulePresenter } from "~/presenters/v3/ViewSchedulePresenter.server";
77
import { authenticateApiRequest } from "~/services/apiAuth.server";
88
import { UpsertSchedule } from "~/v3/schedules";
9+
import { ServiceValidationError } from "~/v3/services/baseService.server";
910
import { UpsertTaskScheduleService } from "~/v3/services/upsertTaskSchedule.server";
1011

1112
const ParamsSchema = z.object({
@@ -87,6 +88,10 @@ export async function action({ request, params }: ActionFunctionArgs) {
8788

8889
return json(responseObject, { status: 200 });
8990
} catch (error) {
91+
if (error instanceof ServiceValidationError) {
92+
return json({ error: error.message }, { status: 422 });
93+
}
94+
9095
return json(
9196
{ error: error instanceof Error ? error.message : "Internal Server Error" },
9297
{ status: 500 }

apps/webapp/app/routes/api.v1.schedules.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { z } from "zod";
55
import { ScheduleListPresenter } from "~/presenters/v3/ScheduleListPresenter.server";
66
import { authenticateApiRequest } from "~/services/apiAuth.server";
77
import { UpsertSchedule } from "~/v3/schedules";
8+
import { ServiceValidationError } from "~/v3/services/baseService.server";
89
import { UpsertTaskScheduleService } from "~/v3/services/upsertTaskSchedule.server";
910

1011
const SearchParamsSchema = z.object({
@@ -63,6 +64,10 @@ export async function action({ request }: ActionFunctionArgs) {
6364

6465
return json(responseObject, { status: 200 });
6566
} catch (error) {
67+
if (error instanceof ServiceValidationError) {
68+
return json({ error: error.message }, { status: 422 });
69+
}
70+
6671
return json(
6772
{ error: error instanceof Error ? error.message : "Internal Server Error" },
6873
{ status: 500 }

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,10 @@ export abstract class BaseService {
3232
);
3333
}
3434
}
35+
36+
export class ServiceValidationError extends Error {
37+
constructor(message: string) {
38+
super(message);
39+
this.name = "ServiceValidationError";
40+
}
41+
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ZodError } from "zod";
44
import { $transaction, PrismaClientOrTransaction } from "~/db.server";
55
import { generateFriendlyId } from "../friendlyIdentifiers";
66
import { CronPattern, UpsertSchedule } from "../schedules";
7-
import { BaseService } from "./baseService.server";
7+
import { BaseService, ServiceValidationError } from "./baseService.server";
88
import { RegisterNextTaskScheduleInstanceService } from "./registerNextTaskScheduleInstance.server";
99
import cronstrue from "cronstrue";
1010
import { calculateNextScheduledTimestamp } from "../utils/calculateNextSchedule.server";
@@ -32,10 +32,10 @@ export class UpsertTaskScheduleService extends BaseService {
3232
CronPattern.parse(schedule.cron);
3333
} catch (e) {
3434
if (e instanceof ZodError) {
35-
throw new Error(`Invalid cron expression: ${e.issues[0].message}`);
35+
throw new ServiceValidationError(`Invalid cron expression: ${e.issues[0].message}`);
3636
}
3737

38-
throw new Error(
38+
throw new ServiceValidationError(
3939
`Invalid cron expression: ${e instanceof Error ? e.message : JSON.stringify(e)}`
4040
);
4141
}

docs/v3-openapi.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040
"400": {
4141
"description": "Invalid request parameters"
4242
},
43+
"422": {
44+
"description": "Unprocessable Entity"
45+
},
4346
"401": {
4447
"description": "Unauthorized"
4548
}
@@ -216,6 +219,9 @@
216219
},
217220
"404": {
218221
"description": "Resource not found"
222+
},
223+
"422": {
224+
"description": "Unprocessable Entity"
219225
}
220226
},
221227
"tags": [

packages/core/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@
7676
"superjson": "^2.2.1",
7777
"ulidx": "^2.2.1",
7878
"zod": "3.22.3",
79-
"zod-error": "1.5.0"
79+
"zod-error": "1.5.0",
80+
"zod-validation-error": "^1.5.0"
8081
},
8182
"devDependencies": {
8283
"@trigger.dev/tsconfig": "workspace:*",

packages/core/src/v3/apiClient/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { context, propagation } from "@opentelemetry/api";
2-
import { ZodFetchOptions, zodfetch } from "../../zodfetch";
2+
import { ZodFetchOptions, zodfetch } from "../zodfetch";
33
import {
44
BatchTriggerTaskRequestBody,
55
BatchTriggerTaskResponse,
@@ -25,7 +25,7 @@ export type TriggerOptions = {
2525

2626
const zodFetchOptions: ZodFetchOptions = {
2727
retry: {
28-
maxAttempts: 5,
28+
maxAttempts: 3,
2929
minTimeoutInMs: 1000,
3030
maxTimeoutInMs: 30_000,
3131
factor: 2,

packages/core/src/v3/apiErrors.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
export type APIHeaders = Record<string, string | null | undefined>;
2+
3+
export class APIError extends Error {
4+
readonly status: number | undefined;
5+
readonly headers: APIHeaders | undefined;
6+
readonly error: Object | undefined;
7+
8+
readonly code: string | null | undefined;
9+
readonly param: string | null | undefined;
10+
readonly type: string | undefined;
11+
12+
constructor(
13+
status: number | undefined,
14+
error: Object | undefined,
15+
message: string | undefined,
16+
headers: APIHeaders | undefined
17+
) {
18+
super(`${APIError.makeMessage(status, error, message)}`);
19+
this.status = status;
20+
this.headers = headers;
21+
22+
const data = error as Record<string, any>;
23+
this.error = data;
24+
this.code = data?.["code"];
25+
this.param = data?.["param"];
26+
this.type = data?.["type"];
27+
}
28+
29+
private static makeMessage(status: number | undefined, error: any, message: string | undefined) {
30+
const msg = error?.message
31+
? typeof error.message === "string"
32+
? error.message
33+
: JSON.stringify(error.message)
34+
: error
35+
? JSON.stringify(error)
36+
: message;
37+
38+
if (status && msg) {
39+
return `${status} ${msg}`;
40+
}
41+
if (status) {
42+
return `${status} status code (no body)`;
43+
}
44+
if (msg) {
45+
return msg;
46+
}
47+
return "(no status code or body)";
48+
}
49+
50+
static generate(
51+
status: number | undefined,
52+
errorResponse: Object | undefined,
53+
message: string | undefined,
54+
headers: APIHeaders | undefined
55+
) {
56+
if (!status) {
57+
return new APIConnectionError({ cause: castToError(errorResponse) });
58+
}
59+
60+
const error = (errorResponse as Record<string, any>)?.["error"];
61+
62+
if (status === 400) {
63+
return new BadRequestError(status, error, message, headers);
64+
}
65+
66+
if (status === 401) {
67+
return new AuthenticationError(status, error, message, headers);
68+
}
69+
70+
if (status === 403) {
71+
return new PermissionDeniedError(status, error, message, headers);
72+
}
73+
74+
if (status === 404) {
75+
return new NotFoundError(status, error, message, headers);
76+
}
77+
78+
if (status === 409) {
79+
return new ConflictError(status, error, message, headers);
80+
}
81+
82+
if (status === 422) {
83+
return new UnprocessableEntityError(status, error, message, headers);
84+
}
85+
86+
if (status === 429) {
87+
return new RateLimitError(status, error, message, headers);
88+
}
89+
90+
if (status >= 500) {
91+
return new InternalServerError(status, error, message, headers);
92+
}
93+
94+
return new APIError(status, error, message, headers);
95+
}
96+
}
97+
98+
export class APIConnectionError extends APIError {
99+
override readonly status: undefined = undefined;
100+
101+
constructor({ message, cause }: { message?: string; cause?: Error | undefined }) {
102+
super(undefined, undefined, message || "Connection error.", undefined);
103+
// in some environments the 'cause' property is already declared
104+
// @ts-ignore
105+
if (cause) this.cause = cause;
106+
}
107+
}
108+
109+
export class BadRequestError extends APIError {
110+
override readonly status: 400 = 400;
111+
}
112+
113+
export class AuthenticationError extends APIError {
114+
override readonly status: 401 = 401;
115+
}
116+
117+
export class PermissionDeniedError extends APIError {
118+
override readonly status: 403 = 403;
119+
}
120+
121+
export class NotFoundError extends APIError {
122+
override readonly status: 404 = 404;
123+
}
124+
125+
export class ConflictError extends APIError {
126+
override readonly status: 409 = 409;
127+
}
128+
129+
export class UnprocessableEntityError extends APIError {
130+
override readonly status: 422 = 422;
131+
}
132+
133+
export class RateLimitError extends APIError {
134+
override readonly status: 429 = 429;
135+
}
136+
137+
export class InternalServerError extends APIError {}
138+
139+
function castToError(err: any): Error {
140+
if (err instanceof Error) return err;
141+
return new Error(err);
142+
}

packages/core/src/v3/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from "./zodNamespace";
77
export * from "./zodSocket";
88
export * from "./zodIpc";
99
export * from "./errors";
10+
export * from "./apiErrors";
1011
export * from "./runtime-api";
1112
export * from "./logger-api";
1213
export * from "./clock-api";

packages/core/src/v3/utils/ioSerialization.ts

Lines changed: 29 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -110,27 +110,25 @@ async function exportPacket(packet: IOPacket, pathPrefix: string): Promise<IOPac
110110

111111
const presignedResponse = await apiClientManager.client!.createUploadPayloadUrl(filename);
112112

113-
if (presignedResponse.ok) {
114-
const uploadResponse = await fetch(presignedResponse.data.presignedUrl, {
115-
method: "PUT",
116-
headers: {
117-
"Content-Type": packet.dataType,
118-
},
119-
body: packet.data,
120-
});
121-
122-
if (!uploadResponse.ok) {
123-
throw new Error(
124-
`Failed to upload output to ${presignedResponse.data.presignedUrl}: ${uploadResponse.statusText}`
125-
);
126-
}
127-
128-
return {
129-
data: filename,
130-
dataType: "application/store",
131-
};
113+
const uploadResponse = await fetch(presignedResponse.presignedUrl, {
114+
method: "PUT",
115+
headers: {
116+
"Content-Type": packet.dataType,
117+
},
118+
body: packet.data,
119+
});
120+
121+
if (!uploadResponse.ok) {
122+
throw new Error(
123+
`Failed to upload output to ${presignedResponse.presignedUrl}: ${uploadResponse.statusText}`
124+
);
132125
}
133126

127+
return {
128+
data: filename,
129+
dataType: "application/store",
130+
};
131+
134132
return packet;
135133
}
136134

@@ -172,24 +170,22 @@ async function importPacket(packet: IOPacket, span?: Span): Promise<IOPacket> {
172170

173171
const presignedResponse = await apiClientManager.client.getPayloadUrl(packet.data);
174172

175-
if (presignedResponse.ok) {
176-
const response = await fetch(presignedResponse.data.presignedUrl);
173+
const response = await fetch(presignedResponse.presignedUrl);
177174

178-
if (!response.ok) {
179-
throw new Error(
180-
`Failed to import packet ${presignedResponse.data.presignedUrl}: ${response.statusText}`
181-
);
182-
}
175+
if (!response.ok) {
176+
throw new Error(
177+
`Failed to import packet ${presignedResponse.presignedUrl}: ${response.statusText}`
178+
);
179+
}
183180

184-
const data = await response.text();
181+
const data = await response.text();
185182

186-
span?.setAttribute("size", Buffer.byteLength(data, "utf8"));
183+
span?.setAttribute("size", Buffer.byteLength(data, "utf8"));
187184

188-
return {
189-
data,
190-
dataType: response.headers.get("content-type") ?? "application/json",
191-
};
192-
}
185+
return {
186+
data,
187+
dataType: response.headers.get("content-type") ?? "application/json",
188+
};
193189

194190
return packet;
195191
}

0 commit comments

Comments
 (0)