Skip to content

Commit 967d653

Browse files
committed
SDK function for query
1 parent 4982428 commit 967d653

File tree

6 files changed

+383
-0
lines changed

6 files changed

+383
-0
lines changed

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ import {
3030
ListScheduleOptions,
3131
QueueItem,
3232
QueueTypeName,
33+
QueryExecuteRequestBody,
34+
QueryExecuteResponseBody,
35+
QueryExecuteCSVResponseBody,
3336
ReplayRunResponse,
3437
RescheduleRunRequestBody,
3538
ResetIdempotencyKeyResponse,
@@ -1406,6 +1409,68 @@ export class ApiClient {
14061409
);
14071410
}
14081411

1412+
async executeQuery(
1413+
query: string,
1414+
options?: {
1415+
scope?: "environment" | "project" | "organization";
1416+
period?: string;
1417+
from?: string;
1418+
to?: string;
1419+
format?: "json" | "csv";
1420+
},
1421+
requestOptions?: ZodFetchOptions
1422+
): Promise<QueryExecuteResponseBody | QueryExecuteCSVResponseBody> {
1423+
const body = {
1424+
query,
1425+
scope: options?.scope ?? "environment",
1426+
period: options?.period,
1427+
from: options?.from,
1428+
to: options?.to,
1429+
format: options?.format ?? "json",
1430+
};
1431+
1432+
const format = options?.format ?? "json";
1433+
1434+
if (format === "csv") {
1435+
// For CSV, we get a text response
1436+
const response = await fetch(`${this.baseUrl}/api/v1/query`, {
1437+
method: "POST",
1438+
headers: this.#getHeaders(false),
1439+
body: JSON.stringify(body),
1440+
});
1441+
1442+
if (!response.ok) {
1443+
const errText = await response.text().catch((e) => (e as Error).message);
1444+
let errJSON: Object | undefined;
1445+
try {
1446+
errJSON = JSON.parse(errText) as Object;
1447+
} catch {
1448+
// ignore
1449+
}
1450+
const errMessage = errJSON ? undefined : errText;
1451+
const responseHeaders = Object.fromEntries(response.headers.entries());
1452+
1453+
throw ApiError.generate(response.status, errJSON, errMessage, responseHeaders);
1454+
}
1455+
1456+
return await response.text();
1457+
}
1458+
1459+
// For JSON, use zodfetch
1460+
return zodfetch(
1461+
z.object({
1462+
rows: z.array(z.record(z.any())),
1463+
}),
1464+
`${this.baseUrl}/api/v1/query`,
1465+
{
1466+
method: "POST",
1467+
headers: this.#getHeaders(false),
1468+
body: JSON.stringify(body),
1469+
},
1470+
mergeRequestOptions(this.defaultRequestOptions, requestOptions)
1471+
);
1472+
}
1473+
14091474
#getHeaders(spanParentAsLink: boolean, additionalHeaders?: Record<string, string | undefined>) {
14101475
const headers: Record<string, string> = {
14111476
"Content-Type": "application/json",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ export * from "./webhooks.js";
1515
export * from "./checkpoints.js";
1616
export * from "./warmStart.js";
1717
export * from "./queues.js";
18+
export * from "./query.js";
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { z } from "zod";
2+
3+
/**
4+
* Request body schema for executing a query
5+
*/
6+
export const QueryExecuteRequestBody = z.object({
7+
query: z.string(),
8+
scope: z.enum(["organization", "project", "environment"]).default("environment"),
9+
period: z.string().nullish(),
10+
from: z.string().nullish(),
11+
to: z.string().nullish(),
12+
format: z.enum(["json", "csv"]).default("json"),
13+
});
14+
15+
export type QueryExecuteRequestBody = z.infer<typeof QueryExecuteRequestBody>;
16+
17+
/**
18+
* Response body schema for JSON format queries
19+
*/
20+
export const QueryExecuteResponseBody = z.object({
21+
rows: z.array(z.record(z.any())),
22+
});
23+
24+
export type QueryExecuteResponseBody = z.infer<typeof QueryExecuteResponseBody>;
25+
26+
/**
27+
* Response body type for CSV format queries (returns a string)
28+
*/
29+
export type QueryExecuteCSVResponseBody = string;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export * from "./otel.js";
1717
export * from "./schemas.js";
1818
export * from "./heartbeats.js";
1919
export * from "./streams.js";
20+
export * from "./query.js";
2021
export type { Context };
2122

2223
import type { Context } from "./shared.js";
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import type {
2+
ApiRequestOptions,
3+
QueryExecuteResponseBody,
4+
QueryExecuteCSVResponseBody,
5+
} from "@trigger.dev/core/v3";
6+
import { apiClientManager, mergeRequestOptions } from "@trigger.dev/core/v3";
7+
import { tracer } from "./tracer.js";
8+
9+
export type QueryScope = "environment" | "project" | "organization";
10+
export type QueryFormat = "json" | "csv";
11+
12+
/**
13+
* Options for executing a TSQL query
14+
*/
15+
export type QueryOptions<TFormat extends QueryFormat | undefined = QueryFormat | undefined> = {
16+
/**
17+
* The scope of the query - determines what data is accessible
18+
* - "environment": Current environment only (default)
19+
* - "project": All environments in the project
20+
* - "organization": All projects in the organization
21+
*
22+
* @default "environment"
23+
*/
24+
scope?: "environment" | "project" | "organization";
25+
26+
/**
27+
* Time period to query (e.g., "7d", "30d", "1h")
28+
* Cannot be used with `from` or `to`
29+
*/
30+
period?: string;
31+
32+
/**
33+
* Start of time range (ISO 8601 timestamp)
34+
* Must be used with `to`
35+
*/
36+
from?: string;
37+
38+
/**
39+
* End of time range (ISO 8601 timestamp)
40+
* Must be used with `from`
41+
*/
42+
to?: string;
43+
44+
/**
45+
* Response format
46+
* - "json": Returns structured data (default)
47+
* - "csv": Returns CSV string
48+
*
49+
* @default "json"
50+
*/
51+
format?: TFormat;
52+
};
53+
54+
/**
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
58+
*/
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;
69+
70+
/**
71+
* Execute a TSQL query against your Trigger.dev data
72+
*
73+
* @template TFormat - The format of the response (inferred from options)
74+
* @param {string} tsql - The TSQL query string to execute
75+
* @param {QueryOptions<TFormat>} [options] - Optional query configuration
76+
* @param {ApiRequestOptions} [requestOptions] - Optional API request configuration
77+
* @returns A promise that resolves with the query results
78+
*
79+
* @example
80+
* ```typescript
81+
* // Basic query with defaults (environment scope, json format)
82+
* const result = await query.execute("SELECT * FROM runs LIMIT 10");
83+
* console.log(result.rows);
84+
*
85+
* // Query with custom period
86+
* const lastMonth = await query.execute(
87+
* "SELECT COUNT(*) as count FROM runs",
88+
* { period: "30d" }
89+
* );
90+
*
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+
* }
98+
* );
99+
*
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" }
104+
* );
105+
*
106+
* // Export as CSV
107+
* const csvData = await query.execute(
108+
* "SELECT * FROM runs",
109+
* { format: "csv", period: "7d" }
110+
* );
111+
* // csvData is a string containing CSV
112+
* ```
113+
*/
114+
function execute<TFormat extends QueryFormat | undefined = undefined>(
115+
tsql: string,
116+
options?: QueryOptions<TFormat>,
117+
requestOptions?: ApiRequestOptions
118+
): Promise<QueryResult<TFormat>> {
119+
const apiClient = apiClientManager.clientOrThrow();
120+
121+
const $requestOptions = mergeRequestOptions(
122+
{
123+
tracer,
124+
name: "query.execute()",
125+
icon: "sparkles",
126+
attributes: {
127+
scope: options?.scope ?? "environment",
128+
format: options?.format ?? "json",
129+
},
130+
},
131+
requestOptions
132+
);
133+
134+
return apiClient.executeQuery(tsql, options, $requestOptions) as Promise<QueryResult<TFormat>>;
135+
}
136+
137+
export const query = {
138+
execute,
139+
};

0 commit comments

Comments
 (0)