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

Latest Results & Query CRUD Operations #26

Merged
merged 4 commits into from
Feb 14, 2024
Merged
Changes from 1 commit
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
Prev Previous commit
Next Next commit
latest results & crud operations
bh2smith committed Feb 14, 2024
commit 268cf667b9ffbf76d6e6e460285787289cd0132b
7 changes: 3 additions & 4 deletions src/api/execution.ts
Original file line number Diff line number Diff line change
@@ -14,10 +14,9 @@ export class ExecutionClient extends Router {
queryID: number,
parameters?: QueryParameter[],
): Promise<ExecutionResponse> {
const response = await this._post<ExecutionResponse>(
`query/${queryID}/execute`,
parameters,
);
const response = await this._post<ExecutionResponse>(`query/${queryID}/execute`, {
query_parameters: parameters ? parameters : [],
});
log.debug(logPrefix, `execute response ${JSON.stringify(response)}`);
return response as ExecutionResponse;
}
9 changes: 4 additions & 5 deletions src/api/extensions.ts
Original file line number Diff line number Diff line change
@@ -48,11 +48,10 @@ export class ExtendedClient extends ExecutionClient {
parameters?: QueryParameter[],
maxAgeHours: number = THREE_MONTHS_IN_HOURS,
): Promise<ResultsResponse> {
let results = await this._get<ResultsResponse>(
`/query/${queryId}/results`,
parameters,
);
const lastRun = results.execution_ended_at;
let results = await this._get<ResultsResponse>(`query/${queryId}/results`, {
query_parameters: parameters ? parameters : [],
});
const lastRun: Date = results.execution_ended_at!;
if (lastRun !== undefined && ageInHours(lastRun) > maxAgeHours) {
log.info(
logPrefix,
4 changes: 4 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./execution";
export * from "./extensions";
export * from "./query";
export * from "./router";
82 changes: 82 additions & 0 deletions src/api/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Assuming the existence of these imports based on your Python code
import { Router } from "./router";
import {
DuneQuery,
DuneError,
QueryParameter,
ExecutionResponse,
CreateQueryResponse,
} from "../types";
import { CreateQueryPayload, UpdateQueryPayload } from "../types/requestPayload";

export class QueryAPI extends Router {
/**
* Creates a Dune Query by ID
* https://dune.com/docs/api/api-reference/edit-queries/create-query/
*/
async createQuery(
name: string,
querySql: string,
params?: QueryParameter[],
isPrivate: boolean = false,
): Promise<DuneQuery> {
const payload: CreateQueryPayload = {
name,
query_sql: querySql,
is_private: isPrivate,
query_parameters: params ? params : [],
};
try {
const responseJson = await this._post<CreateQueryResponse>("query/", payload);
return this.getQuery(responseJson.query_id);
} catch (err: unknown) {
throw new Error(`Fokin Broken: ${err}`);
// throw new DuneError(responseJson, "CreateQueryResponse", err);
}
}

/**
* Retrieves a Dune Query by ID
* https://dune.com/docs/api/api-reference/edit-queries/get-query/
*/
async getQuery(queryId: number): Promise<DuneQuery> {
const responseJson = await this._get(`query/${queryId}`);
return responseJson as DuneQuery;
}

/**
* Updates a Dune Query by ID
* https://dune.com/docs/api/api-reference/edit-queries/update-query/
*/
async updateQuery(
queryId: number,
name?: string,
querySql?: string,
params?: QueryParameter[],
description?: string,
tags?: string[],
): Promise<number> {
const parameters: UpdateQueryPayload = {};
if (name !== undefined) parameters.name = name;
if (description !== undefined) parameters.description = description;
if (tags !== undefined) parameters.tags = tags;
if (querySql !== undefined) parameters.query_sql = querySql;
if (params !== undefined) parameters.parameters = params;

Check failure on line 64 in src/api/query.ts

GitHub Actions / build (20.x)

Property 'parameters' does not exist on type 'UpdateQueryPayload'.

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

try {
const responseJson = await this._patch<CreateQueryResponse>(
`query/${queryId}`,
parameters,
);
return responseJson.query_id;
} catch (err: unknown) {
throw new Error(`Fokin Broken: ${err}`);
// throw new DuneError(responseJson, "UpdateQueryResponse", err);
}
}
}
30 changes: 14 additions & 16 deletions src/api/router.ts
Original file line number Diff line number Diff line change
@@ -2,6 +2,12 @@
import fetch from "cross-fetch";
import log from "loglevel";
import { logPrefix } from "../utils";
import {
ExecuteQueryPayload,
RequestPayload,
isExecuteQueryPayload,

Check failure on line 8 in src/api/router.ts

GitHub Actions / build (20.x)

'"../types/requestPayload"' has no exported member named 'isExecuteQueryPayload'. Did you mean 'ExecuteQueryPayload'?
payloadJSON,
} from "../types/requestPayload";

const BASE_URL = "https://api.dune.com/api/v1";

@@ -48,43 +54,35 @@
protected async _request<T>(
method: RequestMethod,
url: string,
params?: QueryParameter[],
payload?: RequestPayload,
): Promise<T> {
log.debug(
logPrefix,
`${method} 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 requestParameters = JSON.stringify({ query_parameters: reducedParams || {} });
const payloadData = payloadJSON(payload);
log.debug(logPrefix, `${method} received input url=${url}, payload=${payloadData}`);
const response = fetch(url, {
method,
headers: {
"x-dune-api-key": this.apiKey,
},
// conditionally add the body property
...(method !== RequestMethod.GET && {
body: requestParameters,
body: payloadJSON(payload),
}),
...(method === RequestMethod.GET && {
params: requestParameters,
params: payloadJSON(payload),
}),
});
return this._handleResponse<T>(response);
}

protected async _get<T>(route: string, params?: QueryParameter[]): Promise<T> {
protected async _get<T>(route: string, params?: RequestPayload): Promise<T> {
return this._request(RequestMethod.GET, this.url(route), params);
}

protected async _post<T>(route: string, params?: QueryParameter[]): Promise<T> {
protected async _post<T>(route: string, params?: RequestPayload): Promise<T> {
return this._request(RequestMethod.POST, this.url(route), params);
}

protected async _patch<T>(route: string, params?: QueryParameter[]): Promise<T> {
protected async _patch<T>(route: string, params?: RequestPayload): Promise<T> {
return this._request(RequestMethod.PATCH, this.url(route), params);
}

176 changes: 176 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import {
ExecutionResponse,
GetStatusResponse,
ResultsResponse,
ExecutionState,
DuneError,
QueryParameter,
} from "./types";
import fetch from "cross-fetch";
import { sleep } from "./utils";
import log from "loglevel";
import { logPrefix } from "./utils";

const BASE_URL = "https://api.dune.com/api/v1";
const TERMINAL_STATES = [
ExecutionState.CANCELLED,
ExecutionState.COMPLETED,
ExecutionState.FAILED,
];

enum RequestMethod {
GET = "GET",
POST = "POST",
PATCH = "PATCH",
}

// This class implements all the routes defined in the Dune API Docs: https://dune.com/docs/api/
export class OldDuneClient {
apiKey: string;

constructor(apiKey: string) {
this.apiKey = apiKey;
}

private 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);
}
}
return apiResponse;
}

private async _request<T>(
method: RequestMethod,
url: string,
params?: QueryParameter[],
): Promise<T> {
log.debug(
logPrefix,
`${method} 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,
headers: {
"x-dune-api-key": this.apiKey,
},
// conditionally add the body property
...(method !== RequestMethod.GET && {
body: JSON.stringify({ query_parameters: reducedParams || {} }),
}),
});
return this._handleResponse<T>(response);
}

private async _get<T>(url: string): Promise<T> {
return this._request(RequestMethod.GET, url);
}

private async _post<T>(url: string, params?: QueryParameter[]): Promise<T> {
return this._request(RequestMethod.POST, url, params);
}

private async _patch<T>(url: string, params?: QueryParameter[]): Promise<T> {
return this._request(RequestMethod.PATCH, url, params);
}

async execute(
queryID: number,
parameters?: QueryParameter[],
): Promise<ExecutionResponse> {
const response = await this._post<ExecutionResponse>(
`${BASE_URL}/query/${queryID}/execute`,
parameters,
);
log.debug(logPrefix, `execute response ${JSON.stringify(response)}`);
return response as ExecutionResponse;
}

async getStatus(jobID: string): Promise<GetStatusResponse> {
const response: GetStatusResponse = await this._get(
`${BASE_URL}/execution/${jobID}/status`,
);
log.debug(logPrefix, `get_status response ${JSON.stringify(response)}`);
return response as GetStatusResponse;
}

async getResult(jobID: string): Promise<ResultsResponse> {
const response: ResultsResponse = await this._get(
`${BASE_URL}/execution/${jobID}/results`,
);
log.debug(logPrefix, `get_result response ${JSON.stringify(response)}`);
return response as ResultsResponse;
}

async cancelExecution(jobID: string): Promise<boolean> {
const { success }: { success: boolean } = await this._post(
`${BASE_URL}/execution/${jobID}/cancel`,
);
return success;
}

async runQuery(
queryID: number,
parameters?: QueryParameter[],
pingFrequency: number = 1,
): Promise<ResultsResponse> {
log.info(
logPrefix,
`refreshing query https://dune.com/queries/${queryID} with parameters ${JSON.stringify(
parameters,
)}`,
);
const { execution_id: jobID } = await this.execute(queryID, parameters);
let { state } = await this.getStatus(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.getStatus(jobID)).state;
}
if (state === ExecutionState.COMPLETED) {
return this.getResult(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);
}
}

/**
* @deprecated since version 0.0.2 Use runQuery
*/
async refresh(
queryID: number,
parameters?: QueryParameter[],
pingFrequency: number = 1,
): Promise<ResultsResponse> {
return this.runQuery(queryID, parameters, pingFrequency);
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { ExtendedClient as DuneClient } from "./api/extensions";
export * from "./api";
export * from "./types";
10 changes: 9 additions & 1 deletion src/types/queryParameter.ts
Original file line number Diff line number Diff line change
@@ -21,7 +21,6 @@ export class QueryParameter {
}

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

@@ -32,4 +31,13 @@ export class QueryParameter {
static enum(name: string, value: string): QueryParameter {
return new QueryParameter(ParameterType.ENUM, name, value.toString());
}

static unravel(params?: QueryParameter[]): Record<string, string> | undefined {
// Transform Query Parameter list into "dict"
let reducedParams = params?.reduce<Record<string, string>>(
(acc, { name, value }) => ({ ...acc, [name]: value }),
{},
);
return reducedParams || {};
}
}
Loading