Refactor query integration to new api and query result set builder

This commit is contained in:
Jim Myers 2023-05-15 13:11:27 -04:00
parent 4ea1ae8dac
commit 7c0ceb89af
42 changed files with 438 additions and 449 deletions

View File

@ -1,103 +0,0 @@
import {
Query,
CreateQueryResp,
QueryResultResp,
CreateQueryJson,
QueryResultJson,
ApiClient,
} from "./types";
import axios, { AxiosError } from "axios";
import { UnexpectedSDKError } from "./errors";
const PARSE_ERROR_MSG =
"the api returned an error and there was a fatal client side error parsing that error msg";
export class API implements ApiClient {
#baseUrl: string;
#headers: Record<string, string>;
#sdkVersion: string;
#sdkPackage: string;
constructor(baseUrl: string, sdkPackage: string, sdkVersion: string, apiKey: string) {
this.#baseUrl = baseUrl;
this.#sdkPackage = sdkPackage;
this.#sdkVersion = sdkVersion;
this.#headers = {
Accept: "application/json",
"Content-Type": "application/json",
"x-api-key": apiKey,
};
}
getUrl(path: string): string {
return `${this.#baseUrl}/${path}`;
}
async createQuery(query: Query): Promise<CreateQueryResp> {
let result;
try {
result = await axios.post(
this.getUrl("queries"),
{
sql: query.sql,
ttl_minutes: query.ttlMinutes,
cached: query.cached,
sdk_package: this.#sdkPackage,
sdk_version: this.#sdkVersion,
},
{ headers: this.#headers }
);
} catch (err) {
let errData = err as AxiosError;
result = errData.response;
if (!result) {
throw new UnexpectedSDKError(PARSE_ERROR_MSG);
}
}
let data: CreateQueryJson | null;
if (result.status >= 200 && result.status < 300) {
data = result.data;
} else {
data = null;
}
return {
statusCode: result.status,
statusMsg: result.statusText,
errorMsg: data?.errors,
data,
};
}
async getQueryResult(queryID: string, pageNumber: number, pageSize: number): Promise<QueryResultResp> {
let result;
try {
result = await axios.get(this.getUrl(`queries/${queryID}`), {
params: { pageNumber: pageNumber, pageSize: pageSize },
method: "GET",
headers: this.#headers,
});
} catch (err) {
let errData = err as AxiosError;
result = errData.response;
if (!result) {
throw new UnexpectedSDKError(PARSE_ERROR_MSG);
}
}
let data: QueryResultJson | null;
if (result.status >= 200 && result.status < 300) {
data = result.data;
} else {
data = null;
}
return {
statusCode: result.status,
statusMsg: result.statusText,
errorMsg: data?.errors,
data,
};
}
}

View File

