Skip to content

Commit fccfb2a

Browse files
committed
Always return an object
1 parent 967d653 commit fccfb2a

File tree

2 files changed

+141
-72
lines changed

2 files changed

+141
-72
lines changed

packages/trigger-sdk/src/v3/query.ts

Lines changed: 62 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export type QueryFormat = "json" | "csv";
1212
/**
1313
* Options for executing a TSQL query
1414
*/
15-
export type QueryOptions<TFormat extends QueryFormat | undefined = QueryFormat | undefined> = {
15+
export type QueryOptions = {
1616
/**
1717
* The scope of the query - determines what data is accessible
1818
* - "environment": Current environment only (default)
@@ -48,74 +48,89 @@ export type QueryOptions<TFormat extends QueryFormat | undefined = QueryFormat |
4848
*
4949
* @default "json"
5050
*/
51-
format?: TFormat;
51+
format?: QueryFormat;
5252
};
5353

5454
/**
55-
* Result type that automatically narrows based on the format option
56-
* @template TFormat - The format type (json or csv)
57-
* @template TRow - The shape of each row in the result set
55+
* Execute a TSQL query and export as CSV
5856
*/
59-
export type QueryResult<
60-
TFormat extends QueryFormat | undefined = undefined,
61-
TRow extends Record<string, any> = Record<string, any>
62-
> = TFormat extends "csv"
63-
? QueryExecuteCSVResponseBody
64-
: TFormat extends "json"
65-
? { rows: Array<TRow> }
66-
: TFormat extends undefined
67-
? { rows: Array<TRow> }
68-
: { rows: Array<TRow> } | QueryExecuteCSVResponseBody;
57+
function execute(
58+
tsql: string,
59+
options: QueryOptions & { format: "csv" },
60+
requestOptions?: ApiRequestOptions
61+
): Promise<{ format: "csv"; results: string }>;
62+
63+
/**
64+
* Execute a TSQL query and return typed JSON rows
65+
*/
66+
function execute<TRow extends Record<string, any> = Record<string, any>>(
67+
tsql: string,
68+
options?: Omit<QueryOptions, "format"> | (QueryOptions & { format?: "json" }),
69+
requestOptions?: ApiRequestOptions
70+
): Promise<{ format: "json"; results: Array<TRow> }>;
6971

7072
/**
7173
* Execute a TSQL query against your Trigger.dev data
7274
*
73-
* @template TFormat - The format of the response (inferred from options)
75+
* @template TRow - The shape of each row in the result set (provide for type safety)
7476
* @param {string} tsql - The TSQL query string to execute
75-
* @param {QueryOptions<TFormat>} [options] - Optional query configuration
77+
* @param {QueryOptions} [options] - Optional query configuration
7678
* @param {ApiRequestOptions} [requestOptions] - Optional API request configuration
7779
* @returns A promise that resolves with the query results
7880
*
7981
* @example
8082
* ```typescript
8183
* // Basic query with defaults (environment scope, json format)
8284
* const result = await query.execute("SELECT * FROM runs LIMIT 10");
83-
* console.log(result.rows);
85+
* console.log(result.format); // "json"
86+
* console.log(result.results); // Array<Record<string, any>>
8487
*
85-
* // Query with custom period
86-
* const lastMonth = await query.execute(
87-
* "SELECT COUNT(*) as count FROM runs",
88-
* { period: "30d" }
88+
* // Type-safe query with row type
89+
* type RunRow = { id: string; status: string; duration: number };
90+
* const typedResult = await query.execute<RunRow>(
91+
* "SELECT id, status, duration FROM runs LIMIT 10"
8992
* );
93+
* typedResult.results.forEach(row => {
94+
* console.log(row.id, row.status); // Fully typed!
95+
* });
9096
*
91-
* // Query with custom date range
92-
* const januaryRuns = await query.execute(
93-
* "SELECT * FROM runs",
94-
* {
95-
* from: "2025-01-01T00:00:00Z",
96-
* to: "2025-02-01T00:00:00Z"
97-
* }
97+
* // Inline type for aggregation query
98+
* const stats = await query.execute<{ status: string; count: number }>(
99+
* "SELECT status, COUNT(*) as count FROM runs GROUP BY status"
98100
* );
101+
* stats.results.forEach(row => {
102+
* console.log(row.status, row.count); // Fully type-safe
103+
* });
99104
*
100-
* // Organization-wide query
101-
* const orgStats = await query.execute(
102-
* "SELECT project, COUNT(*) as count FROM runs GROUP BY project",
103-
* { scope: "organization", period: "7d" }
105+
* // Query with custom period
106+
* const lastMonth = await query.execute(
107+
* "SELECT COUNT(*) as count FROM runs",
108+
* { period: "30d" }
104109
* );
110+
* console.log(lastMonth.results[0].count); // Type-safe access
105111
*
106-
* // Export as CSV
107-
* const csvData = await query.execute(
112+
* // Export as CSV - automatically narrowed!
113+
* const csvResult = await query.execute(
108114
* "SELECT * FROM runs",
109115
* { format: "csv", period: "7d" }
110116
* );
111-
* // csvData is a string containing CSV
117+
* console.log(csvResult.format); // "csv"
118+
* const lines = csvResult.results.split('\n'); // ✓ results is string
119+
*
120+
* // Discriminated union - can check format at runtime
121+
* const dynamicResult = await query.execute("SELECT * FROM runs");
122+
* if (dynamicResult.format === "json") {
123+
* dynamicResult.results.forEach(row => console.log(row)); // ✓ Typed as array
124+
* } else {
125+
* console.log(dynamicResult.results.length); // ✓ Typed as string
126+
* }
112127
* ```
113128
*/
114-
function execute<TFormat extends QueryFormat | undefined = undefined>(
129+
function execute<TRow extends Record<string, any> = Record<string, any>>(
115130
tsql: string,
116-
options?: QueryOptions<TFormat>,
131+
options?: QueryOptions,
117132
requestOptions?: ApiRequestOptions
118-
): Promise<QueryResult<TFormat>> {
133+
): Promise<{ format: "json"; results: Array<TRow> } | { format: "csv"; results: string }> {
119134
const apiClient = apiClientManager.clientOrThrow();
120135

121136
const $requestOptions = mergeRequestOptions(
@@ -131,7 +146,14 @@ function execute<TFormat extends QueryFormat | undefined = undefined>(
131146
requestOptions
132147
);
133148

134-
return apiClient.executeQuery(tsql, options, $requestOptions) as Promise<QueryResult<TFormat>>;
149+
const format = options?.format ?? "json";
150+
151+
return apiClient.executeQuery(tsql, options, $requestOptions).then((response) => {
152+
if (typeof response === "string") {
153+
return { format: "csv" as const, results: response };
154+
}
155+
return { format: "json" as const, results: response.rows };
156+
}) as Promise<{ format: "json"; results: Array<TRow> } | { format: "csv"; results: string }>;
135157
}
136158

137159
export const query = {
Lines changed: 79 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import { logger, query, task } from "@trigger.dev/sdk";
22

3+
// Type definition for a run row
4+
type RunRow = {
5+
id: string;
6+
status: string;
7+
created_at: string;
8+
duration: number;
9+
};
10+
311
// Simple query example - just the query string, all defaults
412
export const simpleQueryTask = task({
513
id: "simple-query",
@@ -9,31 +17,51 @@ export const simpleQueryTask = task({
917
// Simplest usage - uses environment scope, json format, default period
1018
const result = await query.execute("SELECT * FROM runs LIMIT 10");
1119

12-
logger.info("Query results", {
13-
rowCount: result.rows.length,
14-
firstRow: result.rows[0],
20+
logger.info("Query results (untyped)", {
21+
format: result.format,
22+
rowCount: result.results.length,
23+
firstRow: result.results[0],
24+
});
25+
26+
// Type-safe query with explicit row type
27+
const typedResult = await query.execute<RunRow>(
28+
"SELECT id, status, created_at, duration FROM runs LIMIT 10"
29+
);
30+
31+
logger.info("Query results (typed)", {
32+
format: typedResult.format,
33+
rowCount: typedResult.results.length,
34+
firstRow: typedResult.results[0],
1535
});
1636

17-
// Log all rows
18-
result.rows.forEach((row, index) => {
19-
logger.info(`Row ${index + 1}`, { row });
37+
// Now we have full type safety on the rows!
38+
typedResult.results.forEach((row, index) => {
39+
logger.info(`Run ${index + 1}`, {
40+
id: row.id, // TypeScript knows this is a string
41+
status: row.status, // TypeScript knows this is a string
42+
duration: row.duration, // TypeScript knows this is a number
43+
});
2044
});
2145

2246
return {
23-
totalRows: result.rows.length,
24-
rows: result.rows,
47+
totalRows: typedResult.results.length,
48+
rows: typedResult.results,
2549
};
2650
},
2751
});
2852

29-
// JSON query with all options
53+
// JSON query with all options and inline type
3054
export const fullJsonQueryTask = task({
3155
id: "full-json-query",
3256
run: async () => {
3357
logger.info("Running full JSON query example with all options");
3458

35-
// All options specified
36-
const result = await query.execute(
59+
// All options specified with inline type for aggregation
60+
const result = await query.execute<{
61+
status: string;
62+
count: number;
63+
avg_duration: number;
64+
}>(
3765
`SELECT
3866
status,
3967
COUNT(*) as count,
@@ -44,25 +72,26 @@ export const fullJsonQueryTask = task({
4472
{
4573
scope: "environment", // Query current environment only
4674
period: "30d", // Last 30 days of data
47-
format: "json", // JSON format (default)
75+
// format defaults to "json"
4876
}
4977
);
5078

5179
logger.info("Query completed", {
52-
rowCount: result.rows.length,
80+
format: result.format,
81+
rowCount: result.results.length,
5382
});
5483

55-
// Log the aggregated results
56-
result.rows.forEach((row) => {
84+
// Log the aggregated results - now fully type-safe!
85+
result.results.forEach((row) => {
5786
logger.info("Status breakdown", {
58-
status: row.status,
59-
count: row.count,
60-
averageDuration: row.avg_duration,
87+
status: row.status, // string
88+
count: row.count, // number
89+
averageDuration: row.avg_duration, // number
6190
});
6291
});
6392

6493
return {
65-
summary: result.rows,
94+
summary: result.results,
6695
};
6796
},
6897
});
@@ -73,8 +102,8 @@ export const csvQueryTask = task({
73102
run: async () => {
74103
logger.info("Running CSV query example");
75104

76-
// Query with CSV format - returns a string
77-
const csvData = await query.execute(
105+
// Query with CSV format - automatically typed as discriminated union!
106+
const result = await query.execute(
78107
"SELECT id, status, created_at, duration FROM runs LIMIT 100",
79108
{
80109
scope: "project", // Query all environments in the project
@@ -83,13 +112,15 @@ export const csvQueryTask = task({
83112
}
84113
);
85114

115+
// result.format is "csv" and result.results is automatically typed as string!
86116
logger.info("CSV query completed", {
87-
dataLength: csvData.length,
88-
preview: csvData.substring(0, 200), // Show first 200 chars
117+
format: result.format,
118+
dataLength: result.results.length,
119+
preview: result.results.substring(0, 200), // Show first 200 chars
89120
});
90121

91122
// Count the number of rows (lines - 1 for header)
92-
const lines = csvData.split("\n");
123+
const lines = result.results.split("\n");
93124
const rowCount = lines.length - 1;
94125

95126
logger.info("CSV stats", {
@@ -98,19 +129,29 @@ export const csvQueryTask = task({
98129
});
99130

100131
return {
101-
csv: csvData,
132+
format: result.format,
133+
csv: result.results,
102134
rowCount,
103135
};
104136
},
105137
});
106138

107-
// Organization-wide query with date range
139+
// Organization-wide query with date range and type safety
108140
export const orgQueryTask = task({
109141
id: "org-query",
110142
run: async () => {
111143
logger.info("Running organization-wide query");
112144

113-
const result = await query.execute(
145+
// Define the shape of our aggregated results
146+
type ProjectStats = {
147+
project: string;
148+
environment: string;
149+
total_runs: number;
150+
successful_runs: number;
151+
failed_runs: number;
152+
};
153+
154+
const result = await query.execute<ProjectStats>(
114155
`SELECT
115156
project,
116157
environment,
@@ -124,25 +165,31 @@ export const orgQueryTask = task({
124165
scope: "organization", // Query across all projects
125166
from: "2025-02-01T00:00:00Z", // Custom date range
126167
to: "2025-02-11T23:59:59Z",
127-
format: "json",
168+
// format defaults to "json"
128169
}
129170
);
130171

131172
logger.info("Organization query completed", {
132-
projectCount: result.rows.length,
173+
format: result.format,
174+
projectCount: result.results.length,
133175
});
134176

135-
result.rows.forEach((row) => {
177+
// Full type safety on aggregated results
178+
result.results.forEach((row) => {
179+
const successRate = (row.successful_runs / row.total_runs) * 100;
180+
136181
logger.info("Project stats", {
137182
project: row.project,
138183
environment: row.environment,
139184
totalRuns: row.total_runs,
140-
successRate: `${((row.successful_runs / row.total_runs) * 100).toFixed(2)}%`,
185+
successfulRuns: row.successful_runs,
186+
failedRuns: row.failed_runs,
187+
successRate: `${successRate.toFixed(2)}%`,
141188
});
142189
});
143190

144191
return {
145-
projects: result.rows,
192+
projects: result.results,
146193
};
147194
},
148195
});

0 commit comments

Comments
 (0)