Skip to content

Commit 32c7c08

Browse files
ivpusicbh2smith
andauthored
Add execute raw sql, usage and status endpoint error field APP-6031 (#84)
Co-authored-by: Benjamin Smith <bh2smith@users.noreply.github.com>
1 parent cd8bc34 commit 32c7c08

File tree

10 files changed

+176
-6
lines changed

10 files changed

+176
-6
lines changed

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,24 @@ client
4242
// ]
4343
```
4444

45+
## Execute Raw SQL
46+
47+
You can execute raw SQL queries directly using the `executeSql` method:
48+
49+
```ts
50+
const { DUNE_API_KEY } = process.env;
51+
52+
const client = new DuneClient(DUNE_API_KEY ?? "");
53+
const execution = await client.exec.executeSql({
54+
sql: "SELECT * FROM dex.trades WHERE block_time > now() - interval '1' day LIMIT 10",
55+
performance: QueryEngine.Medium, // optional
56+
});
57+
58+
const executionId = execution.execution_id;
59+
const status = await client.exec.getExecutionStatus(executionId);
60+
const results = await client.exec.getExecutionResults(executionId);
61+
```
62+
4563
## Custom API
4664

4765
```ts
@@ -56,6 +74,21 @@ const results = await client.custom.getResults({
5674
});
5775
```
5876

77+
## Usage API
78+
79+
Get information about your API usage, including credits and storage:
80+
81+
```ts
82+
const { DUNE_API_KEY } = process.env;
83+
84+
const client = new DuneClient(DUNE_API_KEY ?? "");
85+
const usage = await client.usage.getUsage();
86+
87+
console.log(`Credits used: ${usage.billing_periods[0].credits_used}`);
88+
console.log(`Private queries: ${usage.private_queries}`);
89+
console.log(`Storage: ${usage.bytes_used} / ${usage.bytes_allowed} bytes`);
90+
```
91+
5992

6093
Note also that the client has methods `executeQuery`, `getExecutionStatus`, `getExecutionResult` and `cancelExecution`
6194

src/api/client.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { POLL_FREQUENCY_SECONDS } from "../constants";
2424
import { QueryAPI } from "./query";
2525
import { TableAPI } from "./table";
2626
import { CustomAPI } from "./custom";
27+
import { UsageAPI } from "./usage";
2728
import { deprecationWarning } from "../deprecation";
2829

2930
/// Various states of query execution that are "terminal".
@@ -47,12 +48,15 @@ export class DuneClient {
4748
table: TableAPI;
4849
/// Custom Endpoint Interface
4950
custom: CustomAPI;
51+
/// Usage Interface
52+
usage: UsageAPI;
5053

5154
constructor(apiKey: string) {
5255
this.exec = new ExecutionAPI(apiKey);
5356
this.query = new QueryAPI(apiKey);
5457
this.table = new TableAPI(apiKey);
5558
this.custom = new CustomAPI(apiKey);
59+
this.usage = new UsageAPI(apiKey);
5660
}
5761

5862
/**

src/api/execution.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
QueryEngine,
1313
GetResultParams,
1414
validateAndBuildGetResultParams,
15+
ExecuteSqlParams,
1516
} from "../types";
1617
import log from "loglevel";
1718
import { ageInHours, logPrefix } from "../utils";
@@ -51,6 +52,23 @@ export class ExecutionAPI extends Router {
5152
return response as ExecutionResponse;
5253
}
5354

55+
/**
56+
* Executes raw SQL according to:
57+
* https://docs.dune.com/api-reference/executions/endpoint/execute-sql
58+
* @param {ExecuteSqlParams} params including SQL query and execution performance.
59+
* @returns {ExecutionResponse} response containing execution ID and state.
60+
*/
61+
async executeSql(params: ExecuteSqlParams): Promise<ExecutionResponse> {
62+
const { sql, performance = QueryEngine.Medium } = params;
63+
64+
const response = await this.post<ExecutionResponse>(`sql/execute`, {
65+
sql,
66+
performance,
67+
});
68+
log.debug(logPrefix, `execute sql response ${JSON.stringify(response)}`);
69+
return response as ExecutionResponse;
70+
}
71+
5472
/**
5573
* Cancels an execution according to:
5674
* https://docs.dune.com/api-reference/executions/endpoint/cancel-execution

src/api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export * from "./execution";
44
export * from "./query";
55
export * from "./router";
66
export * from "./table";
7+
export * from "./usage";

src/api/usage.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Router } from "./router";
2+
import { UsageResponse } from "../types";
3+
4+
/**
5+
* Usage API Interface:
6+
* Get information about API usage and credits.
7+
* https://docs.dune.com/api-reference/usage/endpoint/get-usage
8+
*/
9+
export class UsageAPI extends Router {
10+
/**
11+
* Get usage information for the current API key.
12+
* Returns details about private queries, dashboards, storage,
13+
* and credit usage for billing periods.
14+
* @returns {Promise<UsageResponse>} usage information including credits and storage
15+
*/
16+
async getUsage(): Promise<UsageResponse> {
17+
return this.post<UsageResponse>("usage");
18+
}
19+
}

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export { DuneClient, QueryAPI, ExecutionAPI, CustomAPI, TableAPI } from "./api";
1+
export { DuneClient, QueryAPI, ExecutionAPI, CustomAPI, TableAPI, UsageAPI } from "./api";
22
export * from "./types";
33
export { Paginator } from "./paginator";

src/types/requestArgs.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export type UploadCSVArgs = {
2424
export type RequestPayload =
2525
| GetResultParams
2626
| ExecuteQueryParams
27+
| ExecuteSqlParams
2728
| UpdateQueryParams
2829
| CreateQueryParams
2930
| UploadCSVArgs
@@ -204,6 +205,13 @@ export interface ExecuteQueryParams extends BaseParams {
204205
performance: QueryEngine;
205206
}
206207

208+
export interface ExecuteSqlParams {
209+
/// The SQL query to execute
210+
sql: string;
211+
/// The performance engine tier the execution will be run on
212+
performance?: QueryEngine;
213+
}
214+
207215
export interface BaseCRUDParams extends BaseParams {
208216
/// Description of the query.
209217
description?: string;

src/types/response.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ interface BaseStatusResponse extends TimeData {
9191
// Unique identifier of the query.
9292
query_id: number;
9393
queue_position?: number;
94+
error?: ErrorResult;
9495
}
9596

9697
/// Format of a `GetStatusResponse` when the query execution is anything but complete.
@@ -215,3 +216,18 @@ export interface CreateTableResult {
215216
export interface InsertTableResult {
216217
rows_written: number;
217218
}
219+
220+
export interface BillingPeriod {
221+
start_date: string;
222+
end_date: string;
223+
credits_used: number;
224+
credits_included: number;
225+
}
226+
227+
export interface UsageResponse {
228+
private_queries: number;
229+
private_dashboards: number;
230+
bytes_used: number;
231+
bytes_allowed: number;
232+
billing_periods: BillingPeriod[];
233+
}

tests/e2e/execution.spec.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,23 @@ describe("ExecutionAPI: native routes", () => {
244244
const expectedRows = ["number\n", "2\n"];
245245
expect(resultCSV.data).toEqual(expectedRows.join(""));
246246
});
247+
248+
it("executes raw SQL with executeSql", async () => {
249+
const execution = await client.executeSql({
250+
sql: "SELECT * FROM dex.trades WHERE block_time > now() - interval '1' day LIMIT 10",
251+
});
252+
expect(execution.execution_id).not.toEqual(null);
253+
expect(execution.state).toBeDefined();
254+
});
255+
256+
it("executes raw SQL with performance parameter", async () => {
257+
const execution = await client.executeSql({
258+
sql: "SELECT 1 as test_value",
259+
performance: QueryEngine.Medium,
260+
});
261+
expect(execution.execution_id).not.toEqual(null);
262+
expect(execution.state).toBeDefined();
263+
});
247264
});
248265

249266
describe("ExecutionAPI: Errors", () => {
@@ -306,7 +323,7 @@ describe("ExecutionAPI: Errors", () => {
306323
// Create a query with intentionally bad SQL
307324
const queryId = await fullClient.query.createQuery({
308325
name: "Test Failed Query",
309-
query_sql: "SELECT x", // This will fail - column x doesn't exist
326+
query_sql: "SELECT x",
310327
is_private: true,
311328
});
312329

@@ -317,13 +334,18 @@ describe("ExecutionAPI: Errors", () => {
317334
// Wait a bit for it to fail
318335
await sleep(3);
319336

320-
// Get the results - should have an error
321-
const result = await fullClient.exec.getExecutionResults(execution.execution_id);
337+
// Get the status - should have an error
338+
const status = await fullClient.exec.getExecutionStatus(execution.execution_id);
339+
expect(status.error).toBeDefined();
340+
expect(status.error?.type).toEqual("FAILED_TYPE_EXECUTION_FAILED");
341+
expect(status.error?.message).toContain("Column 'x' cannot be resolved");
342+
expect(status.state).toEqual(ExecutionState.FAILED);
322343

323-
// Verify error structure exists
344+
// Get the results - should also have an error
345+
const result = await fullClient.exec.getExecutionResults(execution.execution_id);
324346
expect(result.error).toBeDefined();
325347
expect(result.error?.type).toEqual("FAILED_TYPE_EXECUTION_FAILED");
326-
expect(result.error?.message).toContain("x");
348+
expect(result.error?.message).toContain("Column 'x' cannot be resolved");
327349
expect(result.state).toEqual(ExecutionState.FAILED);
328350
} finally {
329351
// Cleanup: archive the query even if test fails

tests/e2e/usage.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { UsageAPI, DuneError } from "../../src";
2+
import log from "loglevel";
3+
4+
log.setLevel("silent", true);
5+
6+
const API_KEY = process.env.DUNE_API_KEY!;
7+
8+
describe("UsageAPI", () => {
9+
let client: UsageAPI;
10+
11+
beforeAll(() => {
12+
client = new UsageAPI(API_KEY);
13+
});
14+
15+
it("retrieves usage information", async () => {
16+
const usage = await client.getUsage();
17+
18+
expect(usage).toBeDefined();
19+
expect(typeof usage.private_queries).toBe("number");
20+
expect(typeof usage.private_dashboards).toBe("number");
21+
expect(typeof usage.bytes_used).toBe("number");
22+
expect(typeof usage.bytes_allowed).toBe("number");
23+
expect(Array.isArray(usage.billing_periods)).toBe(true);
24+
});
25+
26+
it("returns valid billing periods structure", async () => {
27+
const usage = await client.getUsage();
28+
29+
if (usage.billing_periods.length > 0) {
30+
const period = usage.billing_periods[0];
31+
expect(period.start_date).toBeDefined();
32+
expect(period.end_date).toBeDefined();
33+
expect(typeof period.credits_used).toBe("number");
34+
expect(typeof period.credits_included).toBe("number");
35+
}
36+
});
37+
38+
it("throws error with invalid API key", async () => {
39+
const badClient = new UsageAPI("Invalid_Key");
40+
41+
try {
42+
await badClient.getUsage();
43+
expect(false).toBe(true);
44+
} catch (error) {
45+
expect(error).toBeInstanceOf(DuneError);
46+
expect((error as DuneError).message).toContain("401");
47+
}
48+
});
49+
});

0 commit comments

Comments
 (0)