@ -25,43 +25,19 @@ import {
const PARSE_ERROR_MSG = "the api returned an error and there was a fatal client side error parsing that error msg";
class Api {
export class Api {
url: string;
#baseUrl: string;
#headers: Record<string, string>;
#sdkVersion: string;
#sdkPackage: string;
#apiKey: string;
#MAX_RETRIES: number;
#BACKOFF_FACTOR: number;
#STATUS_FORCE_LIST: number[];
#METHOD_ALLOWLIST: string[];
constructor(
baseUrl: string,
sdkPackage: string,
sdkVersion: string,
apiKey: string,
max_retries: number = 10,
backoff_factor: number = 1,
status_forcelist: number[] = [429, 500, 502, 503, 504],
method_allowlist: string[] = ["HEAD", "GET", "PUT", "POST", "DELETE", "OPTIONS", "TRACE"]
) {
constructor(baseUrl: string, apiKey: string) {
this.#baseUrl = baseUrl;
this.url = this.getUrl();
this.#apiKey = apiKey;
this.#sdkPackage = sdkPackage;
this.#sdkVersion = sdkVersion;
this.#headers = {
Accept: "application/json",
"Content-Type": "application/json",
"x-api-key": apiKey,
};
// Session Settings
this.#MAX_RETRIES = max_retries;
this.#BACKOFF_FACTOR = backoff_factor;
this.#STATUS_FORCE_LIST = status_forcelist;
this.#METHOD_ALLOWLIST = method_allowlist;
}
getUrl(): string {

17
js/src/defaults.ts Normal file
View File

@ -0,0 +1,17 @@
import { version } from "../package.json";
import { SdkDefaults } from "./types";
export const DEFAULTS: SdkDefaults = {
apiBaseUrl: "https://api-v2.flipsidecrypto.xyz",
ttlMinutes: 60,
maxAgeMinutes: 0,
cached: true,
dataProvider: "flipside",
dataSource: "snowflake-default",
timeoutMinutes: 20,
retryIntervalSeconds: 0.5,
pageSize: 100000,
pageNumber: 1,
sdkPackage: "js",
sdkVersion: version,
};

View File

@ -0,0 +1,53 @@
export class ApiError extends Error {
constructor(name: string, code: number, message: string) {
super(`${name}: message=${message}, code=${code}`);
}
}
export const errorCodes: { [key: string]: number } = {
MethodValidationError: -32000,
QueryRunNotFound: -32099,
SqlStatementNotFound: -32100,
TemporalError: -32150,
QueryRunNotFinished: -32151,
ResultTransformError: -32152,
ResultFormatNotSupported: -32153,
RowCountCouldNotBeComputed: -32154,
QueryResultColumnMetadataMissing: -32155,
InvalidSortColumn: -32156,
ColumnSummaryQueryFailed: -32157,
QueryResultColumnMetadataMissingColumnName: -32158,
QueryResultColumnMetadataMissingColumnType: -32159,
NoQueryRunsFoundinQueryText: -32160,
DuckDBError: -32161,
RefreshableQueryNotFound: -32162,
AuthorizationError: -32163,
DataSourceNotFound: -32164,
QueryRunInvalidStateToCancel: -32165,
DataProviderAlreadyExists: -32166,
DataProviderNotFound: -32167,
DataSourceAlreadyExists: -32168,
AdminOnly: -32169,
RequestedPageSizeTooLarge: -32170,
MaxConcurrentQueries: -32171,
};
export function getExceptionByErrorCode(errorCode?: number, message?: string): ApiError {
if (!errorCode || !message) {
return new ApiError("UnknownAPIError", errorCode || -1, message || "");
}
let errorName: string | null = null;
for (const key of Object.keys(errorCodes)) {
if (errorCodes[key] === errorCode) {
errorName = key;
break;
}
}
if (errorName === null) {
return new ApiError("UnknownAPIError", errorCode, message);
}
return new ApiError(errorName, errorCode, message);
}

View File

@ -3,3 +3,4 @@ export * from "./server-errors";
export * from "./sdk-errors";
export * from "./query-run-errors";
export * from "./user-errors";
export * from "./api-error";

View File

@ -1,18 +1,14 @@
import { API } from "./api";
import { Api } from "./api";
import { QueryIntegration } from "./integrations";
import { version } from '../package.json';
const API_BASE_URL = "https://api.flipsidecrypto.com";
const SDK_PACKAGE = "js";
const SDK_VERSION = version;
import { QueryResultSet } from "./types";
import { DEFAULTS } from "./defaults";
export class Flipside {
query: QueryIntegration;
constructor(apiKey: string, apiBaseUrl: string = API_BASE_URL) {
constructor(apiKey: string, apiBaseUrl: string = DEFAULTS.apiBaseUrl) {
// Setup API, which will be passed to integrations
const api = new API(apiBaseUrl, SDK_PACKAGE, SDK_VERSION, apiKey);
const api = new Api(apiBaseUrl, apiKey);
// declare integrations on Flipside client
this.query = new QueryIntegration(api);
@ -21,5 +17,5 @@ export class Flipside {
export * from "./types";
export * from "./errors";
import { QueryResultSet } from "./types";
export { QueryResultSet };

View File

@ -1,195 +1,270 @@
import {
Query,
QueryDefaults,
QueryStatusFinished,
QueryStatusError,
QueryResultJson,
CreateQueryJson,
ApiClient,
QueryResultSet,
CreateQueryRunRpcParams,
CreateQueryRunRpcResponse,
mapApiQueryStateToStatus,
GetQueryRunRpcResponse,
Filter,
SortBy,
QueryRun,
ResultFormat,
SqlStatement,
} from "../../types";
import {
expBackOff,
getElapsedLinearSeconds,
linearBackOff,
} from "../../utils/sleep";
import { getElapsedLinearSeconds, linearBackOff } from "../../utils/sleep";
import {
QueryRunExecutionError,
QueryRunRateLimitError,
QueryRunTimeoutError,
ServerError,
UserError,
UnexpectedSDKError,
getExceptionByErrorCode,
} from "../../errors";
import { QueryResultSetBuilder } from "./query-result-set-builder";
const DEFAULTS: QueryDefaults = {
ttlMinutes: 60,
cached: true,
timeoutMinutes: 20,
retryIntervalSeconds: 0.5,
pageSize: 100000,
pageNumber: 1,
};
import { Api } from "../../api";
import { DEFAULTS } from "../../defaults";
export class QueryIntegration {
#api: ApiClient;
#defaults: QueryDefaults;
#api: Api;
constructor(api: ApiClient, defaults: QueryDefaults = DEFAULTS) {
constructor(api: Api) {
this.#api = api;
this.#defaults = defaults;
}
#setQueryDefaults(query: Query): Query {
return { ...this.#defaults, ...query };
}
async run(query: Query): Promise<QueryResultSet> {
query = this.#setQueryDefaults(query);
let createQueryRunParams: CreateQueryRunRpcParams = {
resultTTLHours: query.ttlMinutes ? Math.floor(query.ttlMinutes / 60) : DEFAULTS.ttlMinutes,
sql: query.sql,
maxAgeMinutes: query.maxAgeMinutes ? query.maxAgeMinutes : DEFAULTS.maxAgeMinutes,
tags: {
sdk_language: "javascript",
sdk_package: query.sdkPackage ? query.sdkPackage : DEFAULTS.sdkPackage,
sdk_version: query.sdkVersion ? query.sdkVersion : DEFAULTS.sdkVersion,
},
dataSource: query.dataSource ? query.dataSource : DEFAULTS.dataSource,
dataProvider: query.dataProvider ? query.dataProvider : DEFAULTS.dataProvider,
};
const [createQueryJson, createQueryErr] = await this.#createQuery(query);
if (createQueryErr) {
const createQueryRunRpcResponse = await this.#createQuery(createQueryRunParams);
if (createQueryRunRpcResponse.error) {
return new QueryResultSetBuilder({
queryResultJson: null,
error: createQueryErr,
error: getExceptionByErrorCode(createQueryRunRpcResponse.error.code, createQueryRunRpcResponse.error.message),
});
}
if (!createQueryJson) {
if (!createQueryRunRpcResponse.result?.queryRun) {
return new QueryResultSetBuilder({
queryResultJson: null,
error: new UnexpectedSDKError(
"expected a `createQueryJson` but got null"
),
error: new UnexpectedSDKError("expected a `createQueryRunRpcResponse.result.queryRun` but got null"),
});
}
const [getQueryResultJson, getQueryErr] = await this.#getQueryResult(
createQueryJson.token,
query.pageNumber || 1,
query.pageSize || 100000,
);
// loop to get query state
const [queryRunRpcResp, queryError] = await this.#getQueryRunInLoop({
queryRunId: createQueryRunRpcResponse.result?.queryRun.id,
});
if (getQueryErr) {
if (queryError) {
return new QueryResultSetBuilder({
queryResultJson: null,
error: getQueryErr,
error: queryError,
});
}
if (!getQueryResultJson) {
if (queryRunRpcResp && queryRunRpcResp.error) {
return new QueryResultSetBuilder({
queryResultJson: null,
error: new UnexpectedSDKError(
"expected a `getQueryResultJson` but got null"
),
error: getExceptionByErrorCode(queryRunRpcResp.error.code, queryRunRpcResp.error.message),
});
}
const queryRun = queryRunRpcResp?.result?.queryRun;
if (!queryRun) {
return new QueryResultSetBuilder({
error: new UnexpectedSDKError("expected a `queryRunRpcResp.result.queryRun` but got null"),
});
}
// get the query results
const queryResultResp = await this.#api.getQueryResult({
queryRunId: queryRun.id,
format: ResultFormat.csv,
page: {
number: query.pageNumber || 1,
size: query.pageSize || 100000,
},
});
if (queryResultResp && queryResultResp.error) {
return new QueryResultSetBuilder({
error: getExceptionByErrorCode(queryResultResp.error.code, queryResultResp.error.message),
});
}
const queryResults = queryResultResp.result;
if (!queryResults) {
return new QueryResultSetBuilder({
error: new UnexpectedSDKError("expected a `queryResultResp.result` but got null"),
});
}
return new QueryResultSetBuilder({
queryResultJson: getQueryResultJson,
getQueryRunResultsRpcResult: queryResults,
getQueryRunRpcResult: queryRunRpcResp.result,
error: null,
});
}
async #createQuery(
query: Query,
attempts: number = 0
): Promise<
[
CreateQueryJson | null,
QueryRunRateLimitError | ServerError | UserError | null
]
> {
const resp = await this.#api.createQuery(query);
if (resp.statusCode <= 299) {
return [resp.data, null];
async getQueryResults({
queryRunId,
pageNumber = DEFAULTS.pageNumber,
pageSize = DEFAULTS.pageSize,
filters,
sortBy,
}: {
queryRunId: string;
pageNumber?: number;
pageSize?: number;
filters?: Filter[];
sortBy?: SortBy[];
}): Promise<QueryResultSet> {
const queryRunResp = await this.#api.getQueryRun({ queryRunId });
if (queryRunResp.error) {
return new QueryResultSetBuilder({
error: getExceptionByErrorCode(queryRunResp.error.code, queryRunResp.error.message),
});
}
if (resp.statusCode !== 429) {
if (resp.statusCode >= 400 && resp.statusCode <= 499) {
let errorMsg = resp.statusMsg || "user error";
if (resp.errorMsg) {
errorMsg = resp.errorMsg;
}
return [null, new UserError(resp.statusCode, errorMsg)];
}
return [
null,
new ServerError(resp.statusCode, resp.statusMsg || "server error"),
];
if (!queryRunResp.result) {
return new QueryResultSetBuilder({
error: new UnexpectedSDKError("expected an `rpc_response.result` but got null"),
});
}
let shouldContinue = await expBackOff({
attempts,
timeoutMinutes: this.#defaults.timeoutMinutes,
intervalSeconds: this.#defaults.retryIntervalSeconds,
if (!queryRunResp.result?.queryRun) {
return new QueryResultSetBuilder({
error: new UnexpectedSDKError("expected an `rpc_response.result.queryRun` but got null"),
});
}
const queryRun = queryRunResp.result.redirectedToQueryRun
? queryRunResp.result.redirectedToQueryRun
: queryRunResp.result.queryRun;
const queryResultResp = await this.#api.getQueryResult({
queryRunId: queryRun.id,
format: ResultFormat.csv,
page: {
number: pageNumber,
size: pageSize,
},
filters,
sortBy,
});
if (!shouldContinue) {
return [null, new QueryRunRateLimitError()];
if (queryResultResp.error) {
return new QueryResultSetBuilder({
error: getExceptionByErrorCode(queryResultResp.error.code, queryResultResp.error.message),
});
}
return this.#createQuery(query, attempts + 1);
return new QueryResultSetBuilder({
getQueryRunResultsRpcResult: queryResultResp.result,
getQueryRunRpcResult: queryRunResp.result,
error: null,
});
}
async #getQueryResult(
queryID: string,
pageNumber: number,
pageSize: number,
attempts: number = 0
): Promise<
[
QueryResultJson | null,
QueryRunTimeoutError | ServerError | UserError | null
]
async getQueryRun({ queryRunId }: { queryRunId: string }): Promise<QueryRun> {
const resp = await this.#api.getQueryRun({ queryRunId });
if (resp.error) {
throw getExceptionByErrorCode(resp.error.code, resp.error.message);
}
if (!resp.result) {
throw new UnexpectedSDKError("expected an `rpc_response.result` but got null");
}
if (!resp.result?.queryRun) {
throw new UnexpectedSDKError("expected an `rpc_response.result.queryRun` but got null");
}
return resp.result.redirectedToQueryRun ? resp.result.redirectedToQueryRun : resp.result.queryRun;
}
async getSqlStatement({ sqlStatementId }: { sqlStatementId: string }): Promise<SqlStatement> {
const resp = await this.#api.getSqlStatement({ sqlStatementId });
if (resp.error) {
throw getExceptionByErrorCode(resp.error.code, resp.error.message);
}
if (!resp.result) {
throw new UnexpectedSDKError("expected an `rpc_response.result` but got null");
}
if (!resp.result?.sqlStatement) {
throw new UnexpectedSDKError("expected an `rpc_response.result.sqlStatement` but got null");
}
return resp.result.sqlStatement;
}
async cancelQueryRun({ queryRunId }: { queryRunId: string }): Promise<QueryRun> {
const resp = await this.#api.cancelQueryRun({ queryRunId });
if (resp.error) {
throw getExceptionByErrorCode(resp.error.code, resp.error.message);
}
if (!resp.result) {
throw new UnexpectedSDKError("expected an `rpc_response.result` but got null");
}
if (!resp.result?.queryRun) {
throw new UnexpectedSDKError("expected an `rpc_response.result.queryRun` but got null");
}
return resp.result.queryRun;
}
async #createQuery(params: CreateQueryRunRpcParams, attempts: number = 0): Promise<CreateQueryRunRpcResponse> {
return await this.#api.createQuery(params);
}
async #getQueryRunInLoop({
queryRunId,
attempts = 0,
}: {
queryRunId: string;
attempts?: number;
}): Promise<
[GetQueryRunRpcResponse | null, QueryRunTimeoutError | QueryRunExecutionError | ServerError | UserError | null]
> {
const resp = await this.#api.getQueryResult(queryID, pageNumber, pageSize);
if (resp.statusCode > 299) {
if (resp.statusCode >= 400 && resp.statusCode <= 499) {
let errorMsg = resp.statusMsg || "user input error";
if (resp.errorMsg) {
errorMsg = resp.errorMsg;
}
return [null, new UserError(resp.statusCode, errorMsg)];
}
return [
null,
new ServerError(resp.statusCode, resp.statusMsg || "server error"),
];
let resp = await this.#api.getQueryRun({ queryRunId });
if (resp.error) {
}
const queryRun = resp.result?.redirectedToQueryRun ? resp.result.redirectedToQueryRun : resp.result?.queryRun;
if (!queryRun) {
throw new Error("query run not found");
}
if (!resp.data) {
throw new Error(
"valid status msg returned from server but no data exists in the response"
);
const queryRunState = mapApiQueryStateToStatus(queryRun.state);
if (queryRunState === QueryStatusFinished) {
return [resp, null];
}
if (resp.data.status === QueryStatusFinished) {
return [resp.data, null];
}
if (resp.data.status === QueryStatusError) {
if (queryRunState === QueryStatusError) {
return [null, new QueryRunExecutionError()];
}
let shouldContinue = await linearBackOff({
attempts,
timeoutMinutes: this.#defaults.timeoutMinutes,
intervalSeconds: this.#defaults.retryIntervalSeconds,
timeoutMinutes: DEFAULTS.timeoutMinutes,
intervalSeconds: DEFAULTS.retryIntervalSeconds,
});
if (!shouldContinue) {
const elapsedSeconds = getElapsedLinearSeconds({
attempts,
timeoutMinutes: this.#defaults.timeoutMinutes,
intervalSeconds: this.#defaults.retryIntervalSeconds,
timeoutMinutes: DEFAULTS.timeoutMinutes,
intervalSeconds: DEFAULTS.retryIntervalSeconds,
});
return [null, new QueryRunTimeoutError(elapsedSeconds * 60)];
}
return this.#getQueryResult(queryID, pageNumber, pageSize, attempts + 1);
return this.#getQueryRunInLoop({ queryRunId, attempts: attempts + 1 });
}
}

View File

@ -5,27 +5,43 @@ import {
ServerError,
UserError,
UnexpectedSDKError,
ApiError,
} from "../../errors";
import {
QueryResultJson,
QueryResultSet,
Row,
QueryResultRecord,
QueryRunStats,
QueryStatus,
GetQueryRunResultsRpcResult,
GetQueryRunRpcResult,
mapApiQueryStateToStatus,
} from "../../types";
import { QueryResultSetBuilderInput } from "../../types/query-result-set-input.type";
interface QueryResultSetBuilderData {
getQueryRunResultsRpcResult?: GetQueryRunResultsRpcResult | null;
getQueryRunRpcResult?: GetQueryRunRpcResult | null;
error:
| ApiError
| QueryRunRateLimitError
| QueryRunTimeoutError
| QueryRunExecutionError
| ServerError
| UserError
| UnexpectedSDKError
| null;
}
export class QueryResultSetBuilder implements QueryResultSet {
queryId: string | null;
status: QueryStatus | null;
columns: string[] | null;
columnTypes: string[] | null;
rows: Row[] | null;
rows: any[] | null;
runStats: QueryRunStats | null;
records: QueryResultRecord[] | null;
error:
| ApiError
| QueryRunRateLimitError
| QueryRunTimeoutError
| QueryRunExecutionError
@ -34,10 +50,10 @@ export class QueryResultSetBuilder implements QueryResultSet {
| UnexpectedSDKError
| null;
constructor(data: QueryResultSetBuilderInput) {
this.error = data.error;
const queryResultJson = data.queryResultJson;
if (!queryResultJson) {
constructor({ getQueryRunResultsRpcResult, getQueryRunRpcResult, error }: QueryResultSetBuilderData) {
this.error = error;
if (!getQueryRunResultsRpcResult || !getQueryRunRpcResult) {
this.queryId = null;
this.status = null;
this.columns = null;
@ -48,51 +64,63 @@ export class QueryResultSetBuilder implements QueryResultSet {
return;
}
this.queryId = queryResultJson.queryId;
this.status = queryResultJson.status;
this.columns = queryResultJson.columnLabels;
this.columnTypes = queryResultJson.columnTypes;
this.rows = queryResultJson.results;
this.runStats = this.#computeRunStats(queryResultJson);
this.records = this.#createRecords(queryResultJson);
this.queryId = getQueryRunRpcResult.queryRun.id;
this.status = mapApiQueryStateToStatus(getQueryRunRpcResult.queryRun.state);
this.columns = getQueryRunResultsRpcResult.columnNames;
this.columnTypes = getQueryRunResultsRpcResult.columnTypes;
this.rows = getQueryRunResultsRpcResult.rows;
this.runStats = this.#computeRunStats(getQueryRunRpcResult);
this.records = this.#createRecords(getQueryRunResultsRpcResult);
}
#computeRunStats(
queryResultJson: QueryResultJson | null
): QueryRunStats | null {
if (!queryResultJson) {
#createRecords(getQueryRunResultsRpcResult: GetQueryRunResultsRpcResult | null): QueryResultRecord[] | null {
if (!getQueryRunResultsRpcResult || !getQueryRunResultsRpcResult.columnNames || !getQueryRunResultsRpcResult.rows) {
return null;
}
let startedAt = new Date(queryResultJson.startedAt);
let endedAt = new Date(queryResultJson.endedAt);
let elapsedSeconds = (endedAt.getTime() - startedAt.getTime()) / 1000;
return {
startedAt,
endedAt,
elapsedSeconds,
recordCount: queryResultJson.results.length,
};
}
let columnNames = getQueryRunResultsRpcResult.columnNames;
#createRecords(
queryResultJson: QueryResultJson | null
): QueryResultRecord[] | null {
if (!queryResultJson) {
return null;
}
let columnLabels = queryResultJson.columnLabels;
if (!columnLabels) {
return null;
}
return queryResultJson.results.map((result) => {
return getQueryRunResultsRpcResult.rows.map((row) => {
let record: QueryResultRecord = {};
result.forEach((value, index) => {
record[columnLabels[index].toLowerCase()] = value;
row.forEach((value: any, index: number) => {
record[columnNames[index].toLowerCase()] = value;
});
return record;
});
}
#computeRunStats(getQueryRunRpcResult: GetQueryRunRpcResult): QueryRunStats {
const queryRun = getQueryRunRpcResult.queryRun;
if (
!queryRun.startedAt ||
!queryRun.endedAt ||
!queryRun.createdAt ||
!queryRun.queryStreamingEndedAt ||
!queryRun.queryRunningEndedAt
) {
throw new Error("Query has no data");
}
const createdAt = new Date(queryRun.createdAt);
const startTime = new Date(queryRun.startedAt);
const endTime = new Date(queryRun.endedAt);
const streamingEndTime = new Date(queryRun.queryStreamingEndedAt);
const queryExecEndAt = new Date(queryRun.queryRunningEndedAt);
return {
startedAt: startTime,
endedAt: endTime,
elapsedSeconds: (endTime.getTime() - startTime.getTime()) / 1000,
queryExecStartedAt: startTime,
queryExecEndedAt: queryExecEndAt,
streamingStartedAt: queryExecEndAt,
streamingEndedAt: streamingEndTime,
queuedSeconds: (startTime.getTime() - createdAt.getTime()) / 1000,
streamingSeconds: (streamingEndTime.getTime() - queryExecEndAt.getTime()) / 1000,
queryExecSeconds: (queryExecEndAt.getTime() - startTime.getTime()) / 1000,
bytes: queryRun.totalSize ? queryRun.totalSize : 0,
recordCount: queryRun.rowCount ? queryRun.rowCount : 0,
};
}
}

View File

@ -1,5 +1,5 @@
import { assert, describe, it } from "vitest";
import { QueryResultSetBuilder } from "../integrations/query-integration/query-result-set-builder";
import { QueryResultSetBuilder } from "../integrations/query-integration/query-result-set-builder-old";
import {
QueryResultSetBuilderInput,
QueryStatus,
@ -8,9 +8,7 @@ import {
QueryStatusPending,
} from "../types";
function getQueryResultSetBuilder(
status: QueryStatus
): QueryResultSetBuilderInput {
function getQueryResultSetBuilder(status: QueryStatus): QueryResultSetBuilderInput {
return {
queryResultJson: {
queryId: "test",
@ -23,13 +21,7 @@ function getQueryResultSetBuilder(
],
startedAt: "2022-05-19T00:00:00Z",
endedAt: "2022-05-19T00:01:30Z",
columnLabels: [
"block_id",
"tx_id",
"from_address",
"succeeded",
"amount",
],
columnLabels: ["block_id", "tx_id", "from_address", "succeeded", "amount"],
columnTypes: ["number", "string", "string", "boolean", "number"],
message: "",
errors: null,
@ -41,9 +33,7 @@ function getQueryResultSetBuilder(
}
describe("runStats", () => {
const queryResultSet = new QueryResultSetBuilder(
getQueryResultSetBuilder(QueryStatusFinished)
);
const queryResultSet = new QueryResultSetBuilder(getQueryResultSetBuilder(QueryStatusFinished));
it("runStats startedAt is Date type", async () => {
assert.typeOf(queryResultSet.runStats?.startedAt, "Date");
});
@ -62,9 +52,7 @@ describe("runStats", () => {
});
describe("records", () => {
const queryResultSet = new QueryResultSetBuilder(
getQueryResultSetBuilder(QueryStatusFinished)
);
const queryResultSet = new QueryResultSetBuilder(getQueryResultSetBuilder(QueryStatusFinished));
it("records length = rows length", async () => {
assert.equal(queryResultSet.records?.length, queryResultSet.rows?.length);
});
@ -95,15 +83,11 @@ describe("records", () => {
cells.forEach((cellValue, colIndex) => {
let columns = queryResultSet?.columns;
if (!columns) {
throw new Error(
"QueryResultSetBuilder columns cannot be null for tests"
);
throw new Error("QueryResultSetBuilder columns cannot be null for tests");
}
let column = columns[colIndex];
if (records === null) {
throw new Error(
"QueryResultSetBuilder records cannot be null for tests"
);
throw new Error("QueryResultSetBuilder records cannot be null for tests");
}
let record = records[rowIndex];
let recordValue = record[column];
@ -116,36 +100,26 @@ describe("records", () => {
describe("status", () => {
it("isFinished", async () => {
const queryResultSet = new QueryResultSetBuilder(
getQueryResultSetBuilder(QueryStatusFinished)
);
const queryResultSet = new QueryResultSetBuilder(getQueryResultSetBuilder(QueryStatusFinished));
assert.equal(queryResultSet?.status, QueryStatusFinished);
});
it("isPending", async () => {
const queryResultSet = new QueryResultSetBuilder(
getQueryResultSetBuilder(QueryStatusPending)
);
const queryResultSet = new QueryResultSetBuilder(getQueryResultSetBuilder(QueryStatusPending));
assert.equal(queryResultSet?.status, QueryStatusPending);
});
it("isError", async () => {
const queryResultSet = new QueryResultSetBuilder(
getQueryResultSetBuilder(QueryStatusError)
);
const queryResultSet = new QueryResultSetBuilder(getQueryResultSetBuilder(QueryStatusError));
assert.equal(queryResultSet?.status, QueryStatusError);
});
});
describe("queryID", () => {
it("queryId is set", async () => {
const queryResultSet = new QueryResultSetBuilder(
getQueryResultSetBuilder(QueryStatusFinished)
);
const queryResultSet = new QueryResultSetBuilder(getQueryResultSetBuilder(QueryStatusFinished));
assert.notEqual(queryResultSet?.queryId, null);
});
it("queryId is test", async () => {
const queryResultSet = new QueryResultSetBuilder(
getQueryResultSetBuilder(QueryStatusFinished)
);
const queryResultSet = new QueryResultSetBuilder(getQueryResultSetBuilder(QueryStatusFinished));
assert.equal(queryResultSet?.queryId, "test");
});
});

View File

@ -1,9 +0,0 @@
import { Query } from "../query.type";
import { CreateQueryResp } from "./create-query-resp.type";
import { QueryResultResp } from "./query-result-resp.type";
export interface ApiClient {
getUrl(path: string): string;
createQuery(query: Query): Promise<CreateQueryResp>;
getQueryResult(queryID: string, pageNumber: number, pageSize: number): Promise<QueryResultResp>;
}

View File

@ -1,6 +0,0 @@
export interface ApiResponse {
statusCode: number;
statusMsg: string | null;
errorMsg: string | null | undefined;
data: Record<string, any> | null;
}

View File

@ -1,10 +0,0 @@
import { ApiResponse } from "./api-response.type";
export type CreateQueryJson = {
token: string;
errors?: string | null;
};
export interface CreateQueryResp extends ApiResponse {
data: CreateQueryJson | null;
}

View File

@ -1 +0,0 @@
export type ApiError = Error;

View File

@ -1,5 +0,0 @@
export * from "./create-query-resp.type";
export * from "./errors.type";
export * from "./query-result-resp.type";
export * from "./api-client.type";
export * from "./api-response.type";

View File

@ -1,21 +0,0 @@
import { QueryStatus } from "../query-status.type";
import { ApiResponse } from "./api-response.type";
export type Row = (string | number | boolean | null)[];
export type QueryResultJson = {
queryId: string;
status: QueryStatus;
results: Row[];
startedAt: string;
endedAt: string;
columnLabels: string[];
columnTypes: string[];
message?: string;
errors?: string | null;
pageNumber: number;
pageSize: number;
};
export interface QueryResultResp extends ApiResponse {
data: QueryResultJson | null;
}

View File

@ -1,10 +1,10 @@
// Export classes from core
export { Page } from "./page";
export { PageStats } from "./page-stats";
export { QueryRun } from "./query-run";
export { QueryRequest } from "./query-request";
export { ResultFormat } from "./result-format";
export { RpcRequest, BaseRpcRequest } from "./rpc-request";
export { RpcResponse, BaseRpcResponse } from "./rpc-response";
export { SqlStatement } from "./sql-statement";
export { Tags } from "./tags";
export { Page } from "./page.type";
export { PageStats } from "./page-stats.type";
export { QueryRun } from "./query-run.type";
export { QueryRequest } from "./query-request.type";
export { ResultFormat } from "./result-format.type";
export { RpcRequest, BaseRpcRequest } from "./rpc-request.type";
export { RpcResponse, BaseRpcResponse } from "./rpc-response.type";
export { SqlStatement } from "./sql-statement.type";
export { Tags } from "./tags.type";

View File

@ -1,4 +1,4 @@
import { Tags } from "./tags";
import { Tags } from "./tags.type";
export interface QueryRequest {
id: string;

View File

@ -1,4 +1,4 @@
import { Tags } from "./tags";
import { Tags } from "./tags.type";
export interface QueryRun {
id: string;

View File

@ -1,4 +1,4 @@
import { RpcError } from "./rpc-error";
import { RpcError } from "./rpc-error.type";
export interface RpcResponse<T> {
jsonrpc: string;

View File

@ -1,5 +1,5 @@
import { ColumnMetadata } from "./column-metadata";
import { Tags } from "./tags";
import { ColumnMetadata } from "./column-metadata.type";
import { Tags } from "./tags.type";
export interface SqlStatement {
id: string;

View File

@ -1,6 +1,7 @@
export * from "./cancel-query-run";
export * from "./create-query-run";
export * from "./get-query-run-results";
export * from "./get-query-run";
export * from "./get-sql-statement";
export * from "./query-results";
export * from "./cancel-query-run.type";
export * from "./create-query-run.type";
export * from "./get-query-run-results.type";
export * from "./get-query-run.type";
export * from "./get-sql-statement.type";
export * from "./query-results.type";
export * from "./core";

View File

@ -1,10 +1,8 @@
export * from "./query.type";
export * from "./query-defaults.type";
export * from "./sdk-defaults.type";
export * from "./query-status.type";
export * from "./query-result-set.type";
export * from "./query-result-set-input.type";
export * from "./query-run-stats.type";
export * from "./query-result-record.type";
export * from "./sleep-config.type";
export * from "./api";
export * from "./compass";

View File

@ -1,8 +0,0 @@
export type QueryDefaults = {
ttlMinutes: number;
cached: boolean;
timeoutMinutes: number;
retryIntervalSeconds: number;
pageSize: number;
pageNumber: number;
};

View File

@ -1,21 +0,0 @@
import {
QueryRunExecutionError,
QueryRunRateLimitError,
QueryRunTimeoutError,
ServerError,
UserError,
UnexpectedSDKError,
} from "../errors";
import { QueryResultJson } from "./api/query-result-resp.type";
export type QueryResultSetBuilderInput = {
queryResultJson: QueryResultJson | null;
error:
| QueryRunExecutionError
| QueryRunRateLimitError
| QueryRunTimeoutError
| ServerError
| UserError
| UnexpectedSDKError
| null;
};

View File

@ -1,4 +1,3 @@
import { Row } from "./api";
import {
QueryRunExecutionError,
QueryRunRateLimitError,
@ -6,6 +5,7 @@ import {
ServerError,
UserError,
UnexpectedSDKError,
ApiError,
} from "../errors";
import { QueryRunStats } from "./query-run-stats.type";
import { QueryStatus } from "./query-status.type";
@ -25,7 +25,7 @@ export interface QueryResultSet {
columnTypes: string[] | null;
// The results of the query
rows: Row[] | null;
rows: any[] | null;
// Summary stats on the query run (i.e. the number of rows returned, the elapsed time, etc)
runStats: QueryRunStats | null;
@ -35,6 +35,7 @@ export interface QueryResultSet {
// If the query failed, this will contain the error
error:
| ApiError
| QueryRunRateLimitError
| QueryRunTimeoutError
| QueryRunExecutionError

View File

@ -2,5 +2,13 @@ export type QueryRunStats = {
startedAt: Date;
endedAt: Date;
elapsedSeconds: number;
queryExecStartedAt: Date;
queryExecEndedAt: Date;
streamingStartedAt: Date;
streamingEndedAt: Date;
queuedSeconds: number;
streamingSeconds: number;
queryExecSeconds: number;
bytes: number; // the number of bytes returned by the query
recordCount: number;
};

View File

@ -2,3 +2,22 @@ export const QueryStatusFinished = "finished";
export const QueryStatusPending = "pending";
export const QueryStatusError = "error";
export type QueryStatus = "finished" | "pending" | "error";
export function mapApiQueryStateToStatus(state: string): QueryStatus {
switch (state) {
case "QUERY_STATE_READY":
return QueryStatusPending;
case "QUERY_STATE_RUNNING":
return QueryStatusPending;
case "QUERY_STATE_STREAMING_RESULTS":
return QueryStatusPending;
case "QUERY_STATE_FAILED":
return QueryStatusError;
case "QUERY_STATE_CANCELED":
return QueryStatusError;
case "QUERY_STATE_SUCCESS":
return QueryStatusFinished;
default:
throw new Error(`Unknown query state: ${state}`);
}
}

View File

@ -1,14 +1,26 @@
export type Query = {
// SQL query to execute
sql: string;
// the maximum age of the query results in minutes you will accept, defaults to zero
maxAgeMinutes?: number;
// The number of minutes to cache the query results
ttlMinutes?: number;
// An override on the cahce. A value of true will reexecute the query.
// An override on the cache. A value of true will reexecute the query.
cached?: boolean;
// The number of minutes until your query time out
// The number of minutes until your query times out
timeoutMinutes?: number;
// The number of records to return
pageSize?: number;
// The page number to return
pageNumber?: number;
// The number of seconds to use between retries
retryIntervalSeconds?: number | string;
// The SDK package used for the query
sdkPackage?: string;
// The SDK version used for the query
sdkVersion?: string;
// The data source to execute the query against
dataSource?: string;
// The owner of the data source
dataProvider?: string;
};

View File

@ -0,0 +1,14 @@
export type SdkDefaults = {
apiBaseUrl: string;
ttlMinutes: number;
maxAgeMinutes: number;
dataSource: string;
dataProvider: string;
cached: boolean;
timeoutMinutes: number;
retryIntervalSeconds: number;
pageSize: number;
pageNumber: number;
sdkPackage: string;
sdkVersion: string;
};