Skip to content

Commit bcb29e3

Browse files
authored
surface errors (#24)
* surface errors * fix api key handling * fix db errors
1 parent d97ea19 commit bcb29e3

12 files changed

+559
-275
lines changed

lib/steps/create-ticket.ts

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@ import "server-only";
88

99
import { LinearClient } from "@linear/sdk";
1010
import { fetchCredentials } from "../credential-fetcher";
11+
import { getErrorMessage } from "../utils";
12+
13+
type CreateTicketResult =
14+
| { success: true; id: string; url: string; title: string }
15+
| { success: false; error: string };
1116

1217
export async function createTicketStep(input: {
1318
integrationId?: string;
1419
ticketTitle: string;
1520
ticketDescription: string;
16-
}) {
21+
}): Promise<CreateTicketResult> {
1722
"use step";
1823

1924
const credentials = input.integrationId
@@ -24,38 +29,54 @@ export async function createTicketStep(input: {
2429
const teamId = credentials.LINEAR_TEAM_ID;
2530

2631
if (!apiKey) {
27-
throw new Error(
28-
"LINEAR_API_KEY is not configured. Please add it in Project Integrations."
29-
);
32+
return {
33+
success: false,
34+
error:
35+
"LINEAR_API_KEY is not configured. Please add it in Project Integrations.",
36+
};
3037
}
3138

32-
const linear = new LinearClient({ apiKey });
39+
try {
40+
const linear = new LinearClient({ apiKey });
3341

34-
let targetTeamId = teamId;
35-
if (!targetTeamId) {
36-
const teams = await linear.teams();
37-
const firstTeam = teams.nodes[0];
38-
if (!firstTeam) {
39-
throw new Error("No teams found in Linear workspace");
42+
let targetTeamId = teamId;
43+
if (!targetTeamId) {
44+
const teams = await linear.teams();
45+
const firstTeam = teams.nodes[0];
46+
if (!firstTeam) {
47+
return {
48+
success: false,
49+
error: "No teams found in Linear workspace",
50+
};
51+
}
52+
targetTeamId = firstTeam.id;
4053
}
41-
targetTeamId = firstTeam.id;
42-
}
4354

44-
const issuePayload = await linear.createIssue({
45-
title: input.ticketTitle,
46-
description: input.ticketDescription,
47-
teamId: targetTeamId,
48-
});
55+
const issuePayload = await linear.createIssue({
56+
title: input.ticketTitle,
57+
description: input.ticketDescription,
58+
teamId: targetTeamId,
59+
});
4960

50-
const issue = await issuePayload.issue;
61+
const issue = await issuePayload.issue;
5162

52-
if (!issue) {
53-
throw new Error("Failed to create issue");
54-
}
63+
if (!issue) {
64+
return {
65+
success: false,
66+
error: "Failed to create issue",
67+
};
68+
}
5569

56-
return {
57-
id: issue.id,
58-
url: issue.url,
59-
title: issue.title,
60-
};
70+
return {
71+
success: true,
72+
id: issue.id,
73+
url: issue.url,
74+
title: issue.title,
75+
};
76+
} catch (error) {
77+
return {
78+
success: false,
79+
error: `Failed to create ticket: ${getErrorMessage(error)}`,
80+
};
81+
}
6182
}

lib/steps/database-query.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,9 @@ type DatabaseQueryInput = {
1717
query?: string;
1818
};
1919

20-
type DatabaseQueryResult = {
21-
status: string;
22-
rows?: unknown;
23-
count?: number;
24-
error?: string;
25-
};
20+
type DatabaseQueryResult =
21+
| { success: true; rows: unknown; count: number }
22+
| { success: false; error: string };
2623

2724
function validateInput(input: DatabaseQueryInput): string | null {
2825
const queryString = input.dbQuery || input.query;
@@ -50,7 +47,7 @@ async function executeQuery(
5047
return await db.execute(sql.raw(queryString));
5148
}
5249

53-
function getErrorMessage(error: unknown): string {
50+
function getDatabaseErrorMessage(error: unknown): string {
5451
if (!(error instanceof Error)) {
5552
return "Unknown database error";
5653
}
@@ -90,10 +87,7 @@ export async function databaseQueryStep(
9087

9188
const validationError = validateInput(input);
9289
if (validationError) {
93-
return {
94-
status: "error",
95-
error: validationError,
96-
};
90+
return { success: false, error: validationError };
9791
}
9892

9993
const credentials = input.integrationId
@@ -104,7 +98,7 @@ export async function databaseQueryStep(
10498

10599
if (!databaseUrl) {
106100
return {
107-
status: "error",
101+
success: false,
108102
error:
109103
"DATABASE_URL is not configured. Please add it in Project Integrations.",
110104
};
@@ -119,15 +113,15 @@ export async function databaseQueryStep(
119113
await client.end();
120114

121115
return {
122-
status: "success",
116+
success: true,
123117
rows: result,
124118
count: Array.isArray(result) ? result.length : 0,
125119
};
126120
} catch (error) {
127121
await cleanupClient(client);
128122
return {
129-
status: "error",
130-
error: getErrorMessage(error),
123+
success: false,
124+
error: `Database query failed: ${getDatabaseErrorMessage(error)}`,
131125
};
132126
}
133127
}

lib/steps/generate-image.ts

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,20 @@
66
*/
77
import "server-only";
88

9-
import { experimental_generateImage as generateImage } from "ai";
9+
import type { ImageModelV2 } from "@ai-sdk/provider";
10+
import { createGateway, experimental_generateImage as generateImage } from "ai";
1011
import { fetchCredentials } from "../credential-fetcher";
12+
import { getErrorMessageAsync } from "../utils";
13+
14+
type GenerateImageResult =
15+
| { success: true; base64: string }
16+
| { success: false; error: string };
1117

1218
export async function generateImageStep(input: {
1319
integrationId?: string;
14-
model: string;
15-
prompt: string;
16-
}): Promise<{ base64: string | undefined }> {
20+
imageModel: ImageModelV2;
21+
imagePrompt: string;
22+
}): Promise<GenerateImageResult> {
1723
"use step";
1824

1925
const credentials = input.integrationId
@@ -23,29 +29,43 @@ export async function generateImageStep(input: {
2329
const apiKey = credentials.AI_GATEWAY_API_KEY;
2430

2531
if (!apiKey) {
26-
throw new Error(
27-
"AI_GATEWAY_API_KEY is not configured. Please add it in Project Integrations."
28-
);
32+
return {
33+
success: false,
34+
error:
35+
"AI_GATEWAY_API_KEY is not configured. Please add it in Project Integrations.",
36+
};
2937
}
3038

31-
const result = await generateImage({
32-
// biome-ignore lint/suspicious/noExplicitAny: model string needs type coercion for ai package
33-
model: input.model as any,
34-
prompt: input.prompt,
35-
size: "1024x1024",
36-
providerOptions: {
37-
openai: {
38-
apiKey,
39-
},
40-
},
41-
});
42-
43-
if (!result.image) {
44-
throw new Error("Failed to generate image");
45-
}
39+
try {
40+
const gateway = createGateway({
41+
apiKey,
42+
});
43+
44+
// biome-ignore lint/suspicious/noExplicitAny: AI gateway model ID is dynamic
45+
const modelId = (input.imageModel ?? "bfl/flux-2-pro") as any;
46+
const result = await generateImage({
47+
model: gateway.imageModel(modelId),
48+
prompt: input.imagePrompt,
49+
size: "1024x1024",
50+
});
4651

47-
// Convert the GeneratedFile to base64 string
48-
const base64 = result.image.toString();
52+
if (!result.image) {
53+
return {
54+
success: false,
55+
error: "Failed to generate image: No image returned",
56+
};
57+
}
4958

50-
return { base64 };
59+
// Convert the GeneratedFile to base64 string
60+
const base64 = result.image.toString();
61+
62+
return { success: true, base64 };
63+
} catch (error) {
64+
// Extract meaningful error message from AI SDK errors
65+
const message = await getErrorMessageAsync(error);
66+
return {
67+
success: false,
68+
error: `Image generation failed: ${message}`,
69+
};
70+
}
5171
}

lib/steps/generate-text.ts

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,18 @@ import "server-only";
1010
import { generateObject, generateText } from "ai";
1111
import { z } from "zod";
1212
import { fetchCredentials } from "../credential-fetcher";
13+
import { getErrorMessageAsync } from "../utils";
1314

1415
type SchemaField = {
1516
name: string;
1617
type: string;
1718
};
1819

20+
type GenerateTextResult =
21+
| { success: true; text: string }
22+
| { success: true; object: Record<string, unknown> }
23+
| { success: false; error: string };
24+
1925
/**
2026
* Determines the provider from the model ID
2127
*/
@@ -56,7 +62,7 @@ export async function generateTextStep(input: {
5662
aiPrompt?: string;
5763
aiFormat?: string;
5864
aiSchema?: string;
59-
}): Promise<{ text: string } | Record<string, unknown>> {
65+
}): Promise<GenerateTextResult> {
6066
"use step";
6167

6268
const credentials = input.integrationId
@@ -66,23 +72,28 @@ export async function generateTextStep(input: {
6672
const apiKey = credentials.AI_GATEWAY_API_KEY;
6773

6874
if (!apiKey) {
69-
throw new Error(
70-
"AI_GATEWAY_API_KEY is not configured. Please add it in Project Integrations."
71-
);
75+
return {
76+
success: false,
77+
error:
78+
"AI_GATEWAY_API_KEY is not configured. Please add it in Project Integrations.",
79+
};
7280
}
7381

7482
const modelId = input.aiModel || "gpt-5";
7583
const promptText = input.aiPrompt || "";
7684

7785
if (!promptText || promptText.trim() === "") {
78-
throw new Error("Prompt is required for text generation");
86+
return {
87+
success: false,
88+
error: "Prompt is required for text generation",
89+
};
7990
}
8091

8192
const providerName = getProviderFromModel(modelId);
8293
const modelString = `${providerName}/${modelId}`;
8394

84-
if (input.aiFormat === "object" && input.aiSchema) {
85-
try {
95+
try {
96+
if (input.aiFormat === "object" && input.aiSchema) {
8697
const schema = JSON.parse(input.aiSchema) as SchemaField[];
8798
const zodSchema = buildZodSchema(schema);
8899

@@ -95,19 +106,24 @@ export async function generateTextStep(input: {
95106
},
96107
});
97108

98-
return object;
99-
} catch {
100-
// If structured output fails, fall back to text generation
109+
return { success: true, object };
101110
}
102-
}
103111

104-
const { text } = await generateText({
105-
model: modelString,
106-
prompt: promptText,
107-
headers: {
108-
Authorization: `Bearer ${apiKey}`,
109-
},
110-
});
111-
112-
return { text };
112+
const { text } = await generateText({
113+
model: modelString,
114+
prompt: promptText,
115+
headers: {
116+
Authorization: `Bearer ${apiKey}`,
117+
},
118+
});
119+
120+
return { success: true, text };
121+
} catch (error) {
122+
// Extract meaningful error message from AI SDK errors
123+
const message = await getErrorMessageAsync(error);
124+
return {
125+
success: false,
126+
error: `Text generation failed: ${message}`,
127+
};
128+
}
113129
}

0 commit comments

Comments
 (0)