Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Support for Query Parameters #6

Merged
merged 4 commits into from
Oct 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 34 additions & 22 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
ExecutionResponse,
ExecutionState,
GetStatusResponse,
ResultsResponse,
DuneError,
} from "./responseTypes";
import fetch from "cross-fetch";
import { QueryParameter } from "./queryParameter";
const BASE_URL = "https://api.dune.com/api/v1";

export class DuneClient {
Expand All @@ -15,8 +15,8 @@ export class DuneClient {
this.apiKey = apiKey;
}

private async _handleResponse(responsePromise: Promise<Response>): Promise<any> {
const response = await responsePromise
private async _handleResponse<T>(responsePromise: Promise<Response>): Promise<T> {
const apiResponse = await responsePromise
.then((response) => {
if (response.status > 400) {
console.error(`response error ${response.status} - ${response.statusText}`);
Expand All @@ -27,43 +27,53 @@ export class DuneClient {
console.error(`caught unhandled response error ${JSON.stringify(error)}`);
throw error;
});
if (response.error) {
console.error(`caught unhandled response error ${JSON.stringify(response)}`);
if (response.error instanceof Object) {
throw new DuneError(response.error.type);
if (apiResponse.error) {
console.error(`error contained in response ${JSON.stringify(apiResponse)}`);
if (apiResponse.error instanceof Object) {
throw new DuneError(apiResponse.error.type);
} else {
throw new DuneError(response.error);
throw new DuneError(apiResponse.error);
}
}
return response;
return apiResponse;
}

private async _get(url: string): Promise<any> {
private async _get<T>(url: string): Promise<T> {
console.debug(`GET received input url=${url}`);
const response = fetch(url, {
method: "GET",
headers: {
"x-dune-api-key": this.apiKey,
},
});
return this._handleResponse(response);
return this._handleResponse<T>(response);
}

private async _post(url: string, params?: any): Promise<any> {
private async _post<T>(url: string, params?: QueryParameter[]): Promise<T> {
console.debug(`POST received input url=${url}, params=${JSON.stringify(params)}`);
// Transform Query Parameter list into "dict"
const reducedParams = params?.reduce<Record<string, string>>(
(acc, { name, value }) => ({ ...acc, [name]: value }),
{},
);
const response = fetch(url, {
method: "POST",
body: JSON.stringify(params),
body: JSON.stringify({ query_parameters: reducedParams || {} }),
headers: {
"x-dune-api-key": this.apiKey,
},
});
return this._handleResponse(response);
return this._handleResponse<T>(response);
}

async execute(queryID: number): Promise<ExecutionResponse> {
// TODO - Add Query Parameters to Execution
const response = await this._post(`${BASE_URL}/query/${queryID}/execute`, {});
async execute(
queryID: number,
parameters?: QueryParameter[],
): Promise<ExecutionResponse> {
const response = await this._post<ExecutionResponse>(
`${BASE_URL}/query/${queryID}/execute`,
parameters,
);
console.debug(`execute response ${JSON.stringify(response)}`);
return {
execution_id: response.execution_id,
Expand All @@ -76,10 +86,11 @@ export class DuneClient {
`${BASE_URL}/execution/${jobID}/status`,
);
console.debug(`get_status response ${JSON.stringify(response)}`);
const { execution_id, query_id, state } = response;
return {
execution_id: response.execution_id,
query_id: response.query_id,
state: response.state,
execution_id,
query_id,
state,
// times: parseTimesFrom(data)
};
}
Expand All @@ -102,8 +113,9 @@ export class DuneClient {
}

async cancel_execution(jobID: string): Promise<boolean> {
const data = await this._post(`${BASE_URL}/execution/${jobID}/cancel`);
const success: boolean = data.success;
const { success }: { success: boolean } = await this._post(
`${BASE_URL}/execution/${jobID}/cancel`,
);
return success;
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { DuneClient } from "./client";

export * from "./responseTypes";
export * from "./queryParameter";
35 changes: 35 additions & 0 deletions src/queryParameter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export enum ParameterType {
TEXT = "text",
NUMBER = "number",
DATE = "date",
ENUM = "enum",
}

export class QueryParameter {
type: ParameterType;
value: string;
name: string;

constructor(type: ParameterType, name: string, value: any) {
this.type = type;
this.value = value.toString();
this.name = name;
}

static text(name: string, value: string): QueryParameter {
return new QueryParameter(ParameterType.TEXT, name, value);
}

static number(name: string, value: string | number): QueryParameter {
// TODO - investigate large numbers here...
return new QueryParameter(ParameterType.NUMBER, name, value.toString());
}

static date(name: string, value: string | Date): QueryParameter {
return new QueryParameter(ParameterType.DATE, name, value.toString());
}

static enum(name: string, value: string): QueryParameter {
return new QueryParameter(ParameterType.ENUM, name, value.toString());
}
}
49 changes: 36 additions & 13 deletions tests/e2e/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { expect } from "chai";

import { QueryParameter } from "../../src/queryParameter";
import { DuneClient } from "../../src/client";
import {
DuneError,
ExecutionResponse,
ExecutionState,
GetStatusResponse,
} from "../../src/responseTypes";
import { DuneError, ExecutionState, GetStatusResponse } from "../../src/responseTypes";

const { DUNE_API_KEY } = process.env;
const apiKey: string = DUNE_API_KEY ? DUNE_API_KEY : "No API Key";
Expand All @@ -26,16 +21,16 @@ const expectAsyncThrow = async (promise: Promise<any>, message?: string | object
}
};

// beforeEach(() => {
// console.log = function () {};
// console.debug = function () {};
// console.error = function () {};
// });
beforeEach(() => {
// console.log = function () {};
console.debug = function () {};
console.error = function () {};
});

describe("DuneClient: execute", () => {
// This doesn't work if run too many times at once:
// https://discord.com/channels/757637422384283659/1019910980634939433/1026840715701010473
it.skip("returns expected results on sequence execute-cancel-get_status", async () => {
it("returns expected results on sequence execute-cancel-get_status", async () => {
const client = new DuneClient(apiKey);
// Long running query ID.
const queryID = 1229120;
Expand All @@ -60,6 +55,20 @@ describe("DuneClient: execute", () => {
expect(expectedStatus).to.be.deep.equal(status);
});

it("successfully executes with query parameters", async () => {
const client = new DuneClient(apiKey);
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.execute(queryID, parameters);
expect(execution.execution_id).is.not.null;
});

it("returns expected results on sequence execute-cancel-get_status", async () => {
const client = new DuneClient(apiKey);
// Execute and check state
Expand All @@ -79,6 +88,10 @@ describe("DuneClient: execute", () => {
});

describe("DuneClient: Errors", () => {
// TODO these errors can't be reached because post method is private
// {"error":"unknown parameters (undefined)"}
// {"error":"Invalid request body payload"}

it("returns invalid API key", async () => {
const client = new DuneClient("Bad Key");
await expectAsyncThrow(client.execute(1), "invalid API Key");
Expand All @@ -102,6 +115,16 @@ describe("DuneClient: Errors", () => {
await expectAsyncThrow(client.get_result(invalidJobID), expectedErrorMessage);
await expectAsyncThrow(client.cancel_execution(invalidJobID), expectedErrorMessage);
});
it.only("fails execute with unknown query parameter", async () => {
const client = new DuneClient(apiKey);
const queryID = 1215383;
const invalidParameterName = "Invalid Parameter Name";
const parameters = [QueryParameter.text(invalidParameterName, "")];
await expectAsyncThrow(
client.execute(queryID, parameters),
`unknown parameters (${invalidParameterName})`,
);
});
it("does not allow to execute private queries for other accounts.", async () => {
const client = new DuneClient(apiKey);
await expectAsyncThrow(
Expand Down
7 changes: 5 additions & 2 deletions tslint.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"extends": ["tslint:recommended", "tslint-config-prettier"]
}
"extends": ["tslint:recommended", "tslint-config-prettier"],
"rules": {
"no-console": false
}
}