Skip to content

Commit

Permalink
[Feature] UploadCSV (#36)
Browse files Browse the repository at this point in the history

1. Introducing data upload endpoint according to https://docs.dune.com/api-reference/tables/endpoint/upload
2. exposes base router _post method as a public method to the entire client SDK. This makes sense so not to hinder the user (inline comments added).
3. There was a recent breakage on the error messaging that was also fixed here.
4. We also introduce a couple of new types (one SuccessResponse that should have existed a while ago) related to the function parameters.
  • Loading branch information
bh2smith authored Mar 16, 2024
1 parent 5a0cfc3 commit 86d0349
Show file tree
Hide file tree
Showing 9 changed files with 88 additions and 25 deletions.
23 changes: 22 additions & 1 deletion src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
QueryParameter,
GetStatusResponse,
ExecutionResponseCSV,
SuccessResponse,
} from "../types";
import { ageInHours, sleep } from "../utils";
import log from "loglevel";
Expand All @@ -22,7 +23,11 @@ import {
POLL_FREQUENCY_SECONDS,
THREE_MONTHS_IN_HOURS,
} from "../constants";
import { ExecutionParams, ExecutionPerformance } from "../types/requestPayload";
import {
ExecutionParams,
ExecutionPerformance,
UploadCSVParams,
} from "../types/requestPayload";
import { QueryAPI } from "./query";

/// Various states of query execution that are "terminal".
Expand Down Expand Up @@ -232,6 +237,22 @@ export class DuneClient {
return results;
}

/**
* Allows for anyone to upload a CSV as a table in Dune.
* The size limit per upload is currently 200MB.
* Storage is limited by plan, 1MB on free, 15GB on plus, and 50GB on premium.
* @param params UploadCSVParams relevant fields related to dataset upload.
* @returns boolean representing if upload was successful.
*/
async uploadCsv(params: UploadCSVParams): Promise<boolean> {
const response = await this.exec.post<SuccessResponse>("table/upload/csv", params);
try {
return Boolean(response.success);
} catch (err) {
throw new DuneError(`UploadCsvResponse ${JSON.stringify(response)}`);
}
}

private async _runInner(
queryID: number,
params?: ExecutionParams,
Expand Down
5 changes: 3 additions & 2 deletions src/api/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ExecutionResponseCSV,
concatResultResponse,
concatResultCSV,
SuccessResponse,
} from "../types";
import log from "loglevel";
import { logPrefix } from "../utils";
Expand Down Expand Up @@ -45,7 +46,7 @@ export class ExecutionAPI extends Router {
performance = performance ? performance : ExecutionPerformance.Medium;
}

