Skip to content

Commit

Permalink
all methods for get result csv with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
bh2smith committed Feb 17, 2024
1 parent d5251db commit 8a10f5b
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 62 deletions.
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
"fmt": "prettier --write \"./**/*.ts\"",
"lint": "eslint"
},
"dependencies": {
"cross-fetch": "^4.0.0",
"deprecated": "^0.0.2",
"loglevel": "^1.8.0"
},
"devDependencies": {
"@types/chai": "^4.3.3",
"@types/mocha": "^10.0.6",
Expand All @@ -29,10 +34,5 @@
"ts-mocha": "^10.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.3.3"
},
"dependencies": {
"cross-fetch": "^4.0.0",
"deprecated": "^0.0.2",
"loglevel": "^1.8.0"
}
}
100 changes: 84 additions & 16 deletions src/api/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { DuneError, ResultsResponse, ExecutionState, QueryParameter } from "../types";
import {
DuneError,
ResultsResponse,
ExecutionState,
QueryParameter,
GetStatusResponse,
} from "../types";
import { ageInHours, sleep } from "../utils";
import log from "loglevel";
import { logPrefix } from "../utils";
Expand Down Expand Up @@ -27,22 +33,11 @@ export class DuneClient {
params?: ExecutionParams,
pingFrequency: number = POLL_FREQUENCY_SECONDS,
): Promise<ResultsResponse> {
log.info(
logPrefix,
`refreshing query https://dune.com/queries/${queryID} with parameters ${JSON.stringify(
params,
)}`,
let { state, execution_id: jobID } = await this._runInner(
queryID,
params,
pingFrequency,
);
const { execution_id: jobID } = await this.exec.executeQuery(queryID, params);
let { state } = await this.exec.getExecutionStatus(jobID);
while (!TERMINAL_STATES.includes(state)) {
log.info(
logPrefix,
`waiting for query execution ${jobID} to complete: current state ${state}`,
);
await sleep(pingFrequency);
state = (await this.exec.getExecutionStatus(jobID)).state;
}
if (state === ExecutionState.COMPLETED) {
return this.exec.getExecutionResults(jobID);
} else {
Expand All @@ -52,6 +47,27 @@ export class DuneClient {
throw new DuneError(message);
}
}

async runQueryCSV(
queryID: number,
params?: ExecutionParams,
pingFrequency: number = POLL_FREQUENCY_SECONDS,
): Promise<string> {
let { state, execution_id: jobID } = await this._runInner(
queryID,
params,
pingFrequency,
);
if (state === ExecutionState.COMPLETED) {
return this.exec.getResultCSV(jobID);
} else {
const message = `refresh (execution ${jobID}) yields incomplete terminal state ${state}`;
// TODO - log the error in constructor
log.error(logPrefix, message);
throw new DuneError(message);
}
}

/**
* Goes a bit beyond the internal call which returns that last execution results.
* Here contains additional logic to refresh the results if they are too old.
Expand All @@ -77,6 +93,58 @@ export class DuneClient {
return results;
}

/**
* Get the lastest execution results in CSV format.
* @param queryId - query to get results of.
* @param parameters - parameters for which they were called.
* @param maxAgeHours - oldest acceptable results (if expired results are refreshed)
* @returns Latest execution results for the given parameters.
*/
async getLatestResultCSV(
queryId: number,
parameters?: QueryParameter[],
maxAgeHours: number = THREE_MONTHS_IN_HOURS,
): Promise<string> {
const lastResults = await this.exec.getLastExecutionResults(queryId, parameters);
const lastRun: Date = lastResults.execution_ended_at!;
let results: Promise<string>;
if (lastRun !== undefined && ageInHours(lastRun) > maxAgeHours) {
log.info(
logPrefix,
`results (from ${lastRun}) older than ${maxAgeHours} hours, re-running query.`,
);
results = this.runQueryCSV(queryId, { query_parameters: parameters });
} else {
// TODO (user cost savings): transform the lastResults into CSV instead of refetching
results = this.exec.getLastResultCSV(queryId, parameters);
}
return results;
}

private async _runInner(
queryID: number,
params?: ExecutionParams,
pingFrequency: number = POLL_FREQUENCY_SECONDS,
): Promise<GetStatusResponse> {
log.info(
logPrefix,
`refreshing query https://dune.com/queries/${queryID} with parameters ${JSON.stringify(
params,
)}`,
);
const { execution_id: jobID } = await this.exec.executeQuery(queryID, params);
let status = await this.exec.getExecutionStatus(jobID);
while (!TERMINAL_STATES.includes(status.state)) {
log.info(
logPrefix,
`waiting for query execution ${jobID} to complete: current state ${status.state}`,
);
await sleep(pingFrequency);
status = await this.exec.getExecutionStatus(jobID);
}
return status;
}

/**
* @deprecated since version 0.0.2 Use runQuery
*/
Expand Down
17 changes: 16 additions & 1 deletion src/api/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,26 @@ export class ExecutionAPI extends Router {
return response as ResultsResponse;
}

async getResultCSV(executionId: string): Promise<string> {
const response = await this._get<string>(`execution/${executionId}/results/csv`);
log.debug(logPrefix, `get_result response ${JSON.stringify(response)}`);
return response;
}

async getLastExecutionResults(
queryId: number,
parameters?: QueryParameter[],
): Promise<ResultsResponse> {
return await this._get<ResultsResponse>(`query/${queryId}/results`, {
return this._get<ResultsResponse>(`query/${queryId}/results`, {
query_parameters: parameters ? parameters : [],
});
}

async getLastResultCSV(
queryId: number,
parameters?: QueryParameter[],
): Promise<string> {
return this._get<string>(`query/${queryId}/results/csv`, {
query_parameters: parameters ? parameters : [],
});
}
Expand Down
3 changes: 2 additions & 1 deletion src/api/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { Router } from "./router";
import { DuneQuery, QueryParameter, CreateQueryResponse, DuneError } from "../types";
import { CreateQueryPayload, UpdateQueryPayload } from "../types/requestPayload";
import log from "loglevel";

interface EditQueryResponse {
query_id: number;
Expand Down Expand Up @@ -56,7 +57,7 @@ export class QueryAPI extends Router {
if (params !== undefined) parameters.query_parameters = params;

if (Object.keys(parameters).length === 0) {
console.warn("Called updateQuery with no proposed changes.");
log.warn("updateQuery: called with no proposed changes.");
return queryId;
}

Expand Down
52 changes: 31 additions & 21 deletions src/api/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,39 @@ export class Router {
}

protected async _handleResponse<T>(responsePromise: Promise<Response>): Promise<T> {
const apiResponse = await responsePromise
.then((response) => {
if (!response.ok) {
log.error(
logPrefix,
`response error ${response.status} - ${response.statusText}`,
);
}
return response.json();
})
.catch((error) => {
log.error(logPrefix, `caught unhandled response error ${JSON.stringify(error)}`);
throw error;
});
if (apiResponse.error) {
log.error(logPrefix, `error contained in response ${JSON.stringify(apiResponse)}`);
if (apiResponse.error instanceof Object) {
throw new DuneError(apiResponse.error.type);
} else {
throw new DuneError(apiResponse.error);
let result;
try {
const response = await responsePromise;

if (!response.ok) {
log.error(
logPrefix,
`response error ${response.status} - ${response.statusText}`,
);
}
const clonedResponse = response.clone();
try {
// Attempt to parse JSON
result = await response.json();
} catch {
// Fallback to text if JSON parsing fails
// This fallback is used for CSV retrieving methods.
result = await clonedResponse.text();
}

// Check for error in result after parsing
if (result.error) {
log.error(logPrefix, `error contained in response ${JSON.stringify(result)}`);
// Assuming DuneError is a custom Error you'd like to throw
throw new DuneError(
result.error instanceof Object ? result.error.type : result.error,
);
}
} catch (error) {
log.error(logPrefix, `caught unhandled response error ${JSON.stringify(error)}`);
throw error;
}
return apiResponse;
return result;
}

protected async _request<T>(
Expand Down
12 changes: 12 additions & 0 deletions tests/e2e/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,16 @@ describe("DuneClient Extensions", () => {
]);
expect(results.result?.rows.length).to.be.greaterThan(0);
});

it("getsLatestResultsCSV", async () => {
// https://dune.com/queries/1215383
const resultCSV = await client.getLatestResultCSV(1215383, [
QueryParameter.text("TextField", "Plain Text"),
]);
const expectedRows = [
"text_field,number_field,date_field,list_field\n",
"Plain Text,3.1415926535,2022-05-04 00:00:00.000,Option 1\n",
];
expect(resultCSV).to.be.eq(expectedRows.join(""));
});
});
50 changes: 47 additions & 3 deletions tests/e2e/executionAPI.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import { QueryParameter, ExecutionState, ExecutionAPI } from "../../src/";
import log from "loglevel";
import { ExecutionPerformance } from "../../src/types/requestPayload";
import { BASIC_KEY, expectAsyncThrow } from "./util";
import { sleep } from "../../src/utils";

log.setLevel("silent", true);

describe("ExecutionAPI: native routes", () => {
let client: ExecutionAPI;
let testQueryId: number;

beforeEach(() => {
client = new ExecutionAPI(BASIC_KEY);
testQueryId = 1215383;
});

// This doesn't work if run too many times at once:
Expand Down Expand Up @@ -45,22 +48,21 @@ describe("ExecutionAPI: native routes", () => {
});

it("successfully executes with query parameters", async () => {
const queryID = 1215383;
const parameters = [
QueryParameter.text("TextField", "Plain Text"),
QueryParameter.number("NumberField", 3.1415926535),
QueryParameter.date("DateField", "2022-05-04 00:00:00"),
QueryParameter.enum("ListField", "Option 1"),
];
// Execute and check state
const execution = await client.executeQuery(queryID, {
const execution = await client.executeQuery(testQueryId, {
query_parameters: parameters,
});
expect(execution.execution_id).is.not.null;
});

it("execute with Large tier performance", async () => {
const execution = await client.executeQuery(1215383, {
const execution = await client.executeQuery(testQueryId, {
performance: ExecutionPerformance.Large,
});
expect(execution.execution_id).is.not.null;
Expand All @@ -83,6 +85,48 @@ describe("ExecutionAPI: native routes", () => {
cancelled_at: "2022-10-04T12:08:48.790331Z",
});
});

it("getResults", async () => {
const execution = await client.executeQuery(testQueryId);
await sleep(1);
// expect basic query has completed after 1s
let status = await client.getExecutionStatus(execution.execution_id);
expect(status.state).to.be.eq(ExecutionState.COMPLETED);

// let resultJSON = await client.getExecutionResults(execution.execution_id);
await expect(() => client.getExecutionResults(execution.execution_id)).to.not.throw();

let resultCSV = await client.getResultCSV(execution.execution_id);
const expectedRows = [
"text_field,number_field,date_field,list_field\n",
"Plain Text,3.1415926535,2022-05-04 00:00:00.000,Option 1\n",
];
expect(resultCSV).to.be.eq(expectedRows.join(""));
});

it("getLastResult", async () => {
// https://dune.com/queries/1215383
const resultCSV = await client.getLastResultCSV(1215383, [
QueryParameter.text("TextField", "Plain Text"),
]);
const expectedRows = [
"text_field,number_field,date_field,list_field\n",
"Plain Text,3.1415926535,2022-05-04 00:00:00.000,Option 1\n",
];
expect(resultCSV).to.be.eq(expectedRows.join(""));
});

it("getLastResultCSV", async () => {
// https://dune.com/queries/1215383
const resultCSV = await client.getLastResultCSV(1215383, [
QueryParameter.text("TextField", "Plain Text"),
]);
const expectedRows = [
"text_field,number_field,date_field,list_field\n",
"Plain Text,3.1415926535,2022-05-04 00:00:00.000,Option 1\n",
];
expect(resultCSV).to.be.eq(expectedRows.join(""));
});
});

describe("ExecutionAPI: Errors", () => {
Expand Down
1 change: 0 additions & 1 deletion tests/e2e/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ if (PLUS_API_KEY === undefined) {
export const BASIC_KEY: string = BASIC_API_KEY!;
export const PLUS_KEY: string = PLUS_API_KEY!;


export const expectAsyncThrow = async (
promise: Promise<any>,
message?: string | object,
Expand Down
Loading

0 comments on commit 8a10f5b

Please sign in to comment.