const response = await this._post<ExecutionResponse>(`query/${queryID}/execute`, {
const response = await this.post<ExecutionResponse>(`query/${queryID}/execute`, {
query_parameters,
performance,
});
Expand All @@ -60,7 +61,7 @@ export class ExecutionAPI extends Router {
* @returns {boolean} indicating if success of cancellation request.
*/
async cancelExecution(executionId: string): Promise<boolean> {
const { success }: { success: boolean } = await this._post(
const { success } = await this.post<SuccessResponse>(
`execution/${executionId}/cancel`,
);
return success;
Expand Down
10 changes: 5 additions & 5 deletions src/api/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class QueryAPI extends Router {
params.is_private = false;
}
params.query_parameters = params.query_parameters ? params.query_parameters : [];
const responseJson = await this._post<CreateQueryResponse>("query/", params);
const responseJson = await this.post<CreateQueryResponse>("query/", params);
return responseJson.query_id;
}

Expand Down Expand Up @@ -71,7 +71,7 @@ export class QueryAPI extends Router {
* @returns {boolean} indicating success of request.
*/
public async archiveQuery(queryId: number): Promise<boolean> {
const response = await this._post<EditQueryResponse>(`query/${queryId}/archive`);
const response = await this.post<EditQueryResponse>(`query/${queryId}/archive`);
const query = await this.readQuery(response.query_id);
return query.is_archived;
}
Expand All @@ -83,7 +83,7 @@ export class QueryAPI extends Router {
* @returns {boolean} indicating success of request.
*/
public async unarchiveQuery(queryId: number): Promise<boolean> {
const response = await this._post<EditQueryResponse>(`query/${queryId}/unarchive`);
const response = await this.post<EditQueryResponse>(`query/${queryId}/unarchive`);
const query = await this.readQuery(response.query_id);
return query.is_archived;
}
Expand All @@ -96,7 +96,7 @@ export class QueryAPI extends Router {
* @returns {number} ID of the query made private.
*/
public async makePrivate(queryId: number): Promise<number> {
const response = await this._post<EditQueryResponse>(`query/${queryId}/private`);
const response = await this.post<EditQueryResponse>(`query/${queryId}/private`);
const query = await this.readQuery(response.query_id);
if (!query.is_private) {
throw new DuneError("Query was not made private!");
Expand All @@ -111,7 +111,7 @@ export class QueryAPI extends Router {
* @returns {number} ID of the query made public.
*/
public async makePublic(queryId: number): Promise<number> {
const response = await this._post<EditQueryResponse>(`query/${queryId}/unprivate`);
const response = await this.post<EditQueryResponse>(`query/${queryId}/unprivate`);
const query = await this.readQuery(response.query_id);
if (query.is_private) {
throw new DuneError("Query is still private.");
Expand Down
15 changes: 11 additions & 4 deletions src/api/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ export class Router {
this.apiKey = apiKey;
this.apiVersion = apiVersion;
}
/**
* Allows a post to any route supported by DuneAPI.
* Meant to be low level call only used by available functions,
* but accessible if new routes become available before the SDK catches up.
* @param route request path of the http post
* @param params payload sent with request (should be aligned with what the interface supports)
* @returns a flexible data type representing whatever is expected to be returned from the request.
*/
async post<T>(route: string, params?: RequestPayload): Promise<T> {
return this._request<T>(RequestMethod.POST, this.url(route), params);
}

protected async _handleResponse<T>(responsePromise: Promise<Response>): Promise<T> {
let result;
Expand Down Expand Up @@ -114,10 +125,6 @@ export class Router {
return this._request<T>(RequestMethod.GET, url, params, raw);
}

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

protected async _patch<T>(route: string, params?: RequestPayload): Promise<T> {
return this._request<T>(RequestMethod.PATCH, this.url(route), params);
}
Expand Down
10 changes: 9 additions & 1 deletion src/types/requestPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@ export enum ExecutionPerformance {
Large = "large",
}

export type UploadCSVParams = {
table_name: string;
data: string;
description?: string;
is_private?: boolean;
};

/// Payload sent upon requests to Dune API.
export type RequestPayload =
| GetResultParams
| ExecuteQueryParams
| UpdateQueryParams
| CreateQueryParams;
| CreateQueryParams
| UploadCSVParams;

/// Utility method used by router to parse request payloads.
export function payloadJSON(payload?: RequestPayload): string {
Expand Down
4 changes: 4 additions & 0 deletions src/types/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export interface CreateQueryResponse {
query_id: number;
}

export type SuccessResponse = {
success: boolean;
};

/// Various query times related to an query status request.
export interface TimeData {
submitted_at: Date;
Expand Down
17 changes: 17 additions & 0 deletions tests/e2e/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,21 @@ describe("DuneClient Extensions", () => {
const query = await premiumClient.query.readQuery(queryID);
expect(query.is_archived).to.be.equal(true);
});

it("uploadCSV", async () => {
const premiumClient = new DuneClient(PLUS_KEY);
const public_success = await premiumClient.uploadCsv({
table_name: "ts_client_test",
description: "testing csv upload from node",
data: "column1,column2\nvalue1,value2\nvalue3,value4",
});
expect(public_success).to.be.equal(true);

const private_success = await premiumClient.uploadCsv({
table_name: "ts_client_test_private",
data: "column1,column2\nvalue1,value2\nvalue3,value4",
is_private: true
});
expect(private_success).to.be.equal(true);
});
});
25 changes: 15 additions & 10 deletions tests/e2e/executionAPI.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,18 +168,20 @@ describe("ExecutionAPI: Errors", () => {

it("returns invalid API key", async () => {
const bad_client = new ExecutionAPI("Bad Key");
await expectAsyncThrow(bad_client.executeQuery(1), "invalid API Key");
await expectAsyncThrow(bad_client.executeQuery(1), "Response Error: invalid API Key");
});

it("returns query not found error", async () => {
await expectAsyncThrow(client.executeQuery(999999999), "Query not found");
await expectAsyncThrow(client.executeQuery(0), "Query not found");
await expectAsyncThrow(
client.executeQuery(999999999),
"Response Error: Query not found",
);
await expectAsyncThrow(client.executeQuery(0), "Response Error: Query not found");
});
it("returns invalid job id", async () => {
await expectAsyncThrow(client.executeQuery(999999999), "Query not found");

it("returns invalid job id", async () => {
const invalidJobID = "Wonky Job ID";
const expectedErrorMessage = `The requested execution ID (ID: ${invalidJobID}) is invalid.`;
const expectedErrorMessage = `Response Error: The requested execution ID (ID: ${invalidJobID}) is invalid.`;
await expectAsyncThrow(client.getExecutionStatus(invalidJobID), expectedErrorMessage);
await expectAsyncThrow(
client.getExecutionResults(invalidJobID),
Expand All @@ -194,23 +196,26 @@ describe("ExecutionAPI: Errors", () => {
client.executeQuery(queryID, {
query_parameters: [QueryParameter.text(invalidParameterName, "")],
}),
`unknown parameters (${invalidParameterName})`,
`Response Error: unknown parameters (${invalidParameterName})`,
);
});
it("does not allow to execute private queries for other accounts.", async () => {
await expectAsyncThrow(client.executeQuery(1348384), "Query not found");
await expectAsyncThrow(
client.executeQuery(1348384),
"Response Error: Query not found",
);
});
it("fails with unhandled FAILED_TYPE_UNSPECIFIED when query won't compile", async () => {
// Execute and check state
// V1 query: 1348966
await expectAsyncThrow(
client.getExecutionResults("01GEHG4AY1Z9JBR3BYB20E7RGH"),
"FAILED_TYPE_EXECUTION_FAILED",
"Response Error: FAILED_TYPE_EXECUTION_FAILED",
);
// V2 -query: :1349019
await expectAsyncThrow(
client.getExecutionResults("01GEHGXHQ25XWMVFJ4G2HZ5MGS"),
"FAILED_TYPE_EXECUTION_FAILED",
"Response Error: FAILED_TYPE_EXECUTION_FAILED",
);
});
});
4 changes: 2 additions & 2 deletions tests/e2e/queryAPI.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { QueryParameter, QueryAPI } from "../../src/";
import { PLUS_KEY, BASIC_KEY, expectAsyncThrow } from "./util";

const PREMIUM_PLAN_MESSAGE =
"CRUD queries is an advanced feature included only in our premium subscription plans. Please upgrade your plan to use it.";
"Response Error: Query management endpoints are only available in our paid plans. Please upgrade to a paid plan to use it.";

describe("QueryAPI: Premium - CRUD Operations", () => {
let plusClient: QueryAPI;
Expand All @@ -29,7 +29,7 @@ describe("QueryAPI: Premium - CRUD Operations", () => {
expect(updatedQueryId).to.be.equal(recoveredQuery.query_id);
});

it.only("unarchive, make public, make private, rearchive", async () => {
it("unarchive, make public, make private, rearchive", async () => {
const queryId = 3530410;
let query = await plusClient.readQuery(queryId);
expect(query.is_archived).to.be.equal(true);
Expand Down

0 comments on commit 86d0349

Please sign in to comment.